3.2.2 代码示例
3.2.2 代码示例
假设我们写了一个 C 语言代码文件 mstore.c,包含如下的函数定义:
long mult2(long, long);
void multstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}在命令行上使用 “-S” 选项,就能看到 C 语言编译器产生的汇编代码:
linux> gcc -Og -S mstore.c
这会使 GCC 运行编译器,产生一个汇编文件 mstore.s,但是不做其他进一步的工作。(通常情况下,它还会继续调用汇编器产生目标代码文件。)
汇编代码文件包含各种声明,包括下面几行:
multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret上面代码中每个缩进去的行都对应于一条机器指令。比如,pushq 指令表示应该将寄存器 %rbx 的内容压入程序栈中。这段代码中已经除去了所有关于局部变量名或数据类型的信息。
如果我们使用 “-c” 命令行选项,GCC 会编译并汇编该代码:
linux> gcc -Og -c mstore.c
这就会产生目标代码文件 mstore.o,它是二进制格式的,所以无法直接查看。1368 字节的文件 mstore.o 中有一段 14 字节的序列,它的十六进制表示为:
53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3
这就是上面列出的汇编指令对应的目标代码。从中得到一个重要信息,即机器执行的程序只是一个字节序列,它是对一系列指令的编码。机器对产生这些指令的源代码几乎一无所知。
旁注:如何展示程序的字节表示
要展示程序(比如说
mstore)的二进制目标代码,我们用反汇编器(后面会讲到)确定该过程的代码长度是 14 字节。然后,在文件mstore.o上运行 GNU 调试工具 GDB,输入命令:(gdb) x/14xb multstore这条命令告诉 GDB 显示(简写为
x)从函数multstore所处地址开始的 14 个十六进制格式表示(也简写为x)的字节(简写为b)。你会发现,GDB 有很多有用的特性可以用来分析机器级程序,我们会在 3.10.2 节中讨论。
要查看机器代码文件的内容,有一类称为反汇编器(disassembler)的程序非常有用。这些程序根据机器代码产生一种类似于汇编代码的格式。在 Linux 系统中,带 “-d” 命令行标志的程序 OBJDUMP(表示 “object dump”)可以充当这个角色:
linux> objdump -d mstore.o
结果如下(这里,我们在左边增加了行号,在右边增加了斜体表示的注解):
Disassembly of function multstore in binary file mstore.o
1 0000000000000000 <multstore>:
Offset Bytes Equivalent assembly language
2 0: 53 push %rbx
3 1: 48 89 d3 mov %rdx,%rbx
4 4: e8 00 00 00 00 callq 9 <multstore+0x9>
5 9: 48 89 03 mov %rax,(%rbx)
6 c: 5b pop %rbx
7 d: c3 retq
在左边,我们看到按照前面给出的字节顺序排列的 14 个十六进制字节值,它们分成了若干组,每组有 1~5 个字节。每组都是一条指令,右边是等价的汇编语言。
其中一些关于机器代码和它的反汇编表示的特性值得注意:
- x86-64 的指令长度从 1 到 15 个字节不等。常用的指令以及操作数较少的指令所需的字节数少,而那些不太常用或操作数较多的指令所需字节数较多。
- 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一地解码成机器指令。例如,只有指令
pushq %rbx是以字节值53开头的。 - 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码。它不需要访问该程序的源代码或汇编代码。
- 反汇编器使用的指令命名规则与 GCC 生成的汇编代码使用的有些细微的差别。在我们的示例中,它省略了很多指令结尾的
q。这些后缀是大小指示符,在大多数情况中可以省略。相反,反汇编器给call和ret指令添加了q后缀,同样,省略这些后缀也没有问题。
生成实际可执行的代码需要对一组目标代码文件运行链接器,而这一组目标代码文件中必须含有一个 main 函数。假设在文件 main.c 中有下面这样的函数:
#include <stdio.h>
void multstore(long, long, long *);
int main() {
long d;
multstore(2, 3, &d);
printf("2 * 3 --> %ld\n", d);
return 0;
}
long mult2(long a, long b) {
long s = a * b;
return s;
}然后,我们用如下方法生成可执行文件 prog:
linux> gcc -Og -o prog main.c mstore.c
文件 prog 变成了 8655 个字节,因为它不仅包含了两个过程的代码,还包含了用来启动和终止程序的代码,以及用来与操作系统交互的代码。我们也可以反汇编 prog 文件:
linux> objdump -d prog
反汇编器会抽取出各种代码序列,包括下面这段:
Disassembly of function multstore in binary file prog
1 0000000000400540 <multstore>:
2 400540: 53 push %rbx
3 400541: 48 89 d3 mov %rdx,%rbx
4 400544: e8 42 00 00 00 callq 40058b <mult2>
5 400549: 48 89 03 mov %rax,(%rbx)
6 40054c: 5b pop %rbx
7 40054d: c3 retq
8 40054e: 90 nop
9 40054f: 90 nop
这段代码与 mstore.c 反汇编产生的代码几乎完全一样。其中一个主要的区别是左边列出的地址不同——链接器将这段代码的地址移到了一段不同的地址范围中。第二个不同之处在于链接器填上了 callq 指令调用函数 mult2 需要使用的地址(反汇编代码第 4 行)。链接器的任务之一就是为函数调用找到匹配的函数的可执行代码的位置。最后一个区别是多了两行代码(第 8 和 9 行)。这两条指令对程序没有影响,因为它们出现在返回指令后面(第 7 行)。插入这些指令是为了使函数代码变为 16 字节,使得就存储器系统性能而言,能更好地放置下一个代码块。