本文希望力求精简的解释一行c代码是如何变成一段二进制编码,又是如何进入cpu被执行的。
0x00 示例代码
int div(int a, int b, int c) {
int sum = a + b;
return sum / c;
}
0x01 词法语法分析,翻译为LLVM-IR
LLVM IR是一门低级语言,语法类似于汇编。任何高级编程语言(如C++)都可以用LLVM IR表示。
define dso_local signext i32 @div(i32 noundef signext %a, i32 noundef signext %b, i32 noundef signext %c) #0 {
entry:
%a.addr = alloca i32, align 4
%b.addr = alloca i32, align 4
%c.addr = alloca i32, align 4
%sum = alloca i32, align 4
store i32 %a, ptr %a.addr, align 4
store i32 %b, ptr %b.addr, align 4
store i32 %c, ptr %c.addr, align 4
%0 = load i32, ptr %a.addr, align 4
%1 = load i32, ptr %b.addr, align 4
%add = add nsw i32 %0, %1
store i32 %add, ptr %sum, align 4
%2 = load i32, ptr %sum, align 4
%3 = load i32, ptr %c.addr, align 4
%div = sdiv i32 %2, %3
ret i32 %div
}
0x02 转为DAG
命令:llc -view-dag-combine1-dags -march=riscv64 -o instr.s instr.ll,需要自己编译llc为debug版本
此时的DAG有部分是平台无关的代码
0x03 DAG legalization
DAG中平台无关的代码转换为平台相关的代码
比如将上图中的DIV转换为DIVW。
0x04 选择汇编指令
通过DAG中的DIVW节点,映射到指令DIVW。
0x05 指定调度重排,优化执行速度
st i32 %a, i16* %b, i16 5
st %b, i32* %c, i16 0
%d = ld i32* %c
在riscv cpu上,第3行指令用到了寄存器%c中的值,而第2行使用了寄存器%c,所以要在第2行之后等一个时钟周期,这是cpu的规定。
对指令进行重新排列,交换第1行和第2行,就可以利用等待的时钟周期执行st i32 %a, i16*%b, i16 5这条指令。
0x06 分配寄存器
在此之前LLVM一直使用无限多的寄存器,这样方便代码优化。现在要把无限多的寄存器映射为指定平台的有限多的寄存器,具体过程参考常见的寄存器着色算法。
0x07 输出汇编指令
编译完成,输出汇编指令或者二进制码。
0000000000000002 <div>:
2: 01 11 addi sp, sp, -32
4: 06 ec sd ra, 24(sp)
6: 22 e8 sd s0, 16(sp)
8: 00 10 addi s0, sp, 32
a: 23 26 a4 fe sw a0, -20(s0)
e: 23 24 b4 fe sw a1, -24(s0)
12: 23 22 c4 fe sw a2, -28(s0)
16: 03 25 c4 fe lw a0, -20(s0)
1a: 83 25 84 fe lw a1, -24(s0)
1e: 2d 9d addw a0, a0, a1
20: 23 20 a4 fe sw a0, -32(s0)
24: 03 25 04 fe lw a0, -32(s0)
28: 83 25 44 fe lw a1, -28(s0)
2c: 3b 45 b5 02 divw a0, a0, a1
30: e2 60 ld ra, 24(sp)
32: 42 64 ld s0, 16(sp)
34: 05 61 addi sp, sp, 32
36: 82 80 ret
0x08 CPU执行指令的五大步骤
2c: 3b 45 b5 02 divw a0, a0, a1
这条指令的二进制为(参考riscv指令集手册):
0000001 01011 01010 001 01010 0111011
funct7 寄存器a1 寄存器a0 funct3 寄存器a0 opcode divw
cpu执行指令的5大步骤为:取指,译码,执行,暂存,写回
- 取指
cpu拿到3b45b502这个32位指令
- 译码
查看opcode,发现是divw指令,为执行器准备好寄存器a0和a1
- 执行
执行器用a0除以a1
- 存取
如果指令需要读取或者写入数据,那么就访问cpu缓存或者内存。divw指令不需要这一步。
- 写回
把除法结果写入a0