- // test.c
- int main()
- {
- int a = 1;
- int b = 2;
- a = a + b;
- }
-
在一个 Linux 操作系统上,我们可以简单地使用 gcc 和 objdump 这样两条命令,把对应的汇编代码和机器码都打印出来。
- test.o: file format elf64-x86-64
-
- Disassembly of section .text:
-
- 0000000000000000 <main>:
- #include<stdio.h>
-
- int main()
- {
- 0: 55 push rbp
- 1: 48 89 e5 mov rbp,rsp
- 4: 48 83 ec 10 sub rsp,0x10
- int a = 1;
- 8: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
- int b = 2;
- f: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
- a = a + b;
- 16: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
- 19: 01 45 fc add DWORD PTR [rbp-0x4],eax
- printf("a=%d\n",a);
- 1c: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
- 1f: 89 c6 mov esi,eax
- 21: bf 00 00 00 00 mov edi,0x0
- 26: b8 00 00 00 00 mov eax,0x0
- 2b: e8 00 00 00 00 call 30 <main+0x30>
-
- return 0;
- 30: b8 00 00 00 00 mov eax,0x0
- }
- 35: c9 leave
- 36: c3 ret
-
因为汇编代码其实就是“给程序员看的机器码”,也正因为这样,机器码和汇编代码是一一对应的。
常见的指令可以分成五大类
一个 CPU 里面会有很多种不同功能的寄存器,三种比较特殊的
除了这些特殊的寄存器,CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。通常根据存放的数据内容来给它们取名字,比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放数据,又能存放地址,我们就叫它通用寄存器。
- #include <time.h>
- #include <stdlib.h>
- int main()
- {
- srand(time(NULL));
- int r = rand() % 2;
- int a = 10;
- if (r == 0)
- {
- a = 1;
- } else {
- a = 2;
- }
- }
-
if…else 条件判断语句。对应的汇编代码是这样的:
- if (r == 0)
- 33: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0
- /*
- cmp 指令比较了前后两个操作数的值,这里的 DWORD PTR 代表操作的数据类型是 32 位的整数,而 [rbp-0x4] 则是一个寄存器的地址。所以,
- 第一个操作数就是从寄存器里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。cmp 指令的比较结果,会存入到
- 条件码寄存器当中去。
-
- 如果比较的结果是 True,也就是 r == 0,就把零标志条件码(对应的条件码是 ZF,Zero Flag)设置为 1。除了零标志之外,Intel 的 CPU
- 下还有进位标志(CF,Carry Flag)、符号标志(SF,Sign Flag)以及溢出标志(OF,Overflow Flag),用在不同的判断条件下。
- */
- 37: 75 09 jne 42 <main+0x42>
- /*
- jne 指令,是 jump if not equal 的意思,它会查看对应的零标志位。如果为 0,会跳转到后面跟着的操作数 42 的位置。这个 42,对应这
- 里汇编代码的行号,也就是上面设置的 else 条件里的第一条指令。当跳转发生的时候,PC 寄存器就不再是自增变成下一条指令的地址,而是被
- 直接设置成这里的 42 这个地址。这个时候,CPU 再把 42 地址里的指令加载到指令寄存器中来执行。
- */
- {
- a = 1;
- 39: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1
- 40: eb 07 jmp 49 <main+0x49>
- /*
- if 条件,如果满足的话,在赋值的 mov 指令执行完成之后,有一个 jmp 的无条件跳转指令。跳转的地址就是这一行的地址 49。我们的 main 函
- 数没有设定返回值,而 mov eax, 0x0 其实就是给 main 函数生成了一个默认的为 0 的返回值到累加器里面。if 条件里面的内容执行完成之后也
- 会跳转到这里,和 else 里的内容结束之后的位置是一样的。
- */
- } else {
- a = 2;
- 42: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
- /*
- 跳转到执行地址为 42 的指令,实际是一条 mov 指令,第一个操作数和前面的 cmp 指令一样,是另一个 32 位整型的寄存器地址,以及对应的2
- 的16进制值 0x2。mov 指令把 2 设置到对应的寄存器里去,相当于一个赋值操作。然后,PC 寄存器里的值继续自增,执行下一条 mov 指令
- */
- }
- return 0;
- 49: b8 00 00 00 00 mov eax,0x0
- /*
- 这条 mov 指令的第一个操作数 eax,代表累加寄存器,第二个操作数 0x0 则是 16 进制的 0 的表示。这条指令其实没有实际的作用,它的作用
- 是一个占位符。
- */
- }
- 4e: c9 leave
- 4f: c3 ret
-
“C 语言代码 - 汇编代码 - 机器码” 这个过程,在我们的计算机上进行的时候是由两部分组成的。
ELF其实是一种文件格式的标准,ELF文件有三类:可重定向文件、可执行文件、共享目标文件。代码经过预处理、编译、汇编后形成可重定向文件,可重定向文件经过链接后生成可执行文件。
Linux 下的 ELF 文件格式,而 Windows 的可执行文件格式是一种叫作PE(Portable Executable Format)的文件格式。Linux 下的装载器只能解析 ELF 格式而不能解析 PE 格式。
装载器需要满足两个要求:
把指令里用到的内存地址叫作虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址叫物理内存地址(Physical Memory Address)。
要满足装载器的两个要求,我门可以在内存里面,找到一段连续的内存空间,然后分配给装载的程序,然后把这段连续的内存空间地址,和整个程序指令里指定的内存地址做一个映射。