这是笔者学习《深入理解计算机系统》学习笔记,但不是从头开始。
禁止禁止套娃
程序的机器级表示
程序编码
对于C语言的程序,我们可以使用 gcc 作为编译器使用Linux命令行进行编译,我们假设有两个文件:p1.c 和 p2.c,考虑如下命令:1
gcc -Og -o p p1.c p2.c
在这里,gcc 指的是 GCC C 编译器,这是 Linux 系统上默认的编译器,亦可使用 cc 启动。后面的 -Og 代表优化的等级,优化等级较高可能会导致产生代码的严重变形,若是如此,则不利于对代码的理解。但在实际上,使用较高级别的优化能使程序的性能更优。
将源代码转换成可执行代码大约有以下的步骤:
- 首先,C 预处理器扩展源代码,将所有用 #include 指定的文件插入,并扩展所有的 #define 声明的指定宏。
- 接下来,汇编器会将源码转换成二进制目标代码文件,即上例的 p1.o 和 p2.o。目标代码是机器码的一中,包含所有指令的二进制表示,但是没有填入全局值的地址。
- 最后,链接器将两个目标代码文件与实现库函数(例如 printf )合并,并产生最终的可执行代码文件。
代码示例
考虑如下代码文件 mstore.c1
2
3
4
5
6long mult2(long, long);
void multstore(long x, long y, long *dest){
long t = mult2(x, y);
*dest = t;
}
我们使用如下命令来查看汇编代码:1
gcc -Og -S mstore.c
-S 选项能够使我们看到汇编码。这会使得 GCC 运行编译器,产生一个汇编文件 mstore.s,但并不能正常工作。(通常情况下,它还会继续调用汇编器产生目标代码文件)
汇编代码文件包含各种声明,包括:1
2
3
4
5
6
7multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
注意到它和我们所学的8086汇编代码有点类似:(这是笔者课设程序的一个部分)1
2
3
4
5
6
7Out_Enter:
push dx
lea dx, NR
mov ah, 9
int 21h
pop dx
ret
总体来讲大概还是相似的,不过值得注意的是类型确定的位置。这一点我们后面再讨论罢。(笑)
如果我们将 S 换成 C 使用如下命令:1
gcc- Og -c mstore.c
就会产生目标代码文件 mstore.o,它是二进制格式的,无法直接查看。其中有一段 14 字节的序列,其十六进制表示为:1
53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3
这便是上面列出的汇编指令对应的目标代码,这表示机器执行的程序只是一个字节序列,它是对一系列指令的编码。
我们可以使用反汇编器来查看机器代码的内容,它可以根据机器代码产生一种类似于汇编代码的格式代码。在 Linux 系统,我们使用 -d 命令行标志的程序 OBJDUMP(表示 Object Dump)来充当这个角色。1
objdump -d mstore.o
结果如下:1
2
3
4
5
6
70000000000000000 <multstore>:
0: 53 push %rbx
1: 48 89 d3 mov %rdx, %rbx
4: e8 00 00 00 00 callq 9<multstore+0x9>
9: 48 89 03 mov %rax, (%rbx)
c: 5b pop %rbx
d: c3 retq
在左边,我们可以看到按字节顺序排列的 14 个十六进制字节值,它们分成了若干组,每组有 1~5 个字节,每组都是一条指令,右边是等价的汇编语言。
同时注意到关于机器码和它的反汇编码的一些特性:
- x86-64的指令长度从 1 到 15 个字节不等。常用指令与操作数较少的指令的字节数少,而不太常用的或操作数多的指令所需要的字节数较多。
- 设计指令格式的方法是,从某个给定位置开始,可以将字节唯一地解码成机器指令。例如只有 pushq %rbx 是以字节值 53 开头的。
- 反汇编码只是基于机器代码文件中的字节序列来确定汇编码,不需要了解访问源码或是汇编码。
- 反汇编器使用的指令命名规则与 GCC 生成的代码有所不同。
生成实际可执行代码需要对一组目标代码文件运行链接器,而这一组目标代码中必须有一个 main 函数。考虑如下文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// main.c
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;
}
我们使用如下方法生成可执行文件:1
gcc -Og -o prog main.c mstore.c
文件 prog 变成了 8655 个字节,因为它不仅包含了两个过程的代码,还包括了终止程序的代码,以及与操作系统交互的代码。我们亦可以反汇编 prog:1
objdump -d prog
反汇编器会抽取出各种代码序列,其中有:1
2
3
4
5
6
7
8
90000000000400540 <multstore>
400540: 53 push %rbx
400541: 48 89 d3 mov %rdx, %rbx
400544: e8 00 00 00 00 callq 9<multstore+0x9>
400549: 48 89 03 mov %rax, (%rbx)
40054c: 5b pop %rbx
40054d: c3 retq
40054e: 90 nop
40054f: 90 nop
这段代码与反汇编产生的代码几乎 完 全 一 致 ,主要区别则是地址不同——链接器将这段代码移到了一段不同的地址范围中。第二个不同之处在于链接器填上了 callq 指令调用函数 mult2 需要使用的地址。链接器的任务之一则是为函数调用找到匹配函数可执行代码的位置。最后一个区别则是最后多了两行代码。插入它们对程序没有影响,只是为了使得函数变为 16 字节,使得就存储器系统而言,能更好地放置下一个代码块。(对齐)
关于格式的注解
如果我们采用以下格式生成 mstore.s:1
gcc -Og -S mstore.c
其完整内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 .file "010-mstore.c"
.globl multstore
.type multstore, @function
multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
.size multstore, .-multstore
.ident "GCC: (Ubuntu 4.8.1 4-2ubuntu1-12.04) 4.8.1"
-section .note.GNU-stack,"",@progbits
这是书上给的,笔者在 Win10 环境下用 Mingw 编译的结果如下:
1 | .file "mstore.c" |
所有以 ‘.’ 开头的行都是指导汇编器和链接器工作的伪指令。
ATT与Intel汇编代码格式
CSAPP使用的是ATT(根据“AT&T”命名的,它是运营贝尔实验室多年的公司)格式的汇编代码。这是 GCC、OBJDUMP 和其他一些我们使用的工具的格式,但是如果是 Microsoft 等的工具,就不是这样的了。这两种格式有所不同,例如,使用如下命令生成 multstore 函数的 Intel 格式代码:1
gcc -Og -S -masm=intel mstore.c
这个命令得到的代码是:1
2
3
4
5
6
7multstore:
push rbx
mov rbx, rdx
call mult2
mov QWORD PTR [rbx], rax
pop rbx
ret
我们注意到区别如下:
- Intel 代码省略了指示大小的后缀。我们看到的是 push 和 pop 而非 pushq 和 popq
- Intel 代码省略了寄存器名字前的 ‘%’ 符号。
- Intel 代码是用不同方式描述内存中的位置,例如是 ‘QWORD PTR [rbx]’ 而非 ‘(%rbx)’
- 在带有多个操作数的指令情况下,列出操作数的顺序相反。
