GCC Internals
本文是对 GNU Compiler Collection Internals 文档的整理和总结。文章引用了部分 GCC 源码。
RTL
查看 RTL
GCC 的 RTL 是采用 Lisp 语言描述的低层次的中间语言,是转换为机器指令前的最后一个表示。在生成汇编的时候,添加参数 -dP
就可以看到最后的 RTL 和汇编的对应关系:
#(insn 13 8 14 (set (reg/i:DI 10 a0)
# (sign_extend:DI (mult:SI (reg:SI 10 a0 [77])
# (reg:SI 11 a1 [78])))) "kb.c":3:1 21 {*mulsi3_extended}
# (expr_list:REG_DEAD (reg:SI 11 a1 [78])
# (nil)))
mulw a0,a0,a1 # 13 [c=20 l=4] *mulsi3_extended
#(jump_insn 21 20 22 (simple_return) "kb.c":3:1 246 {simple_return}
# (nil)
# -> simple_return)
ret # 21 [c=0 l=4] simple_return
如果想要 dump 出每个 RTL pass 后的结果,可以添加参数:-fdump-rtl-all
,可以看到形如下面的 RTL 代码:
;; Function mul (mul, funcdef_no=0, decl_uid=1488, cgraph_uid=1, symbol_order=0)
(note 1 0 5 NOTE_INSN_DELETED)
(note 5 1 19 [bb 2] NOTE_INSN_BASIC_BLOCK)
(note 19 5 2 NOTE_INSN_PROLOGUE_END)
(note 2 19 3 NOTE_INSN_DELETED)
(note 3 2 4 NOTE_INSN_DELETED)
(note 4 3 7 NOTE_INSN_FUNCTION_BEG)
(note 7 4 8 NOTE_INSN_DELETED)
(note 8 7 13 NOTE_INSN_DELETED)
(insn 13 8 14 (set (reg/i:DI 10 a0)
(sign_extend:DI (mult:SI (reg:SI 10 a0 [77])
(reg:SI 11 a1 [78])))) "kb.c":3:1 21 {*mulsi3_extended}
(expr_list:REG_DEAD (reg:SI 11 a1 [78])
(nil)))
(insn 14 13 20 (use (reg/i:DI 10 a0)) "kb.c":3:1 -1
(nil))
(note 20 14 21 NOTE_INSN_EPILOGUE_BEG)
(jump_insn 21 20 22 (simple_return) "kb.c":3:1 246 {simple_return}
(nil)
-> simple_return)
(barrier 22 21 18)
(note 18 22 0 NOTE_INSN_DELETED)
阅读 RTL
可以看到 RTL 是一系列的 S-exp,每个 S-exp 的前三个整数参数分别是 id,prev 和 next,表示一个双向链表。核心是要看 (insn) 的第四个参数,例如:
表示这个指令的作用是,把 int32 类型的 a0 寄存器的值乘以 int32 类型的 a1 寄存器的值,结果符号扩展到 int64,写入到 a0 寄存器中。其中表示类型的是 DI/SI,下面是一个简单的名字和位数的对应关系:
- 整数:B(Bit)I=1, Q(Quarter)I=8, H(Half)I=16, S(Single)I=32, D(Double)I=64, T(Tetra)I=128
- 浮点:Q(Quarter)F=8, H(Half)F=16, S(Single)F=32, D(Double)F=64, T(Tetra)F=128
- 十进制浮点:*D
- 有理数:*Q
- 复数:*C
稍微和习惯不同的是 Q 表示 Quarter 而不是 Quad。
寄存器的表示方式是 (reg:m n)
:
这里的 DI/SI 就是类型,10/11 是寄存器编号,后面的 a0/a1 是寄存器 10/11 在 RISC-V 架构里的 ABI 名称。/i
表示这个寄存器会保存函数的返回值(REG_FUNCTION_VALUE_P(x)
)。
指令模板
同一行最后还有一个 {*mulsi3_extended}
,这表示的是这一个 insn 对应了哪一个规则,这可以在 riscv.md 中找到:
(define_insn "mulsi3_extended"
[(set (match_operand:DI 0 "register_operand" "=r")
(sign_extend:DI
(mult:SI (match_operand:SI 1 "register_operand" " r")
(match_operand:SI 2 "register_operand" " r"))))]
"(TARGET_ZMMUL || TARGET_MUL) && TARGET_64BIT"
"mulw\t%0,%1,%2"
[(set_attr "type" "imul")
(set_attr "mode" "SI")])
这表示一个模式匹配的规则,构成了从 RTL 到指令的映射关系。模式匹配的部分是:
[(set (match_operand:DI 0 "register_operand" "=r")
(sign_extend:DI
(mult:SI (match_operand:SI 1 "register_operand" " r")
(match_operand:SI 2 "register_operand" " r"))))]
意思是可以匹配两个操作数,这两个操作数应当是寄存器操作数(register_operand
,r
表示通用寄存器),类型是 int32(:SI
),操作数乘法后符号扩展到 int64(:DI
),最后写入一个寄存器的目的操作数(register_operand
,类型是 int64 :DI
,保存在通用寄存器 r
,=
表示写入,旧数据丢弃)。满足这些要求,就匹配上了 mulsi3_extended
。
下一行:
指的是额外的条件,用 C 代码编写。比如这里要用到 mulw 指令,但是如果当前的 target 不支持这个指令,那就不要生成 mulw 指令,所以这里就要制定限制条件:实现了乘法,并且是 64 位。
再下一行就是输出的指令:
这里的 %0
%1
%2
就会替换为前面匹配得到的操作数,注意前面 match_operand
后的整数,这就是对应的编号。可以看到这里和 GCC 内联汇编的语法特别相似,包括前面 match_operand
的最后一个参数:=r
和 r
,其实和内联汇编里对操作数的 specifier 也是非常像的。
最后是一些额外的属性,这个是用来给运算标记类型的,例如要针对处理器流水线进行优化,那就需要知道每个指令会被分到哪个流水线里面。
完整的格式如下:
忽略了可选的命名。
例子回顾
回顾一下开头的例子:
#(insn 13 8 14 (set (reg/i:DI 10 a0)
# (sign_extend:DI (mult:SI (reg:SI 10 a0 [77])
# (reg:SI 11 a1 [78])))) "kb.c":3:1 21 {*mulsi3_extended}
# (expr_list:REG_DEAD (reg:SI 11 a1 [78])
# (nil)))
mulw a0,a0,a1 # 13 [c=20 l=4] *mulsi3_extended
这一个 insn 与 mulsi3_extended 相匹配,操作数 0 匹配到了 (reg/i:DI 10 a0)
,操作数 1 匹配到了 (reg:SI 10 a0 [77])
,操作数 2 匹配到了 (reg:SI 11 a1 [78])
,所以最后生成指令的时候,把对应的寄存器名字填进去,就得到了 mulw a0,a0,a1
。
有时候,比较复杂的指令会在 C 代码中生成,例如 simple_return
:
(define_insn "simple_return"
[(simple_return)]
""
{
return riscv_output_return ();
}
[(set_attr "type" "jump")
(set_attr "mode" "none")])
那么匹配以后,会调用 riscv_output_return
函数:
实现也很简单,除了 naked 函数以外,都是一条 ret 指令。
原理探究
具体地,在 riscv.md
中写模式匹配的时候:
[(set (match_operand:DI 0 "register_operand" "=r")
(sign_extend:DI
(mult:SI (match_operand:SI 1 "register_operand" " r")
(match_operand:SI 2 "register_operand" " r"))))]
会在 gcc/insn-recog.cc
生成如下的代码:
// x1 = (set (reg/i:DI 10 a0)
// (sign_extend:DI (mult:SI (reg:SI 10 a0 [77])
// (reg:SI 11 a1 [78]))))
static int
recog_7 (rtx x1 ATTRIBUTE_UNUSED,
rtx_insn *insn ATTRIBUTE_UNUSED,
int *pnum_clobbers ATTRIBUTE_UNUSED)
{
rtx * const operands ATTRIBUTE_UNUSED = &recog_data.operand[0];
rtx x2, x3, x4, x5, x6;
int res ATTRIBUTE_UNUSED;
// x2 = (reg/i:DI 10 a0)
x2 = XEXP (x1, 0);
// operands[0] = (reg/i:DI 10 a0)
operands[0] = x2;
// x3 = (sign_extend:DI (mult:SI (reg:SI 10 a0 [77])
// (reg:SI 11 a1 [78])))
x3 = XEXP (x1, 1);
// x4 = (mult:SI (reg:SI 10 a0 [77])
// (reg:SI 11 a1 [78]))
x4 = XEXP (x3, 0);
switch (GET_CODE (x4))
{
case MULT:
if (pattern14 (x3, E_SImode) != 0
|| !
#line 869 "/home/jiegec/ct-ng/.build/riscv64-unknown-linux-gnu/src/gcc/gcc/config/riscv/riscv.md"
(TARGET_MUL && TARGET_64BIT))
return -1;
return 21; /* *mulsi3_extended */
}
}
// x1 = (sign_extend:DI (mult:SI (reg:SI 10 a0 [77])
// (reg:SI 11 a1 [78])))
static int
pattern14 (rtx x1, machine_mode i1)
{
rtx * const operands ATTRIBUTE_UNUSED = &recog_data.operand[0];
rtx x2, x3, x4;
int res ATTRIBUTE_UNUSED;
// x2 = (mult:SI (reg:SI 10 a0 [77])
// (reg:SI 11 a1 [78]))
// operands[0] = (reg/i:DI 10 a0)
x2 = XEXP (x1, 0);
if (GET_MODE (x2) != E_SImode
|| !register_operand (operands[0], E_DImode)
|| GET_MODE (x1) != E_DImode)
return -1;
// x3 = (reg:SI 10 a0 [77])
x3 = XEXP (x2, 0);
// operands[1] = (reg:SI 10 a0 [77])
operands[1] = x3;
if (!register_operand (operands[1], E_SImode))
return -1;
// x4 = (reg:SI 11 a1 [78])
x4 = XEXP (x2, 1);
// operands[2] = (reg:SI 11 a1 [78])
operands[2] = x4;
if (!register_operand (operands[2], i1))
return -1;
return 0;
}
代码中用注释标注了每一步匹配的内容,可见代码在匹配的同时,也把操作数保存了下来。
指令拆分
有些时候,RTL 层次上的指令可能对应多条实际的机器指令。例如在 32 位 target 上,拷贝一个 64 位整数,这意味着需要把一个 DI 类型的 move,拆成两个 SI 类型的 move,分别处理高位和低位。下面来看一个 rv32 的例子:
;; 64-bit modes for which we provide move patterns.
(define_mode_iterator MOVE64 [DI DF])
(define_split
[(set (match_operand:MOVE64 0 "nonimmediate_operand")
(match_operand:MOVE64 1 "move_operand"))]
"reload_completed
&& riscv_split_64bit_move_p (operands[0], operands[1])"
[(const_int 0)]
{
riscv_split_doubleword_move (operands[0], operands[1]);
DONE;
})
可以看到,它会尝试匹配从一个 64 位整数(DI)或浮点数(DF)到另一个 64 位整数(DI)或浮点数(DF)的 set,也就是 move 操作。匹配上以后,判断条件:
是否成立。这里 reload_completed == true
表示已经完成寄存器分配。
如果成立,就认为可以拆分 64 位的 move 为两个 32 位的 move,就生成下列 RTL:
但是实际上不会生成这个,而是调用后面的代码来生成新的 RTL 代码:
完整的 define_split
定义如下:
(define-split
[insn-pattern]
"condition"
[new-insn-pattern-1
new-insn-pattern-2
...]
"preparation-statements")
忽略了可选的命名。
下面来看一个具体的例子,这一段是在 split 之前的 RTL 代码:
(insn 6 3 13 (set (reg:DI 14 a4 [orig:72 _2 ] [72])
(mem/c:DI (plus:SI (reg/f:SI 8 s0)
(const_int -24 [0xffffffffffffffe8])) [1 a+0 S8 A64])) "kb.c":1:38 134 {*movdi_32bit}
(nil))
它做的事情是,从内存地址 $s0-24
读取读取 8 个字节,写入到 a4 寄存器当中,由于 RV32 寄存器宽度只有 32 位,所以需要拆成两个读取:
Splitting with gen_split_14 (riscv.md:1510)
deleting insn with uid = 6.
deleting insn with uid = 6.
(insn 30 3 31 (set (reg:SI 14 a4 [orig:72 _2 ] [72])
(mem/c:SI (plus:SI (reg/f:SI 8 s0)
(const_int -24 [0xffffffffffffffe8])) [1 a+0 S4 A64])) "kb.c":1:38 136 {*movsi_internal}
(nil))
(insn 31 30 32 (set (reg:SI 15 a5 [ _2+4 ])
(mem/c:SI (plus:SI (reg/f:SI 8 s0)
(const_int -20 [0xffffffffffffffec])) [1 a+4 S4 A32])) "kb.c":1:38 136 {*movsi_internal}
(nil))
可以看到,64 位的目的寄存器被拆成了两个 32 位寄存器,分别是 a4 和 a5;内存读取指令也拆分成了两个 SI 类型的读取,栈上的偏移也做了相应的调整。
有时候还可以见到 define_insn_and_split
:
(define_insn_and_split
[insn-pattern]
"condition"
"output-template"
"split-condition"
[new-insn-pattern-1
new-insn-pattern-2
...]
"preparation-statements"
[insn-attribute])
这实际上就是一个 define_insn
加一个 define_split
,二者的 insn-pattern
一致:
(define_insn
[insn-pattern]
"condition"
"output-template"
[insn-attribute])
(define_split
[insn-pattern]
"split-condition"
[new-insn-pattern-1
new-insn-pattern-2
...]
"preparation-statements")
构建用于开发的 GCC
完整的 GCC 需要经过 bootstrap,但是为了开发,可以简化:
mkdir gcc-build
cd gcc-build
../gcc/configure --prefix=$PREFIX --enable-languages=c,c++ --disable-bootstrap CFLAGS="-g -O0" CXXFLAGS="-g -O0"
make -j8
make install -j8
交叉编译:给 configure 添加参数 --target=loongarch64-unknown-linux-gnu
,但是有一些组件可能会因为缺少交叉编译环境而无法编译,可以只编译和安装 gcc:make all-gcc && make install-gcc
。
调试 GCC
给 GCC 传递编译选项,可以让 GCC 打印中间结果,见 GCC Developer Options:-fdump-tree-all -fdump-rtl-all -dP
。想要更加详细的日志,可以用 -fdump-tree-all-details
甚至 -fdump-tree-all-all
。
为了给 GCC 打断点,可以添加 -v
参数,找到实际的 cc1
命令行调用,再用调试器调试 cc1。GCC 提供了 gdbinit 配置:gcc/gdbinit.in
。
测试 GCC
参考:Installing GCC: Testing Working with the testsuite
运行所有 gcc 测试:make check-gcc
。针对测试类型进行过滤:
# test according to all files named execute.exp
make check-gcc RUNTESTFLAGS="execute.exp"
# test according to all files named execute.exp
# ./gcc/testsuite/g++.target/loongarch/loongarch.exp
# ./gcc/testsuite/gcc.target/loongarch/loongarch.exp
make check-gcc RUNTESTFLAGS="loongarch.exp"
进一步,限制到某个测例,例如要测试 gcc.target/loongarch/cas-acquire.c
: