22、汇编语言调试与指令语法全解析

汇编语言调试与指令语法全解析

汇编语言调试与指令语法全解析

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 的官方指令手册。这些手册提供了最全面和准确的信息,但内容较多,需要有针对性地查阅。

通过不断学习和实践,相信大家能够熟练掌握汇编语言的调试技巧和指令语法,为深入学习计算机底层原理和进行高效编程打下坚实的基础。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值