-
所在小组:第三组
-
组内昵称:hy
-
心得体会
编译器基于目标机器的指令集和操作系统规则生成机器代码,例如常见的 Linux x64、Win x64 等。
GCC 编译器产生汇编代码输出,然后调用汇编器和链接器,生成可执行的机器代码。
汇编代码与特定机器密切相关,是与平台(x86-64)耦合的。
历史观点
操作系统兼容处理器,根据处理器设计操作系统。
x86 是 Intel 处理器系统的俗称,目前也代表 64 位系统 x86-64
的意思。
AMD 生产的处理器兼容 x86 与 Intel 竞争。
程序编码
gcc -Og -o p p1.c p2.c
从 C 源码到可执行文件:
-
C 预处理器扩展源代码,替换 #include、#define
头文件和宏;
-
编译器产生源文件的汇编代码:p1.s、p2.s
;
-
汇编器将汇编代码转化成二进制目标代码:p1.o、p2.o
,目标代码是机器代码的一种形式,包含所有指令的二进制表示,但是还没有填入全局值的地址;
-
链接器将目标代码和库函数(printf)的代码合并,产生最终的可执行代码文件 p
,会给各种链接的函数分配地址,保证这些函数有自己的调用(call)地址。
机器级代码
机器级代码抽象:
-
使用指令集架构提供的指令,CPU 顺序执行指令,指令会影响 CPU、寄存器等的状态,即使能够并行,也遵循顺序执行的模型;
-
使用虚拟内存地址。
机器代码把内存看成一个很大、按字节寻址的数组,各种数据类型在机器代码中都是一组连续的字节表示,也不区分无符号、有符号整数,不区分指针和整数,不区分各种类型的指针。
虚拟地址 2^48 ~ 2^64
这部分高 16 位必须设置为 0。
objdump 反汇编
反汇编程序 objdump -d x.o
可以查看机器代码,而且会转成类似汇编的代码。
数据格式
Intel 规定一个字(word)等于 16 位,两个字节。因此双字为 32 位,四字为 64 位。在 64 位机器中,指针就是四字。
-
movb 表示移动半个字,1 个字节
-
movw 移动 1 个字,2 个字节
-
movl(long word) 移动 2 个字
-
movq 移动 4 个字
数据访问
刚开始 8086 CPU 中有 8 个 16 位寄存器:rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp,名字反映了不同的用途。
扩展到 IA32 架构时,增加到 32 位。
再到 x86-64 的 CPU,增加了 8 个 r8 - r15,最终有 16 个通用的 64 位寄存器,拿来存储指针和整数。
64 位可以拿一部分低字节当 16、32 位使用,所以是向下兼容的。
rsp 寄存器是栈指针(stack point)指明运行时栈的结束位置,很多栈操作都是在这个指针上加减的相对位置操作,非常灵活。
有一组编程规范指导寄存器的使用,实现栈管理、函数调用(参数、返回值)、局部变量…
操作数指示符
一组如何从寄存器、内存、中读写值的规范。
-
立即数:$5 就是一个直接的数字,数字字面量(有 $ 符号);
-
绝对寻址:0x400001
直接写死一个内存地址,没有 $ 符号,表示拿出这个内存地址的值;
-
ra:直接写某个寄存器,代码这个寄存器中的值 R[ra]
(R 可以看成所有寄存器的数组,ra 是取值下标)寄存器寻址;
-
(ra)
:M[R[ra]]
:在内存中取值寄存器中存储地址的值(M 代表内存)内存寻址;
最复杂的情况,其他都是这种情况的简化:
Imm(rb, ri, s) = M[Imm + R[rb] + R[ri] * s]
这个比较复杂,Imm 是立即数偏移,rb 是基址寄存器,ri 是变址寄存器,s 是比例因子,取值必须是 1、2、4、8,访问数组会用到这种形式。
最终算出来都是一个寄存器或内存地址。
数据传送指令
操作数指示符就相当于数据传送指令的参数,把一个地址的数复制到另一个地址。
mov S, D
= Source -> Dest
b、w、l、q 分别代表复制 8、16、32、64 个字节。
内存地址不能复制到内存地址,需要寄存器中转。
movz 指令会对不够长度的源值在目的地址填充 0。
movs 系列指令会对不够长度的情况填充符号扩展。
压栈、出栈
push、pop
操作栈数据结构,会修改栈指针的值,其指向内存的某个区域。
根据惯例,栈向下生长,下面是栈底push 时栈指针值会减 8,也就是上面的地址大。
因为栈在内存中,也就是大数组的一部分,因此经常使用偏移栈指针的方式访问数据,例如:movq 8(%rsp), %rdx
将第二个四字从栈中复制到寄存器 %rdx。
流程控制
流程控制是一个难点,但也是提升 CPU 性能的一个重要领域,好的分支预测技术可以显著提高 CPU 执行的效率,在并行的情况下实现流水线顺序执行的抽象。
函数
函数是非常重要的抽象,在函数的基础上,我们可以使用各种编程范式和设计模式,在高层次上思考问题。
对于函数的底层实现,则是依赖栈和寄存器进行传参、调用和返回,在 call 指令的过程中压入返回地址。