汇编语言简易教程(6):工具链以及调试器
汇编语言简易教程(6):工具链以及调试器
通常,用于创建程序的编程工具集称为工具链。
就本文而言,工具链包括以下内容
- 汇编器
Assembler
- 连接器
Linker
- 加载器
Loader
- 调试器
Debugger
虽然工具链有很多选项,但本文使用了一组相当标准的开源工具,这些工具可以很好地协同工作并完全支持 x86 64 位环境.
概览
从广义上讲,汇编、链接和加载过程是将程序员编写的源文件转换为可执行程序的过程.
汇编器将人类可读的源文件转换为目标文件。
在最基本的形式中,目标文件由链接器转换为可执行文件。
加载器会将可执行文件加载到内存中.
典型流程
Assembler(汇编器)
assembler 是一个程序,它将读取汇编语言输入文件并将代码转换为机器语言二进制文件。
输入文件是包含人类可读形式的汇编语言指令的汇编语言源文件。
机器语言输出称为
object file
。作为此过程的一部分,注释将被删除,变量名称和标签将被转换为适当的地址(根据 CPU 在执行期间的要求)
汇编器命令
yasm -g dwarf2 -f elf64 example.asm -l example.lst
-
-g dwarf2
:这是调试信息格式的选项。dwarf2
是一种调试信息格式,用于在生成的目标文件中包含调试信息,使调试器能够更好地理解程序的结构。dwarf
是目前最常见的调试格式之一,dwarf2
表示这种格式的一个版本。-
-f elf64
:这个选项指定了目标文件的格式。elf64
表示生成的是64位环境下的 ELF (Executable and Linkable Format) 格式的目标文件。ELF 是一种常用的文件格式,用于定义程序、库或者其他一些二进制文件的结构。-
example.asm
:这是输入文件的名称,即你要汇编的源代码文件。-
-l example.lst
:这个选项告诉yasm
需要生成一个列表文件(.lst
),其中包含源代码和对应的机器码。example.lst
是生成的列表文件的名称。综上所述,整个命令的作用是:使用
yasm
汇编器,将example.asm
文件中的源代码汇编成64位 ELF 格式的目标文件,并生成调试信息(使用dwarf2
格式),同时输出一个包含汇编源代码和机器码的列表文件example.lst
。这对于开发者调试汇编程序是非常有用的。List文件
汇编器的一个可选项是创建List文件.
List文件显示了指令的行号、相对地址、机器语言版本(包括变量引用)以及原始源代码.
List文件在Debug时非常有作用
data定义
36 00000009 40660301 dVar1 dd 17000000 37 0000000D 40548900 dVar2 dd 9000000 38 00000011 00000000 dResult dd 0
36
指代行号
0x00000009
表示该数据存储的地址, 即指针位置, 因为dVar1是两字长度, 占用4个byte, 所以下一个变量dVar2
的起始位置是0x0000000D
0x40660301
表示这个变量的实际值, 16进制表示, 存储在内存中, 17000000的实际表示是0x01036640
但是因为内容中的存储是小端存储, 所以每两位颠倒.参考下图
text示例
95 last: 96 0000005A 48C7C03C000000 mov rax, SYS_exit 97 00000061 48C7C300000000 mov rdi, EXIT_SUCCESS 98 00000068 0F05 syscall
- 96 行号
-
0x0000005A
将放置代码行的相对地址。-
0x48C7C03C000000
是 CPU 读取和理解的指令的机器语言版本(十六进制)。该行的其余部分是原始的汇编语言源指令。- 标签
last
:没有机器语言指令,因为该标签用于引用特定地址并且不是可执行指令二阶段汇编
汇编器将读取源文件并将程序员输入的每个汇编语言指令转换为CPU知道是该指令的一组1和0。
1 和 0 被称为机器语言。汇编语言指令和二进制机器语言之间存在一一对应的关系。这种关系意味着可执行文件形式的机器语言可以转换回人类可读的汇编语言。
当然,注释、变量名称和标签名称都丢失了,因此生成的代码可能非常难以阅读。
当汇编器读取每一行汇编语言时,它会为该指令生成机器代码。这对于不执行跳转的指令非常有效。但是,对于可能更改控制流的指令(例如 IF 语句、无条件跳转),汇编器无法转换指令。
例如,给出以下代码片段
这被称为 forward reference (我不知道该如何翻译, 或许是
前向引用
), 如果汇编器只读取一次, 此时其完全不知道skipRest
的位置甚至是否有定义skipRest
此问题可以通过两次读取汇编源码文件来解决.
第一次
在不同的汇编器上的表现可能不太一致, 但是可以概括为:
- 创建符号表
- 展开宏
- 计算常量表达式
展开宏
会在11章节细聊
计算常量表达式
常量表达式是完全由常量组成的表达式。由于表达式只是常量,因此可以在汇编时对其进行完全评估。例如,假设定义了常量 BUFF,则以下指令包含常量表达式:
mov rax, BUFF+5
这种类型的常量表达式通常用在大型或复杂的程序中
地址被分配给程序中的所有语句。符号表是所有程序符号、变量名和程序标签及其在程序中各自地址的列表或表
第二次
在不同的汇编器上的表现可能不太一致, 但是可以概括为:
- 最终生成代码
- 创建List文件(如果需要)
- 创建Object文件
“代码生成”是指将程序提供的汇编语言指令转换为CPU可执行的机器语言指令.由于一对一的对应关系,这可以针对在第一遍或第二遍中不使用符号的指令来完成.
应该注意的是,根据汇编器设计,大部分代码生成可能在第一遍完成,或者全部在第二遍完成。无论哪种方式,最终生成都是在第二遍中执行的。这将需要使用符号表来检查程序符号并从表中获取适当的地址
列表文件虽然是可选的,但对于调试很有用。如果需要,它将在第二遍时生成.如果没有错误,则在第二遍时创建最终目标文件。
Linker(链接器)
定义
组合一个或多个object file成为一个可执行文件. 也可能会将用户或系统的lib包含起来.
使用
以GNU的ld作为参考
一个典型的ld命令:
ld -g -o example example.o
-g
表示包含debug信息, 在最终的可执行文件中. 会增加可执行文件的大小, 但为了有效的调试是必要的
-o
指定可执行文件的名称, 如果文件名称被忽略, 那么会默认成a.out
. ├── a.out // 未添加-o 的 默认的结果 ├── eg // 添加 -o 指定的结果 ├── eg.asm ├── eg.o └── example.lst
链接多个文件
ld -g -o example main.o funcs.o
需要在args中指定所有需要的
*.o
链接的过程
作为组合目标文件的一部分,链接器必须根据需要调整可重定位地址。
假设有两个源文件,主源文件和次源文件包含一些函数,这两个源文件都已被组装成目标文件main.o和funcs.o。
汇编每个文件时,将使用外部汇编器指令声明对正在汇编的文件外部的例程的调用。该代码不可用于外部引用,此类引用在目标文件中被标记为外部。
列表文件将针对此类可重定位地址标记为 R。链接器必须满足外部引用。此外,外部引用的最终位置必须放置在代码中。
示例
这里,函数 fnc1 位于 main.o 目标文件外部,并用 R 标记。
实际函数 fnc1 位于 funcs.o 文件中,该文件从 0x0(在文本部分中)开始其相对寻址,因为它不知道关于主要代码。
当目标文件合并时,fnc1 的原始相对地址(显示为 0x0100:)将更改为其在可执行文件中的最终地址(显示为 0x0400:)。
链接器必须将此最终地址插入到 main 中的调用语句中(显示为 call 0x0400:),以完成链接过程并确保函数调用正常工作。
代码和数据的所有可重定位地址都会发生这种情况。
请注意:
实际调用的
main.o
并不知道func.o的内容, 实际是通过代码位置的地址调用的来实现. 所以linker的一个作用就是处理各个函数/变量的地址, 并准确的处理其调用.动态链接
Linux 操作系统支持动态链接,它允许推迟某些符号的解析,直到程序正在执行为止
实际指令不会放置在可执行文件中,而是在需要时在运行时解析和访问:
- 常用的库(例如标准系统库)只能存储在一个位置,而不是在每个二进制文件中重复
- 如果库函数中的错误得到纠正,所有动态使用它的程序都将从纠正中受益(在下一次执行时)。否则,通过静态链接使用此功能的程序必须在应用更正之前重新链接
缺点:
- 不兼容的更新库将破坏依赖于先前版本库的行为的可执行文件
- 程序及其使用的库可以作为包进行认证(例如,正确性、文档要求或性能),但不能替换组件
在 Linux/Unix 中,动态链接的目标文件通常具有 .so(共享对象)扩展名。在 Windows 中,它是 .dll(动态链接库)扩展名。动态链接的更多细节超出了本文的范围.
Loader(加载器)
加载器是操作系统的一部分组件, 将程序从次级存储载入主存.
加载器将尝试查找,如果找到,则读取格式正确的可执行文件,创建一个新的进程. 并将代码加载到内存中并将程序标记为准备执行。操作系统调度程序将决定执行哪个进程以及何时执行该进程.
Debugger(调试器)
调试器通常用于控制程序的执行以执行测试或调试工作.
使用的调试器是 GNU DDD 调试器,它为 GNU 命令行调试器 gdb 提供可视化前端。
DDD 网站和文档在第 1 章简介的参考资料部分中注明。由于调试器的复杂性和重要性,为调试提供了单独的一章
使用
ddd ./eg
设置
断点
运行
点击
**run**
单步调试
Next
命令将执行到下一条指令。这包括在必要时执行整个函数。步骤命令将执行一步,如有必要,单步执行函数。
对于单个非功能指令,
Next
命令和Step
命令之间没有区别查看寄存器状态
Status→ Registers
默认只显示整数寄存器, 可以打开显示浮点数寄存器.