从-fpatchable-function-entry=N[,M]说起
source link: http://maskray.me/blog/2020-02-01-fpatchable-function-entry
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
从-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。
# 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
(glibcsysdeps/x86_64/_mcount.S
gmon/mcount.c
, FreeBSDsys/amd64/amd64/prof_machdep.c
)。 - musl不提供
gcrt1.o
https://www.openwall.com/lists/musl/2014/11/05/2
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)引入-finstrument-functions
, 在函数prologue后插入__cyg_profile_func_enter(callee, caller)
、epilogue前插入__cyg_profile_func_enter(callee, caller)
。程序实现这两个函数后可以记录函数调用。
# 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使用-pg
和mcount
。 Linux定义了mcount
,比较一个函数指针来检查ftrace是否开启,倘若没有开启,mcount
则相当于一个空函数。
#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把"JIT"改成了"AOT"。 构建时,一个Perl script scripts/recordmcount.pl
调用objdump记录所有call mcount
的地址,存储在__mcount_loc
section里。Kernel启动时预先把所有call mcount
修改成NOP,免去了daemon。 由于Perl+objdump太慢,2010年,16444a8a40d添加了一个C实现scripts/recordmcount.c
。
GCC r162651 (2010) (GCC 4.6)引入-mfentry
, 把prologue后的call mcount
改成prologue前的call __fentry__
。
mcount有一个弊端是stack frame size难以确定,ftrace不能访问tracee的参数。2011年,d57c5d51a30添加了x86-64的-mfentry
支持。
GCC r206111 (2013)引入了SystemZ特有的-mhotpatch
。 注意描述,function entry后仅有一个NOP,对entry前的NOP类型进行了限定。这样缺乏通用性,其他arch用不上。后来一般化为-mhotpatch=pre-halfwords,post-halfwords
。
GCC r215629 (2014)引入-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。)
GCC r250521 (2017)引入-fpatchable-function-entry=N[,M]
。 和SystemZ特有选项-mhotpatch=
类似,在function entry前插入M个NOP,在entry后插入N-M个NOP。现在被Linux arm64和parisc采用。这个功能设计理念挺好的,可惜实现有诸多问题,仅能用于Linux kernel。
# 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
- https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93197
__patchable_function_entries
会被ld --gc-sections
(linker section garbage collection)收集。导致GCC的实现无法用于大部分程序。 - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93195
__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’
- https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93194
__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 ;"- https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92424 AArch64 Branch Target Identification开启时,NOP sled应在BTI后
- https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93492 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的功能:
除AArch64 BTI外,其余问题都是我报告的~
给clang添加-fpatchable-function-entry=的步骤如下:
- D72215 引入LLVM function attribute "patchable-function-entry",AArch64 AsmPrinter支持
- D72220 x86 AsmPrinter支持
- D72221 在clang里实现function attribute
__attribute__((patchable_function_entry(0,0)))
- D72222 给clang添加driver option
-fpatchable-function-entry=N[,0]
- D73070 引入LLVM function attribute "patchable-function-prefix"
- 移动codegen passes,改变NOP sled与BTI/ENDBR的顺序,顺便修好了XRay、-mfentry与-fcf-protection=branch的协作。
- 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后端目前仍是年久失修状态
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK