5

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

 3 years ago
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.
neoserver,ios ssh client

从-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)

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使用-pgmcount。 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后端目前仍是年久失修状态

Share Comments


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK