从-fpatchable-function-entry=N[,M]说起
2020-02-01 17:00:00
Author: maskray.me(查看原文)
阅读量:128
收藏
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后端目前仍是年久失修状态
Newer
gcov与LLVM中的实现
Older
2019年总结——工具链的一年
文章来源: http://maskray.me/blog/2020-02-01-fpatchable-function-entry 如有侵权请联系:admin#unsafe.sh