汇编语言调试与指令语法全解析
1. GDB 调试基础操作
1.1 查看寄存器值
在调试过程中,我们常常需要查看寄存器的值。要查看寄存器的值,只需在 GDB 中输入以下命令:
info registers
这个命令会提供所有寄存器的完整信息。当前,大部分寄存器初始化为零,因为第一条指令尚未执行,所以
%rax
也为零。寄存器的值会以两列形式呈现,左列是原始值,右列是调试器对该值的解释。左列的值是“官方”值,但右列可能会提供额外的重要上下文信息,比如将值转换为相对于某个地址的偏移量,或者将十六进制数转换为十进制数等。
1.2 执行下一条指令
若要执行下一条指令,可在 GDB 中输入:
stepi
此命令会使程序执行下一条机器指令。再次执行
disassemble
命令,箭头会指向代码的下一行。若执行
info registers
命令,会发现
%rax
现在被设置为
0x3c
。多次执行
stepi
命令,程序会逐步执行后续指令,当执行系统调用时,程序会退出,调试器会报告如下信息:
[Inferior 1 (process 152) exited with code 03]
这里的 “Inferior 1” 是 GDB 对我们进程的内部名称,退出代码是程序返回给 shell 的值。
1.3 管理断点
除了逐行执行代码,我们还可以在任意位置设置断点。如果调试器已知某个符号(因为该符号使用
.globl
声明),就可以像为
_start
设置断点那样,直接在这些位置添加断点,并且可以添加任意数量的断点。
-
查看断点列表
:使用
info break
命令可获取所有已定义断点的列表,每个断点都有一个编号,调试器通过该编号引用断点。
-
删除断点
:使用
delete NUMBER
命令删除断点,其中
NUMBER
是要删除的断点编号(可通过
info break
命令查看)。需要注意的是,删除断点后不会重新编号,即使重新添加断点,也会使用新的编号。
-
在指定地址添加断点
:可以在汇编语言函数的任意位置添加断点。执行
disassemble
命令时,调试器会给出每条指令的地址。要在某条指令前添加断点,可使用
break *ADDRESS
命令,其中
ADDRESS
是该指令的内存地址(由 GDB 提供)。也可以使用 GDB 给出的相对于函数起始位置的偏移量,例如,当反汇编
myexit.s
时,第二条指令的偏移量为
+7
,那么可以使用
break *_start+7
命令在该指令处添加断点。
-
在未到达的函数内部添加断点
:若要在尚未到达的函数内部添加断点,可以使用
disassemble MYFUNCTION
命令反汇编该函数,其中
MYFUNCTION
是要反汇编的代码标签。也可以使用地址代替函数名,但不要像在
break
命令中那样在地址前加
*
。
1.4 打印值
1.4.1 打印寄存器值
除了显示所有寄存器的值,还可以使用
print
命令单独显示某个寄存器的值。不过在 GDB 中,打印寄存器时需要在寄存器名前加
$
而不是
%
。例如,要打印
%rax
的内容,可输入:
print $rax
还可以通过添加格式代码指定值的显示格式:
-
print/d
:以十进制形式显示值。
-
print/x
:以十六进制形式显示值。
-
print/t
:以二进制形式显示值。
-
print/c
:以字符形式显示值。
-
print/f
:以浮点数形式显示值。
1.4.2 打印内存中的值
打印内存中的值时,需要告诉计算机要打印的值的类型。由于 GNU 调试器主要围绕 C 编程语言构建,所以需要将值“转换”为 GDB 能理解的类型。例如,若有一个名为
myQuadword
的内存位置包含一个四字(quadword),可以使用以下命令打印其值:
print/d (long long)myQuadword
这里的
(long long)
是代表四字的 C 类型名称,
(long long)myQuadword
将值转换为指定类型。基本的 C 类型包括:
-
char
(单字节)
-
short
(2 字节)
-
int
或
long
(4 字节)
-
long long
(8 字节)
-
float
(4 字节浮点数)
-
double
(8 字节浮点数)
-
int *
(指向
int
类型的 8 字节指针)
-
void *
(指向未确定类型的 8 字节指针)
-
void **
(指向 8 字节指针的 8 字节指针,该 8 字节指针指向未确定类型)
若要解引用指针(查找指针指向的值),可以添加
*
。假设
%rax
包含一个指向整数的指针,可使用以下命令查找该值:
print/d *(int *)$rax
2. Nasm(Intel)汇编语言语法
2.1 大小写规范
在 AT&T 语法中,指令名和寄存器名通常以小写形式书写;而在 Nasm 语法中,惯例是使用大写。例如,
mul
指令在 Nasm 语法中为
MUL
。虽然这只是一种惯例而非强制要求,但遵循此惯例有助于我们明确使用的是哪种语法。
2.2 寄存器命名和立即数前缀
在 AT&T 语法中,寄存器名前加
%
符号,而在 Nasm 语法中,寄存器名无需前缀。例如,在 Nasm 语法中,
%rax
直接写为
RAX
。此外,Nasm 语法中的立即数没有
$
前缀,数字
1
就直接写为
1
。
2.3 操作数顺序
Nasm 语法和 AT&T 语法在操作数顺序上有很大差异。在 Nasm 语法中,每个指令的操作数顺序与 AT&T 语法相反,目标操作数放在首位,其余参数(如果有)跟在后面。例如,AT&T 语法中的
mov %rax, %rdx
指令,在 Nasm 语法中为
MOV RDX, RAX
。
2.4 指定内存寻址模式
Nasm 语法的内存寻址模式与 AT&T 语法不同。在 Nasm 语法中,内存引用总是用方括号括起来,形式如下:
[BASEREG + IDXREG*MULTIPLIER + VALUE]
这一形式直观地展示了如何计算地址,并且可以根据需要重新排列方括号内的值以提高清晰度。而 AT&T 语法中,内存地址引用的完整格式为:
VALUE(BASEREG, IDXREG, MULTIPLIER)
计算最终地址的公式为:
address = VALUE + BASEREG + IDXREG * MULTIPLIER
2.5 指定操作数大小
在 AT&T 语法中,通常会在指令后添加后缀来表示使用的数据“大小”,如
q
表示四字(64 位)值,
l
表示 32 位值,
w
表示 16 位值,
b
表示单字节值。在 Nasm 语法中,大小信息应用于操作数而非指令。由于操作数大小不明确的情况通常出现在指针上(寄存器大小比较明显,立即数大小可从上下文推断),这些操作数会以
SIZE PTR
为前缀,其中
SIZE
可以是
QWORD
、
DWORD
、
WORD
或
BYTE
。例如,要将数字
1
移动到由标签
myvalue
指定的一个字节内存中,可使用以下指令:
MOV BYTE PTR [myvalue], 1
3. 常见 x86 - 64 指令
3.1 数据移动指令
| 指令 | 含义 |
|---|---|
mov SRC, DST
|
将
SRC
中存储的值移动到
DST
指定的位置。
|
lea SRC, DST
|
将
SRC
指示的地址移动到
DST
指定的位置。
|
xchg SRC, DST
|
交换
SRC
和
DST
中的值。
|
bswap DST
| 反转目标寄存器(仅 32 位或 64 位寄存器)的字节顺序。 |
3.2 算术指令
| 指令 | 含义 |
|---|---|
add SRC, DST
|
将
SRC
的内容加到
DST
中,并将结果存储在
DST
中。
|
sub SRC, DST
|
从
DST
中减去
SRC
的内容,并将结果存储在
DST
中。
|
mul SRC
|
将
%rax
乘以
SRC
,并将结果存储在组合寄存器
%rax
和
%rdx
中,将所有值视为无符号数。
|
div SRC
|
将组合的
%rax
和
%rdx
除以
SRC
,结果存储在
%rax
中,余数存储在
%rdx
中。若仅使用
%rax
存储被除数,最好显式将
%rdx
设置为零,将所有值视为无符号数。
|
inc DST
|
将
DST
指定位置的值加 1。
|
dec DST
|
将
DST
指定位置的值减 1。
|
idiv SRC
|
与
div
类似,但执行有符号整数除法。
|
imul SRC
|
与
mul
类似,但执行有符号整数乘法。
|
adc SRC, DST
|
带进位加法,与
add
类似,但如果进位标志置位,则再加 1。
|
3.3 栈操作指令
| 指令 | 含义 |
|---|---|
push SRC
|
将给定值压入栈中,将
%rsp
减去
SRC
中值的大小,然后将
SRC
中存储的值移动到
%rsp
新指定的位置。
|
pop DST
|
从栈的当前位置弹出值,将
%rsp
指定位置存储的值移动到
DST
中,然后将
%rsp
加上
DST
的大小。
|
3.4 比较、分支和循环指令
| 指令 | 含义 |
|---|---|
cmp SRC, DST
|
通过执行虚拟减法(从
DST
中减去
SRC
)比较
SRC
和
DST
,但仅设置标志位。
|
test SRC, DST
| 对两个参数执行逻辑与操作,然后根据结果设置标志位。 |
jmp DST
|
跳转到
DST
指定的值处。
|
jmp (DST)
|
跳转到(设置程序计数器)
DST
寄存器指定的地址。
|
jmp *DST
|
间接跳转,查找
DST
指定地址中的值,并将程序计数器设置为该地址。
|
jCC DST
|
条件跳转,
CC
是条件码,用于指定根据
%eflags
寄存器的哪些条件进行跳转。
|
loop DST
|
递减
%rcx
,如果
%rcx
不为零,则跳转到
DST
。
|
loopne DST
|
与
loop
类似,但如果零标志(ZF)置位,则不跳转。
|
loope DST
|
与
loop
类似,但如果零标志(ZF)未置位,则不跳转。
|
3.5 状态标志操作指令
| 指令 | 含义 |
|---|---|
sahf
|
将
%ah
的内容存储到
%eflags
中。
|
lahf
|
将
%eflags
的内容存储到
%ah
中。
|
clc
| 清除进位标志(CF)。 |
setc
| 设置进位标志(CF)。 |
cld
| 清除方向标志(DF)。 |
setd
| 设置方向标志(DF)。 |
3.6 位操作指令
| 指令 | 含义 |
|---|---|
and SRC, DST
|
对
SRC
和
DST
中的每一位执行逻辑与操作,并将结果存储在
DST
中。
|
or SRC, DST
|
对
SRC
和
DST
中的每一位执行逻辑或操作,并将结果存储在
DST
中。
|
xor SRC, DST
|
对
SRC
和
DST
中的每一位执行逻辑异或操作,并将结果存储在
DST
中。
|
nor SRC, DST
|
对
SRC
和
DST
中的每一位执行逻辑或非操作,并将结果存储在
DST
中。
|
not DST
|
对
DST
中的每一位执行逻辑非操作,并将结果存储在
DST
中。
|
shl DST
|
将
DST
的位向左移动(朝向最高有效位),移出的位被丢弃,从右边移入的“新”位为零。
|
shr DST
|
将
DST
的位向右移动(朝向最低有效位),移出的位被丢弃,从左边移入的“新”位为零。
|
rol DST
|
将
DST
的位向左旋转(移动),向左移出的位用作从右边移入的“新”位。
|
ror DST
|
将
DST
的位向右旋转(移动),向右移出的位用作从左边移入的“新”位。
|
bsf SRC, DST
|
从
SRC
中查找第一个非零位(从最低有效位开始,将最低有效位视为位 0),并将该位的索引存储在
DST
中,
DST
必须是寄存器。
|
bsr SRC, DST
|
从
SRC
中查找第一个非零位(从最高有效位开始),并将该位的索引存储在
DST
中,
DST
必须是寄存器。
|
lzcnt SRC, DST
|
计算
SRC
中值的前导零的数量,并将该计数存储在
DST
中,
DST
必须是寄存器。
|
3.7 调用相关指令
| 指令 | 含义 |
|---|---|
syscall
| 调用“系统调用”,将控制权转移到操作系统。 |
call DST
|
调用一个函数,将下一条指令的地址(返回地址)压入栈中,并将控制权转移到
DST
。
|
enter $SIZE, $0
|
设置栈帧,具体操作包括:(a) 将
%rbp
压入栈中;(b) 将
%rsp
复制到
%rbp
;(c) 从
%rsp
中减去
SIZE
字节,为栈上的局部变量腾出空间。
|
leave
|
拆除栈帧,具体操作包括:(a) 将
%rbp
复制到
%rsp
;(b) 从栈中弹出
%rbp
。
|
ret
| 从函数调用返回,从栈中弹出返回地址,并将控制权转移到该地址。 |
3.8 字符串和内存块指令
| 指令 | 含义 |
|---|---|
movs
|
从
%rsi
指定的地址加载一个值,并将该值存储在
%rdi
指定的地址中,然后根据方向标志(DF)将
%rsi
和
%rdi
移动到“下一个”地址。
|
cmps
|
从
%rsi
和
%rdi
指定的地址加载值并进行比较,设置
%eflags
,然后根据方向标志(DF)将
%rsi
和
%rdi
移动到“下一个”地址。
|
scas
|
从
%rdi
指定的地址加载一个值,并将该值与
%rax
进行比较(修改
%eflags
),然后根据方向标志(DF)将
%rdi
移动到“下一个”地址。该指令可以使用
repne
作为前缀,只要值不相等就会重复执行。
|
rep
|
可作为前面指令的前缀,添加该前缀会使给定指令重复执行,每次递减
%rcx
,当
%rcx
为零时停止重复。
|
repe
|
与
rep
类似,但如果零标志(ZF)置位,则会终止。
|
repne
|
与
rep
类似,但如果零标志(ZF)未置位,则会终止。
|
3.9 SSE 指令
SSE 指令通常使用 XMM 寄存器作为至少一个参数,操作中通常会隐含这一点。这些操作涉及的可用类型包括:
- 单值单精度浮点数(ss)
- 单值双精度浮点数(sd)
- 单值双字整数(si)
- 单值双四字整数(dq)
- 打包单精度浮点数(ps)
- 打包双精度浮点数(pd)
- 打包双字整数(pi 或有时为 pd)
- 打包四字整数(pq)
- 打包字节整数(pb)
- 打包字整数(pw)
需要注意的是,在并行整数加法指令中,
p
和大小(即
b
、
w
、
d
和
q
)由指令名分隔。例如,不是
addpq
,而是
paddq
。具体指令如下:
| 指令 | 含义 |
| ---- | ---- |
|
movsd SRC, DST
| 将
SRC
移动到
DST
的低 8 字节中,不影响高 8 字节。 |
|
movss SRC, DST
| 将
SRC
移动到
DST
的低 4 字节中,不影响更高字节。 |
|
movaps SRC, DST
| 将一个 128 位的值移动到
DST
中,如果值未对齐会引发异常,针对浮点值进行了优化。 |
|
movups SRC, DST
| 将一个 128 位的值移动到
DST
中,即使值未对齐也不会引发异常,针对浮点值进行了优化。 |
|
movdqa SRC, DST
| 将一个 128 位的值移动到
DST
中,如果值未对齐会引发异常,针对整数值进行了优化。 |
|
movdqu SRC, DST
| 将一个 128 位的值移动到
DST
中,即使值未对齐也不会引发异常,针对整数值进行了优化。 |
|
pslldq VAL, DST
| 将
SRC
中的值向左移动
VAL
字节。 |
|
psrldq VAL, DST
| 将
SRC
中的值向右移动
VAL
字节。 |
|
addXX SRC, DST
| 对
SRC
和
DST
进行并行加法,并将结果存储在
DST
中(仅适用于浮点值)。 |
|
subXX SRC, DST
| 从
DST
中并行减去
SRC
,并将结果存储在
DST
中(仅适用于浮点值)。 |
|
mulXX SRC, DST
| 对
SRC
和
DST
进行并行乘法,并将结果存储在
DST
中(仅适用于浮点值)。 |
|
divXX SRC, DST
| 将
DST
并行除以
SRC
,并将结果存储在
DST
中(仅适用于浮点值)。 |
|
paddX SRC, DST
| 对
SRC
和
DST
进行并行加法,并将结果存储在
DST
中(仅适用于整数值)。 |
|
psubX SRC, DST
| 从
DST
中并行减去
SRC
,并将结果存储在
DST
中(仅适用于整数值)。 |
|
pmulX SRC, DST
| 对
SRC
和
DST
进行并行乘法,并将结果存储在
DST
中(仅适用于整数值)。 |
通过以上对汇编语言调试和指令语法的详细介绍,我们可以更深入地理解汇编语言的调试过程和不同语法的特点,以及常见指令的使用方法,这对于进行汇编语言编程和调试具有重要的指导意义。
4. 汇编语言调试与指令运用示例
4.1 调试流程示例
下面通过一个简单的示例来展示如何运用前面提到的 GDB 调试操作。假设我们有一个简单的汇编程序,其功能是将一个值从一个寄存器移动到另一个寄存器,然后退出程序。
_start:
mov $0x3c, %rax ; 设置系统调用号为退出
mov $0, %rdi ; 设置退出状态码为 0
syscall ; 调用系统调用退出程序
以下是调试该程序的具体步骤:
1.
启动 GDB
:在终端中输入
gdb <可执行文件名>
启动 GDB 调试器。
2.
设置断点
:使用
break _start
命令在
_start
标签处设置断点。
3.
运行程序
:输入
run
命令开始运行程序,程序会在断点处停止。
4.
查看寄存器值
:使用
info registers
命令查看当前寄存器的值。此时,
%rax
和
%rdi
应该为初始值。
5.
单步执行
:使用
stepi
命令逐行执行指令。每执行一条指令后,再次使用
info registers
命令查看寄存器值的变化。
6.
继续执行
:如果想让程序继续执行到下一个断点或结束,可以使用
continue
命令。
4.2 不同语法指令转换示例
我们以 AT&T 语法和 Nasm 语法之间的指令转换为例,进一步说明两种语法的差异。
4.2.1 数据移动指令
-
AT&T 语法
:
mov %rax, %rdx -
Nasm 语法
:
MOV RDX, RAX
4.2.2 算术指令
-
AT&T 语法
:
add $1, %rax -
Nasm 语法
:
ADD RAX, 1
4.2.3 栈操作指令
-
AT&T 语法
:
push %rbp -
Nasm 语法
:
PUSH RBP
4.3 指令运用流程图
下面是一个简单的程序执行流程图,展示了程序从开始到结束的基本流程,包括函数调用、系统调用等操作。
graph TD
A[程序开始] --> B[初始化寄存器]
B --> C{是否有函数调用?}
C -- 是 --> D[调用函数]
D --> E[执行函数体]
E --> F[返回调用点]
C -- 否 --> G{是否有系统调用?}
G -- 是 --> H[执行系统调用]
H --> I[程序结束]
G -- 否 --> J[继续执行其他指令]
J --> C
5. 总结与建议
5.1 总结
本文详细介绍了汇编语言调试和指令语法的相关知识。在调试方面,我们学习了如何使用 GDB 进行基本的调试操作,包括查看寄存器值、单步执行指令、设置断点和打印值等。在指令语法方面,对比了 AT&T 语法和 Nasm(Intel)语法的差异,包括大小写规范、寄存器命名、操作数顺序、内存寻址模式和操作数大小指定等。此外,还列举了常见的 x86 - 64 指令,涵盖了数据移动、算术、栈操作、比较、分支、循环、状态标志操作、位操作、调用相关、字符串和内存块以及 SSE 等多个方面。
5.2 建议
- 实践出真知 :汇编语言是一门实践性很强的学科,建议多编写一些简单的汇编程序,并使用 GDB 进行调试,通过实践加深对调试操作和指令语法的理解。
- 熟悉不同语法 :虽然 AT&T 语法在 Linux 内核和 GCC 中广泛使用,但了解 Nasm 语法对于学习 Intel 手册中的新指令非常有帮助。可以尝试将一些简单的 AT&T 语法程序转换为 Nasm 语法,反之亦然,以提高对不同语法的熟练度。
- 查阅官方手册 :对于指令的详细功能和使用注意事项,建议查阅 AMD 和 Intel 的官方指令手册。这些手册提供了最全面和准确的信息,但内容较多,需要有针对性地查阅。
通过不断学习和实践,相信大家能够熟练掌握汇编语言的调试技巧和指令语法,为深入学习计算机底层原理和进行高效编程打下坚实的基础。
汇编语言调试与指令语法全解析
超级会员免费看
988

被折叠的 条评论
为什么被折叠?



