从-fpatchable-function-entry=N[,M]说起

Linux kernel用了很多GCC选项支持ftrace。 * `-pg` * `-mfentry` * `-mnop-mcount` * `-mrecord-mcount` * `-mhotpatch=pre-halfwords,post-halfwords` * `-fpatchable-function-entry=N[,M]` 在当前GCC git repo的“史前”时期(Initial revision)就能看到`-pg`支持了。`-pg`在函数prologue后插入`mcount()`(Linux x86),在其他OS或arch上可能叫不同名字,如`_mcount`、`__mcount`、`.mcount`。 trace信息可用于gprof和gcov。
1
2
3
4
5
# gcc -S -pg -O3 -fno-asynchronous-unwind-tables
foo:
pushq %rbp
movq %rsp, %rbp
1: call *mcount@GOTPCREL(%rip)
* 链接时GCC会选择一个不同的crt1文件`gcrt1.o` * libc实现`gcrt1.o` (glibc `sysdeps/x86_64/_mcount.S` `gmon/mcount.c`, FreeBSD `sys/amd64/amd64/prof_machdep.c`)。 * musl不提供`gcrt1.o` glibc的用法: * `gcrt1.o`定义`__gmon_start__`。其他`crt1.o`没有定义 * `crti.o`用undefined weak `__gmon_start__`检测`gcrt1.o`,是则调用 * `gcrt1.o`的`__gmon_start__`调用`__monstartup`初始化。在程序运行前初始化完可以避免call-once的同步。 [GCC r21495 (1998)](https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=07417085a14349cde788c5cc10663815da40c26f)引入`-finstrument-functions`, 在函数prologue后插入`__cyg_profile_func_enter(callee, caller)`、epilogue前插入`__cyg_profile_func_enter(callee, caller)`。程序实现这两个函数后可以记录函数调用。
1
2
3
4
5
6
7
8
9
10
11
12
# gcc -S -O3 -finstrument-functions -fno-asynchronous-unwind-tables
foo:
subq $8, %rsp
leaq foo(%rip), %rdi
movq 8(%rsp), %rsi
call __cyg_profile_func_enter@PLT
movq 8(%rsp), %rsi
leaq foo(%rip), %rdi
call __cyg_profile_func_exit@PLT
xorl %eax, %eax
addq $8, %rsp
ret
`-finstrument-functions`作用在inlining前。inlining后,一个函数里可能有多个`__cyg_profile_func_enter()`。如果希望inlining后再trace,可以使用clang的`-finstrument-functions-after-inlining`扩展。 Linux kernel 2008年最早的ftrace实现[16444a8a40d](https://github.com/torvalds/linux/commit/16444a8a40d4c7b4f6de34af0cae1f76a4f6c901)使用`-pg`和`mcount`。 Linux定义了`mcount`,比较一个函数指针来检查ftrace是否开启,倘若没有开启,`mcount`则相当于一个空函数。
1
2
3
4
5
6
7
8
#ifdef CONFIG_FTRACE
ENTRY(mcount)
cmpq $ftrace_stub, ftrace_trace_function
jnz trace
.globl ftrace_stub
ftrace_stub:
...
#endif
所有函数的prologue后都执行`call mcount`,会产生很大的开销。因此,后来Linux kernel在一个hash table里记录mcount的caller的PC,用一个一秒运行一次的daemon检查hash table,把不需要trace的函数的`call mcount`修改成NOP。 之后,[8da3821ba56](https://github.com/torvalds/linux/commit/16444a8a40d4c7b4f6de34af0cae1f76a4f6c901)把"JIT"改成了"AOT"。 构建时,一个Perl script `scripts/recordmcount.pl`调用objdump记录所有`call mcount`的地址,存储在`__mcount_loc` section里。Kernel启动时预先把所有`call mcount`修改成NOP,免去了daemon。 由于Perl+objdump太慢,2010年,[16444a8a40d](https://github.com/torvalds/linux/commit/16444a8a40d4c7b4f6de34af0cae1f76a4f6c901)添加了一个C实现`scripts/recordmcount.c`。 [GCC r162651 (2010) (GCC 4.6)](https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=3c5273a96ba8dbf98c40bc6d9d0a1587b4cfedb2)引入`-mfentry`, 把prologue后的`call mcount`改成prologue前的`call __fentry__`。 mcount有一个弊端是stack frame size难以确定,ftrace不能访问tracee的参数。2011年,[d57c5d51a30](https://github.com/torvalds/linux/commit/d57c5d51a30152f3175d2344cb6395f08bf8ee0c)添加了x86-64的`-mfentry`支持。 [GCC r206111 (2013)](https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=d0de9e136f1dbe307e1d6ebb04b23131056cfa29)引入了SystemZ特有的`-mhotpatch`。 注意描述,function entry后仅有一个NOP,对entry前的NOP类型进行了限定。这样缺乏通用性,其他arch用不上。后来一般化为`-mhotpatch=pre-halfwords,post-halfwords`。 [GCC r215629 (2014)](https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=ecc81e33123d7ac9c11742161e128858d844b99d)引入`-mrecord-mcount`、`-mnop-mcount`。 `-mrecord-mcount`用于代替`linux/scripts/record_mcount.{pl,c}`。`-mnop-mcount`不可用于PIC,把`__fentry__`替换成NOP。 设计时没有考虑通用性,大多数RISC都用不上不带参数的`-mnop-mcount`。截至今天,`-mnop-mcount`只有x86和SystemZ支持。 (2019年,Linux x86移除了mcount支持[562e14f7229](https://github.com/torvalds/linux/commit/562e14f72292249e52e6346a9e3a30be652b0cf6)。) [GCC r250521 (2017)](https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=417ca0117a1a9a8aaf5bc5ca530adfd68cb00399)引入`-fpatchable-function-entry=N[,M]`。 和SystemZ特有选项`-mhotpatch=`类似,在function entry前插入M个NOP,在entry后插入N-M个NOP。现在被Linux arm64和parisc采用。这个功能设计理念挺好的,可惜实现又诸多问题,仅能用于Linux kernel。
1
2
3
4
5
6
7
8
9
10
11
12
# gcc -fpatchable-function-entry=3,1 -S -O3 a.c -fno-asynchronous-unwind-tables
.section __patchable_function_entries,"aw",@progbits
.quad .LPFE1
.text
.LPFE1:
nop
.type foo, @function
foo:
nop
nop
xorl %eax, %eax
ret
* `__patchable_function_entries`会被`ld --gc-sections`(linker section garbage collection)收集。导致GCC的实现无法用于大部分程序。 * `__patchable_function_entries` entry所属的COMDAT section group被收集会产生链接错误。导致很多使用`inline`的C++程序无法使用。 * 错误信息写错选项名:`gcc -fpatchable-function-entry=a -c a.c` => `cc1: error: invalid arguments for ‘-fpatchable_function_entry’` * `__patchable_function_entries`没有指定section alignment。我的第二个GCC patch~ * `__patchable_function_entries`的entries应用PC-relative relocations,而非absolute relocations,避免链接后生成`R_*_RELATIVE` dynamic relocations。 这一点我一开始不能接受,因为其他缺陷clang这边修复后也能保持backward compatible,但relocation type是没法改的。 后来我认识到MIPS没有提供`R_MIPS_PC64`……那么选择原谅GCC了。MIPS就是这样,ISA缺陷->psABI“发明”聪明的ELF技巧绕过+引入新的问题。 "mips is really the worst abi i've ever seen." "you mean worst dozen abis ;" * AArch64 Branch Target Identification开启时,NOP sled应在BTI后 * x86 Indirect Branch Tracking开启时,NOP sled应在ENDBR32/ENDBR64后。 在开始实现-fpatchable-function-entry=前,正巧给lld加-z force-ibt。因此在看到AArch64问题很自然地想到了x86也有类似问题。 * 没有考虑和`-fasynchronous-unwind-tables`的协作。再一次,Linux kernel使用`-fno-asynchronous-unwind-tables`。所以GCC实现时很自然地没有思考这个问题 * Initial `.loc` directive应在NOP sled前。会导致symbolize function address得不到文件名/行号信息 修复--gc-sections和COMDAT比较棘手,还需要binutils这边的GNU as和GNU ld的功能: * 支持unique section ID。2月2日GNU as添加了支持 * 支持`SHF_LINK_ORDER`。HJ Lu发了patch: * GNU ld --gc-sections semantics 除AArch64 BTI外,其余问题都是我报告的~ 给clang添加-fpatchable-function-entry=的步骤如下: * [D72215](https://reviews.llvm.org/D72215) 引入LLVM function attribute "patchable-function-entry",AArch64 AsmPrinter支持 * [D72220](https://reviews.llvm.org/D72220) x86 AsmPrinter支持 * [D72221](https://reviews.llvm.org/D72221) 在clang里实现function attribute `__attribute__((patchable_function_entry(0,0)))` * [D72222](https://reviews.llvm.org/D72222) 给clang添加driver option `-fpatchable-function-entry=N[,0]` * [D73070](https://reviews.llvm.org/D73070) 引入LLVM function attribute "patchable-function-prefix" * 移动codegen passes,改变NOP sled与BTI/ENDBR的顺序,顺便修好了XRay、-mfentry与-fcf-protection=branch的协作。 * [D73680](https://reviews.llvm.org/D73680) AArch64 BTI,处理M=0时,patch label的位置:`bti c; .Lpatch0: nop`而不是`.Lpatch0: bti c; nop` * x86 ENDBR32/ENDBR64,处理M=0时,patch label的位置:`endbr64; .Lpatch0: nop`而不是`.Lpatch0: endbr64; nop` 上述patches,除了x86 ENDBR的patch label位置调整,都会包含在clang 10.0.0里。 在-fpatchable-function-entry=之前,clang已经有多种在function entry插入代码的方法了: * `-fxray-instrument`。XRay使用类似`-finstrument-functions`的方法trace,和Linux kernel类似,运行时修改代码 * Azul Systems引入了PatchableFunction用于JIT。我引入"patchable-function-entry"时就复用了这个pass * IR feature: prologue data,在function entry后添加任意字节。用于function sanitizer * IR feature: prefix data,在function entry前添加任意字节。用于GHC `TABLES_NEXT_TO_CODE`。Info table放在entry code前。GHC的LLVM后端目前仍是年久失修状态