标量指令集编译器简易实现
之前没有接触过标量isa的编译器该怎么写,所以需要学习一下.
主要参考自RednaxelaFX的寄存器分配问题
以及chibicc
简易c编译器.
x86 通用寄存器使用建议
寄存器 | Callee Save | 描述 |
---|---|---|
%rax | 结果寄存器;同时被用于idiv/imul指令中 | |
%rbx | yes | 杂项寄存器 |
%rcx | 第4个参数寄存器 | |
%rdx | 第3个参数寄存器; 也被用在idiv / imul指令 | |
%rsp | 栈指针 | |
%rbp | yes | 帧指针 |
%rsi | 第2个参数寄存器 | |
%rdi | 第1个参数寄存器 | |
%r8 | 第5个参数寄存器 | |
%r9 | 第6个参数寄存器 | |
%r10 | 杂项寄存器 | |
%r11 | 杂项寄存器 | |
%r12 | yes | 杂项寄存器 |
%r13 | yes | 杂项寄存器 杂项寄存器 |
%r14 | yes | 杂项寄存器 |
%r15 | yes | 杂项寄存器 |
- %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中。在imul 指令中,两个64位的乘法最多会产生128位的结果,需要 %rax 与 %rdx 共同存储乘法结果,在div 指令中被除数是128 位的,同样需要%rax 与 %rdx 共同存储被除数。
- %rsp指向了内存中堆栈的栈顶,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。
- %rbp是当前的栈帧指针,标记当前栈帧的起始位置
- Callee
Save表示当前寄存器的值是
被调用者保存
,也就是发生函数调用的时候,这些寄存器的值在进去子函数后,子函数先保存这些寄存器的值,然后在返回上一级时恢复. - Caller Save表示在进行子函数调用前,就需要由调用者提前保存好这些寄存器的值,保存方法通常是把寄存器的值压入堆栈中,调用者保存完成后,在被调用者(子函数)中就可以随意覆盖这些寄存器的值.
基于chibicc检查具体行为
我利用chibicc
对一些代码进行编译,然后调试汇编进行检查他的行为.
函数调用帧栈指针行为
|
1. 在main函数中
这里先把imm加载,然后push到栈上,然后再pop到两个寄存器上.开始调用foo
0040117D: 48 C7 C0 02 00 00 00 movq $0x2, %rax |
step 1 调用前
%rbp -> | xxx | high |
step 2 开始调用
callq *%r10
后的结果如下:
因为call
会把call
下一条指令的地址压到栈上作为return要用的address.
%rbp -> | xxx | high |
2. 在foo函数中
004011B3: 55 pushq %rbp # 保存之前的rbp之后, rsp = 0xbc08 |
step 1
push %rbq
⚠️ rsp的push是先递减然后修改对应的值! %rbp -> | xxx | high
| xxx | ^
| xxx | |
| return address | |
%rsp -> | old rbp | |
| empty | |
| empty | low
step 2
movq %rsp, %rbp
| xxx | high |
step 3
subq $0x10, %rsp
| xxx | high <-┐ |
step 4
从寄存器中把参数写入内存. 他这里还有存了一个rsp,可能是有别的用途.?
movq %rsp, -0x8(%rbp)
movl %edi, -0xc(%rbp)
movl %esi, -0x10(%rbp)
| xxx | high <---┐ |
step 5
addl %edi, %eax
这里把计算结果存入eax
,eax
是rax
的一半.
step 6
004011D7: EB 00 jmp 0x4011d9 |
返回时, 先jmp到return的位置, 然后rsp指向当前帧顶部:
| xxx | high <---┐ |
step 7
004011DC: 5D popq %rbp |
接下来恢复rbp到上一帧的栈顶, 此时rsp
指向返回地址.
%rbp -> | xxx | high <---┐ |
step 8
004011DD: C3 retq |
return实际上是先推出rsp的中的值,然后根据此地址进行跳转.这里的return address就是之前call的下一条指令.
%rbp -> | xxx | high <---┐ |
函数通过栈传参行为分析
在x86中,通常通过6个寄存器进行int类型参数传递,分别是rdi
,
rsi
, rdx
, rcx
, r8
,
r9
.
如果是浮点类型的参数,利用的是8个浮点寄存器.当参数为大的结构体/联合体,或者参数个数超过寄存器能容纳的数量时,
将通过栈传递参数.
栈传递参数是在caller中进行的,
将函数参数从右到左的压到栈上(便于支持变长参数): %rbp -> | xxx | high
| xxx | ^
| callee arg 9 | |
| callee arg 8 | |
%rsp -> | callee arg 7 | |
| empty | |
| empty | |
| empty | |
| empty | low
压完栈之后进入函数中后,帧栈位置如下:
%rbp -> | xxx | high <---┐ |
在代码生成前我们就需要确定所有的参数是通过寄存器传递还是栈传递,因此在子函数中获取local var只需要给出之前分配变量位置时设定的偏移即可.
同时要注意,结构体的压栈顺序也是倒序的,例如结构体如下:
typedef struct
{
int n;
int c;
int h;
int w;
} shape_t;
typedef struct
{
shape_t shape;
unsigned int addr;
} buffer_t;
压栈的时候是先把栈向下到对应位置,然后向上copy,
最终的数据摆放应该是如下的: %rbp -> | xxx | high <---┐
| xxx | ^ |
| buffer.addr | | |
| buffer.shape.w | | |
| buffer.shape.h | | |
| buffer.shape.c | | |
| buffer.shape.n | | |
| return address | | |
%rsp -> | old rbp | | ------┘
| empty | |
| empty | low
函数调用相对地址计算
我才发现在调用函数的时候是通过%rip
寄存器去寻址的,给出如下函数:
int foo(int a) { return a + 1; } |
编译结果:
注意到下面调用函数时使用了lea foo2(%rip), %rax
来获得对应的地址.
然后我查看了%rip
的作用是:
The role of the %rip register The
%rip
register on x86-64 is a special-purpose register that always holds the memory address of the next instruction to execute in the program's code segment. The processor increments%rip
automatically after each instruction, and control flow instructions like branches set the value of%rip
to change the next instruction. Perhaps surprisingly,%rip
also shows up when an assembly program refers to a global variable. See the sidebar under "Addressing modes" below to understand how%rip
-relative addressing works.
也就是他指向了下一个指令的地址.
main: |
接下来我再用gnu as
进行汇编得到:
main: |
上面有个很奇怪的地方,lea
不是应该得到的是foo
的地址,
他这里的注释的解释如下: lea rax,[rip+0x0] # b4 <main+0xb4> , b4 是下一个指令的地址, <main + 0xb4>就是main为0,加上偏移b4
lea rax,[rip+0x0] # 105 <foo2+0x2a>, 105 是下一个指令的地址, <foo2 + 0x2a>就是foo2为0xdb,加上偏移2a