From fa67c33b0732b67c9d33c043c02d9687c03221a9 Mon Sep 17 00:00:00 2001 From: Juhan Jin Date: Sat, 29 Jun 2024 02:44:40 +0800 Subject: [PATCH] added an article analyzing out-of-line static call implmentation for x86 Signed-off-by: Juhan Jin --- .../20240619-out-of-line-static-call-x86.md | 894 ++++++++++++++++++ 1 file changed, 894 insertions(+) create mode 100644 articles/20240619-out-of-line-static-call-x86.md diff --git a/articles/20240619-out-of-line-static-call-x86.md b/articles/20240619-out-of-line-static-call-x86.md new file mode 100644 index 0000000..3cd2064 --- /dev/null +++ b/articles/20240619-out-of-line-static-call-x86.md @@ -0,0 +1,894 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [comments codeblock codeinline]
+> Author: 金钜涵 juhan.jin@foxmail.com
+> Date: 2024/06/19
+> Revisor: NULL
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [【老师提案】Static Call 技术分析与 RISC-V 移植](https://gitee.com/tinylab/riscv-linux/issues/I5Y585)
+> Sponsor: PLCT Lab, ISCAS +> Environ: [Linux Lab Disk](https://tinylab.org/linux-lab-disk) + +# Out-of-line Static Call x86 代码分析 + +## 前言 + +Static Call 分为 out-of-line、inline、generic 3 个版本。本文将对 out-of-line 版本的代码实现进行分析,架构相关部分将分析 x86 相关代码。Linux 版本为 6.9.2。 + +## API 陈列 + +`include/linux/static_call.h` 开头的注释给出了 API: + +```c +// include/linux/static_call.h:17 + +DECLARE_STATIC_CALL(name, func); +DEFINE_STATIC_CALL(name, func); +DEFINE_STATIC_CALL_NULL(name, typename); +DEFINE_STATIC_CALL_RET0(name, typename); + +__static_call_return0; + +static_call(name)(args...); +static_call_cond(name)(args...); +static_call_update(name, func); +static_call_query(name); + +EXPORT_STATIC_CALL{,_TRAMP}{,_GPL}() +``` + +我们先关注比较重要的 3 个 API: + +- 用 `DEFINE_STATIC_CALL` 定义一个 static call; +- 用 `static_call` 执行这个 static call; +- 用 `static_call_update` 更新这个 static call。 + +然后关注剩下的 API。 + +## API 实现 + +### 代码文件分布 + +架构无关的代码实现主要在: + +- `include/linux/static_call.h` +- `include/linux/static_call_types.h` +- `kernel/static_call.c` + +架构相关的代码实现主要在(对于 x86): + +- `arch/x86/include/asm/static_call.h` +- `arch/x86/kernel/static_call.c` + +### 数据结构 + +首先我们需要知道 out-of-line 版本定义了哪些数据结构。`include/linux/static_call_types.h` 中提供了 `static_call_key`,它的作用主要是记录当前 static call 的函数指向: + +```c +// include/linux/static_call_types.h:76 +struct static_call_key { + void *func; +}; +``` + +### 几个辅助宏 + +`include/linux/static_call_types.h` 的开头定义了几个辅助宏,`STATIC_CALL_KEY` 类宏主要和记录函数指向的结构体 `static_call_key` 有关,`STATIC_CALL_TRAMP` 类宏主要和 static call 对应的 trampoline 有关系。 + +```c +// include/linux/static_call_types.h:9,三个版本定义一样 + +#define STATIC_CALL_KEY_PREFIX __SCK__ +#define STATIC_CALL_KEY_PREFIX_STR __stringify(STATIC_CALL_KEY_PREFIX) +#define STATIC_CALL_KEY_PREFIX_LEN (sizeof(STATIC_CALL_KEY_PREFIX_STR) - 1) +#define STATIC_CALL_KEY(name) __PASTE(STATIC_CALL_KEY_PREFIX, name) +#define STATIC_CALL_KEY_STR(name) __stringify(STATIC_CALL_KEY(name)) + +#define STATIC_CALL_TRAMP_PREFIX __SCT__ +#define STATIC_CALL_TRAMP_PREFIX_STR __stringify(STATIC_CALL_TRAMP_PREFIX) +#define STATIC_CALL_TRAMP_PREFIX_LEN (sizeof(STATIC_CALL_TRAMP_PREFIX_STR) - 1) +#define STATIC_CALL_TRAMP(name) __PASTE(STATIC_CALL_TRAMP_PREFIX, name) +#define STATIC_CALL_TRAMP_STR(name) __stringify(STATIC_CALL_TRAMP(name)) +``` + +这些宏中主要被使用的是 `STATIC_CALL_KEY`、`STATIC_CALL_KEY_STR` 以及 `STATIC_CALL_TRAMP`、`STATIC_CALL_TRAMP_STR`,它们的具体展开如下所示: + +```c +STATIC_CALL_KEY(my_name) // __SCK__my_name +STATIC_CALL_KEY_STR(my_name) // "__SCK__my_name" +STATIC_CALL_TRAMP(my_name) // __SCT__my_name +STATIC_CALL_TRAMP_STR(my_name) // "__SCT__my_name" +``` + +### DEFINE_STATIC_CALL + +`DEFINE_STATIC_CALL` 在 `include/linux/static_call.h` 中定义,out-of-line 版本的定义如下: + +```c +// include/linux/static_call.h:230 +#define DEFINE_STATIC_CALL(name, _func) \ + DECLARE_STATIC_CALL(name, _func); \ + struct static_call_key STATIC_CALL_KEY(name) = { \ + .func = _func, \ + }; \ + ARCH_DEFINE_STATIC_CALL_TRAMP(name, _func) +``` + +`DECLARE_STATIC_CALL` 在 `include/linux/static_call_types.h` 中定义,3 个版本的定义都一样: + +```c +// include/linux/static_call_types.h:37 +#define DECLARE_STATIC_CALL(name, func) \ + extern struct static_call_key STATIC_CALL_KEY(name); \ + extern typeof(func) STATIC_CALL_TRAMP(name); +``` + +假设我们像这样调用 `DEFINE_STATIC_CALL`:`int func_a(int arg1, int arg2); DEFINE_STATIC_CALL(my_name, func_a);`。那么实际上我们会得到: + +```c +int func_a(int arg1, int arg2); + +extern struct static_call_key __SCK__my_name; +extern int __SCT__my_name(int arg1, int arg2); +struct static_call_key __SCK__my_name = { + .func = func_a, +}; + +ARCH_DEFINE_STATIC_CALL_TRAMP(my_name, func_a); +``` + +`func_a` 会被记录在 `__SCK__my_name` key 中,同时 `ARCH_DEFINE_STATIC_CALL_TRAMP` 会生成一个名字为 `__SCT__my_name` 的 trampoline。 + +#### ARCH_DEFINE_STATIC_CALL_TRAMP + +而 `ARCH_DEFINE_STATIC_CALL_TRAMP` 这个宏是架构相关的,它的作用是为每一个 static call 制造一个 trampoline。它的 x86 定义在 `arch/x86/include/asm/static_call.h` 中: + +```c +// arch/x86/include/asm/static_call.h:46 +#define ARCH_DEFINE_STATIC_CALL_TRAMP(name, func) \ + __ARCH_DEFINE_STATIC_CALL_TRAMP(name, ".byte 0xe9; .long " #func " - (. + 4)") + +// arch/x86/include/asm/static_call.h:34 +#define __ARCH_DEFINE_STATIC_CALL_TRAMP(name, insns) \ + asm(".pushsection .static_call.text, \"ax\" \n" \ + ".align 4 \n" \ + ".globl " STATIC_CALL_TRAMP_STR(name) " \n" \ + STATIC_CALL_TRAMP_STR(name) ": \n" \ + ANNOTATE_NOENDBR \ + insns " \n" \ + ".byte 0x0f, 0xb9, 0xcc \n" \ + ".type " STATIC_CALL_TRAMP_STR(name) ", @function \n" \ + ".size " STATIC_CALL_TRAMP_STR(name) ", . - " STATIC_CALL_TRAMP_STR(name) " \n" \ + ".popsection \n") +``` + +宏展开之后对应的汇编代码为: + +``` +.pushsection .static_call.text, "ax" +.align 4 +.globl __SCT__my_name +__SCT__my_name: + # ANNOTATE_NOENDBR + 986: + .pushsection .discard.noendbr + .long 986b + .popsection + .byte 0xe9 + .long func_a - (. + 4) + .byte 0x0f, 0xb9, 0xcc +.type __SCT__my_name, @function # 设置 __SCT__my_name 的 type 为 function +.size __SCT__my_name, . - __SCT__my_name # 设置 __SCT__my_name 的 size +.popsection +``` + +这段汇编代码可以参考 [GNU Assembler Documentation][GNU] 进行理解。`.pushsection` 将现在操作的 section 变为 `.static_call.text`。`.align 4` 使后续内容 4 字节对齐。`.globl` 使 `__SCT__my_name` 这个符号可以被整个内核调用。`ANNOTATE_NOENDBR` 和 intel 的 endbr 指令有关,无需深究。`.byte`、`.long`、`.byte` 这三条 directive 对 `.static_call.text` 进行内容填充。最后 `.type` 和 `.size` 对 `__SCT__my_name` 进行相应设置,`.popsection` 回到原来的 section。 + +[GNU]: https://sourceware.org/binutils/docs/as/index.html + +然后我们来看填充内容,可以参考 [Intel Software Developer’s Manual Volume 2 Instruction Set Reference][Intel]。首先填充一条 `JMP rel32` 指令,0xe9 是指令的 opcode,后面跟着计算出来的 4 个字节相对偏移量。然后在后边填充上 `0f b9 cc` 三个字节,这三个字节对应的指令是 `ud1 %esp, %ecx`,即 undefined instructon。这条指令如果被执行,会产生异常。可以看到,我们实际上只需要第一条指令,根据下面的注释,第二条指令主要是让 speculation 停止并给 static call trampoline 提供一个独特特征。 + +[Intel]: https://software.intel.com/en-us/articles/intel-sdm#three-volume + +``` +// arch/x86/include/asm/static_call.h:30 + + * That trailing #UD provides both a speculation stop and serves as a unique + * 3 byte signature identifying static call trampolines. Also see tramp_ud[] + * and __static_call_fixup(). +``` + +所以,`.static_call.text` 这个段实际上用 8 个字节表示一个 trampoline,如下所示。因为我们将 `__SCT__my_name` 设为了 global function,所以它可以在其他地方被调用。 + +``` +<__SCT__my_name>: +e9 xx xx xx xx jmp +0f b9 cc ud1 %esp,%ecx +... +``` + +#### 二进制分析 + +我们可以挑一个使用了 `DEFINE_STATIC_CALL` 的内核 c 文件的二进制文件看一下,比如 `arch/x86/events/intel/core.c`。 + +首先,这个 c 文件调用了 2 次 `DEFINE_STATIC_CALL`: + +```c +// arch/x86/events/intel/core.c:2574 +DEFINE_STATIC_CALL(intel_pmu_set_topdown_event_period, x86_perf_event_set_period); + +// arch/x86/events/intel/core.c:2716 +DEFINE_STATIC_CALL(intel_pmu_update_topdown_event, x86_perf_event_update); +``` + +所以,可以预见的是,`core.o` 的符号表中应该会含有上面两个 trampoline,并且这两个 trampoline 是在 `.static_call.text` 这个 section 中。我们可以用 `readelf -s core.o` 验证一下,有关信息如下所示。可以看到这两个 trampoline 确实是在 `.static_call.text` 中,并且每个的 size 为 8。 + +``` +Symbol table '.symtab' contains 552 entries: + Num: Value Size Type Bind Vis Ndx Name + 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND + 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS core.c + 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text + 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 .data + 4: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .bss + 5: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .static_call.text +... + 376: 0000000000000000 8 FUNC GLOBAL DEFAULT 6 __SCT__intel_pmu[...] + 377: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND x86_perf_event_s[...] + 378: 0000000000000008 8 FUNC GLOBAL DEFAULT 6 __SCT__intel_pmu[...] +... +``` + +最后 `core.o` 会被链接进 `vmlinux`。根据链接脚本 `arch/x86/kernel/vmlinux.lds.S`,`.static_call.text` 段会被链接进 `vmlinux` 的 `.text` 段,所以我们应该可以在 `vmlinux` 符号表中找到这两个属于 `.text` 段的 trampoline。事实确实如此: + +``` +Symbol table '.symtab' contains 200211 entries: + Num: Value Size Type Bind Vis Ndx Name + 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND + 1: ffffffff81000000 0 SECTION LOCAL DEFAULT 1 .text + 2: ffffffff82200000 0 SECTION LOCAL DEFAULT 2 .rodata + 3: ffffffff8281da00 0 SECTION LOCAL DEFAULT 3 .pci_fixup + 4: ffffffff828213e0 0 SECTION LOCAL DEFAULT 4 .tracedata +... +166295: ffffffff81007140 158 FUNC GLOBAL DEFAULT 1 x86_perf_event_update +... +178087: ffffffff81008530 422 FUNC GLOBAL DEFAULT 1 x86_perf_event_s[...] +... +179521: ffffffff81f175f8 8 FUNC GLOBAL DEFAULT 1 __SCT__intel_pmu[...] +... +199689: ffffffff81f17600 8 FUNC GLOBAL DEFAULT 1 __SCT__intel_pmu[...] +... +``` + +我们还可以用 objdump 看一下 `vmlinux` 中这两个 trampoline 的实际内容,如下所示。可以看到,每个 trampoline 就是一个 jmp 指令加上 `0f b9 cc`。那么 jmp 指令对应的跳转地址是否正确呢?可以参考上面的符号表,两个 jmp 指令对应的跳转地址都是正确的,它们分别跳转到 `ffffffff81008530` 和 `ffffffff81007140` 上,这两个地址即为 `x86_perf_event_set_period` 和 `x86_perf_event_update` 的地址。 + +``` +ffffffff81f175f8 <__SCT__intel_pmu_set_topdown_event_period>: +ffffffff81f175f8: e9 33 0f 0f ff jmp ffffffff81008530 +ffffffff81f175fd: 0f b9 cc ud1 %esp,%ecx + +ffffffff81f17600 <__SCT__intel_pmu_update_topdown_event>: +ffffffff81f17600: e9 3b fb 0e ff jmp ffffffff81007140 +ffffffff81f17605: 0f b9 cc ud1 %esp,%ecx +``` + +### static_call + +对 out-of-line 版本的 `static_call`,调用链如下: + +```c +// include/linux/static_call_types.h:90,out-of-line 和 inline 的定义一样 +#define static_call(name) __static_call(name) + +// include/linux/static_call_types.h:74,out-of-line 版本定义如下: +#define __static_call(name) __raw_static_call(name) + +// include/linux/static_call_types.h:43,out-of-line 和 inline 的定义一样 +#define __raw_static_call(name) (&STATIC_CALL_TRAMP(name)) +``` + +所以下面 API 使用例子中的 `static_call(my_name)(arg1, arg2);` 实际会展开为 `(&__SCT__my_name)(arg1, arg2);`,即直接函数调用。因为我们在 `DEFINE_STATIC_CALL` 的时候将 `__SCT__my_name` 的 type 设置成 function 了,所以这个 trampoline 可以看作函数。 + +```c +int func_a(int arg1, int arg2); +DEFINE_STATIC_CALL(my_name, func_a); +static_call(my_name)(arg1, arg2); +``` + +根据 x86 64 位的 calling convention,在调用 `__SCT__my_name` 以前,我们需要将参数放在对应的寄存器中,然后 `call <__SCT__my_name>` 会被执行,call 指令会把返回地址放在栈上,然后跳转到 `__SCT__my_name`。我们已经清楚 `__SCT__my_name` 的内容,实际有用的就是一条 jump 指令 `jmp `。在这条 jump 指令执行后,我们就会跳到 `func_a` 中。 + +注意,在跳到 `func_a` 以后,`func_a` **需要的参数还是在对应的寄存器中**,`func_a` **执行结束以后需要的返回地址还是在栈顶**,所以 `func_a` 可以顺利执行并返回。 + +我们可以看到,static call 对应的 trampoline 调用 `(&__SCT__my_name)(arg1, arg2);` 实际上就等于函数 direct call `func_a(arg1, arg2);`。 + +#### 二进制分析 + +继续我们上一节的 `arch/x86/events/intel/core.c` 的例子,我们选择 `intel_pmu_update_topdown_event` 这个 static call,分析一下它在 `intel_pmu_read_topdown_event` 中的调用。下面是 `intel_pmu_read_topdown_event` 的上下文: + +```c +// arch/x86/events/intel/core.c:2718 +static void intel_pmu_read_topdown_event(struct perf_event *event) +{ + struct cpu_hw_events *cpuc = this_cpu_ptr(&cpu_hw_events); + + /* Only need to call update_topdown_event() once for group read. */ + if ((cpuc->txn_flags & PERF_PMU_TXN_READ) && + !is_slots_event(event)) + return; + + perf_pmu_disable(event->pmu); + static_call(intel_pmu_update_topdown_event)(event); + perf_pmu_enable(event->pmu); +} + +// arch/x86/events/intel/core.c:2732 +static void intel_pmu_read_event(struct perf_event *event) +{ + if (event->hw.flags & PERF_X86_EVENT_AUTO_RELOAD) + intel_pmu_auto_reload_read(event); + else if (is_topdown_count(event)) + intel_pmu_read_topdown_event(event); + else + x86_perf_event_update(event); +} +``` + +我们在 `vmlinux` 的 `.text` 段中找到用 objdump 找到这两个函数对应的汇编代码,如下所示,两个函数应该是被优化在一起了。我们关注 `ffffffff81012bfc` 和 `ffffffff81012bff` 这两行,这两行对应的就是 `static_call(intel_pmu_update_topdown_event)(event);` 这条语句。可以看到,第一个参数被移到了 rdi 寄存器里,然后用 call 指令跳转到 trampoline `__SCT__intel_pmu_update_topdown_event` 上。call 指令编码中存放的是相对偏移量,其值为 `00 f0 49 fc`,加在下条指令的地址 `ffffffff81012c04` 上,即可得到 `ffffffff81f17600`,即 trampoline 的地址。 + +``` +ffffffff81012bb0 : +ffffffff81012bb0: f3 0f 1e fa endbr64 +ffffffff81012bb4: 53 push rbx +ffffffff81012bb5: 8b 87 94 01 00 00 mov eax,DWORD PTR [rdi+0x194] +ffffffff81012bbb: 48 89 fb mov rbx,rdi +ffffffff81012bbe: f6 c4 02 test ah,0x2 +ffffffff81012bc1: 75 4e jne ffffffff81012c11 +ffffffff81012bc3: f6 c4 40 test ah,0x40 +ffffffff81012bc6: 74 22 je ffffffff81012bea +ffffffff81012bc8: 65 48 8b 05 48 2e 00 mov rax,QWORD PTR gs:[rip+0x7f002e48] # 15a18 +ffffffff81012bcf: 7f +ffffffff81012bd0: f6 80 4c 6b 01 00 02 test BYTE PTR [rax+0x16b4c],0x2 +ffffffff81012bd7: 74 17 je ffffffff81012bf0 +ffffffff81012bd9: 66 81 bf e0 00 00 00 cmp WORD PTR [rdi+0xe0],0x400 +ffffffff81012be0: 00 04 +ffffffff81012be2: 74 0c je ffffffff81012bf0 +ffffffff81012be4: 5b pop rbx +ffffffff81012be5: e9 f6 48 f0 00 jmp ffffffff81f174e0 <__x86_return_thunk> +ffffffff81012bea: 5b pop rbx +ffffffff81012beb: e9 50 45 ff ff jmp ffffffff81007140 +ffffffff81012bf0: 48 8b bb 98 00 00 00 mov rdi,QWORD PTR [rbx+0x98] +ffffffff81012bf7: e8 34 ee 1c 00 call ffffffff811e1a30 +ffffffff81012bfc: 48 89 df mov rdi,rbx +ffffffff81012bff: e8 fc 49 f0 00 call ffffffff81f17600 <__SCT__intel_pmu_update_topdown_event> +ffffffff81012c04: 48 8b bb 98 00 00 00 mov rdi,QWORD PTR [rbx+0x98] +ffffffff81012c0b: 5b pop rbx +ffffffff81012c0c: e9 5f ee 1c 00 jmp ffffffff811e1a70 +ffffffff81012c11: 5b pop rbx +ffffffff81012c12: e9 49 83 00 00 jmp ffffffff8101af60 +ffffffff81012c17: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0] +ffffffff81012c1e: 00 00 +``` + +那么 trampoline 是否在这个地址?我们可以用 readelf 查一下 `vmlinux` 的符号表,`__SCT__intel_pmu_update_topdown_event` 确实在 `ffffffff81f17600` 上: + +``` +Symbol table '.symtab' contains 200211 entries: + Num: Value Size Type Bind Vis Ndx Name + 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND + 1: ffffffff81000000 0 SECTION LOCAL DEFAULT 1 .text + 2: ffffffff82200000 0 SECTION LOCAL DEFAULT 2 .rodata + 3: ffffffff8281da00 0 SECTION LOCAL DEFAULT 3 .pci_fixup + 4: ffffffff828213e0 0 SECTION LOCAL DEFAULT 4 .tracedata +... +199689: ffffffff81f17600 8 FUNC GLOBAL DEFAULT 1 __SCT__intel_pmu[...] +... +``` + +### static_call_update + +对 out-of-line 版本的 `static_call_update`,调用链如下: + +```c +// include/linux/static_call.h:152,三个版本的定义一样 +#define static_call_update(name, func) \ +({ \ + typeof(&STATIC_CALL_TRAMP(name)) __F = (func); \ + __static_call_update(&STATIC_CALL_KEY(name), \ + STATIC_CALL_TRAMP_ADDR(name), __F); \ +}) + +// include/linux/static_call.h:146,out-of-line 和 inline 的定义一样 +#define STATIC_CALL_TRAMP_ADDR(name) &STATIC_CALL_TRAMP(name) + +// include/linux/static_call.h:253,out-of-line 版本定义如下 +static inline +void __static_call_update(struct static_call_key *key, void *tramp, void *func) +{ + cpus_read_lock(); + WRITE_ONCE(key->func, func); + arch_static_call_transform(NULL, tramp, func, false); + cpus_read_unlock(); +} +``` + +我们继续之前 API 使用例子,在定义好 static call `my_name` 以后,将它的指向用 `static_call_update` 改变。 + +```c +int func_a(int arg1, int arg2); +int func_b(int arg1, int arg2); + +DEFINE_STATIC_CALL(my_name, func_a); +static_call(my_name)(arg1, arg2); +static_call_update(my_name, &func_b); +static_call(my_name)(arg1, arg2); +``` + +其中的 `static_call_update(my_name, &func_b);` 实际展开为: + +```c +int (* __F)(arg1, arg2) = (&func_b); + +cpus_read_lock(); +WRITE_ONCE((&__SCK__my_name)->func, __F); +arch_static_call_transform(NULL, &__SCT__my__name, __F, false); +cpus_read_unlock(); +``` + +第一条语句用一个临时的函数指针 `__F` 记录了 `func_b`。`cpus_read_lock/cpus_read_unlock` 主要处理 cpu 热插拔导致的 race condition,无需深究。`WRITE_ONCE` 主要防止编译器优化。在将新函数 `func_b` 记录到 `__SCK__my_name` 以后,`arch_static_call_transform` 这个函数对 trampoline 进行具体的更新操作。 + +#### arch_static_call_transform + +`arch_static_call_transform` 这个函数是架构相关的,它的 x86 定义在 `arch/x86/kernel/static_call.c` 中,如下所示: + +```c +// arch/x86/kernel/static_call.c:157 +// 参数为 NULL, &__SCT__my__name, __F, false +void arch_static_call_transform(void *site, void *tramp, void *func, bool tail) +{ + mutex_lock(&text_mutex); + + if (tramp) { + __static_call_validate(tramp, true, true); + __static_call_transform(tramp, __sc_insn(!func, true), func, false); + } + + if (IS_ENABLED(CONFIG_HAVE_STATIC_CALL_INLINE) && site) { + __static_call_validate(site, tail, false); + __static_call_transform(site, __sc_insn(!func, tail), func, false); + } + + mutex_unlock(&text_mutex); +} +``` + +`text_mutex` 主要防止内核中的指令被多方同时修改。因为参数 `tramp` 为 `&__SCT__my__name`,所以执行第一个 if 语句。因为目前我们讨论的是 out-of-line 版本,所以第二个 if 语句不执行。 + +在第一个 if 语句中,先进行验证。验证函数 `__static_call_validate` 如下。该函数首先将 trampoline 的后三个字节和 `tramp_ud` 进行比较,如果该 trampoline 是使用 `DEFINE_STATIC_CALL` 定义的,那么后三个字节一定是 `0f b9 cc`。验证通过后,再看 trampoline 的第一个字节是否是 `JMP32_INSN_OPCODE`,即 `e9`,很显然是。经过这两步后,验证通过。 + +```c +// arch/x86/kernel/static_call.c:20 +static const u8 tramp_ud[] = { 0x0f, 0xb9, 0xcc }; + +// arch/x86/kernel/static_call.c:114 +// 参数为 &__SCT__my__name, true, true +static void __static_call_validate(u8 *insn, bool tail, bool tramp) +{ + u8 opcode = insn[0]; + + if (tramp && memcmp(insn+5, tramp_ud, 3)) { + pr_err("trampoline signature fail"); + BUG(); + } + + if (tail) { + if (opcode == JMP32_INSN_OPCODE || + opcode == RET_INSN_OPCODE || + __is_Jcc(insn)) + return; + } else { + if (opcode == CALL_INSN_OPCODE || + !memcmp(insn, x86_nops[5], 5) || + !memcmp(insn, xor5rax, 5)) + return; + } + + /* + * If we ever trigger this, our text is corrupt, we'll probably not live long. + */ + pr_err("unexpected static_call insn opcode 0x%x at %pS\n", opcode, insn); + BUG(); +} +``` + +现在,我们只需要关注 `__static_call_transform`。在讨论 `__static_call_transform` 的定义前,我们需要先算出它的第二个参数。它的第二个参数为 `__sc_insn(!__F, true)`。如果 `__F` 即我们的新函数不是 null ,那么 `__sc_insn` 返回枚举值 `JMP`,表示更新 trampoline 以后,我们依然使用 jmp 指令;如果 `__F` 是 null,那么 `__sc_insn` 返回枚举值 `RET`,表示更新 trampoline 以后,jmp 指令被替换为 ret 指令。 + +现在我们可以来讨论 `__static_call_transform` 了。首先,函数内检测 trampoline 的第一条指令是不是 jcc 指令,即条件跳转指令。我们的 trampoline 显然不是。接下来,根据 `type` 执行不同的分支,现在我们的 `type` 只可能是 `JMP` 或者 `RET`,先讨论 `JMP` 的情况。 + +```c +// arch/x86/kernel/static_call.c:53 +// 参数为 &__SCT__my__name, JMP/RET, __F, false +static void __ref __static_call_transform(void *insn, enum insn_type type, + void *func, bool modinit) +{ + const void *emulate = NULL; + int size = CALL_INSN_SIZE; + const void *code; + u8 op, buf[6]; + + if ((type == JMP || type == RET) && (op = __is_Jcc(insn))) + type = JCC; + + switch (type) { + case CALL: + func = callthunks_translate_call_dest(func); + code = text_gen_insn(CALL_INSN_OPCODE, insn, func); + if (func == &__static_call_return0) { + emulate = code; + code = &xor5rax; + } + + break; + + case NOP: + code = x86_nops[5]; + break; + + case JMP: + code = text_gen_insn(JMP32_INSN_OPCODE, insn, func); + break; + + case RET: + if (cpu_feature_enabled(X86_FEATURE_RETHUNK)) + code = text_gen_insn(JMP32_INSN_OPCODE, insn, x86_return_thunk); + else + code = &retinsn; + break; + + case JCC: + if (!func) { + func = __static_call_return; + if (cpu_feature_enabled(X86_FEATURE_RETHUNK)) + func = x86_return_thunk; + } + + buf[0] = 0x0f; + __text_gen_insn(buf+1, op, insn+1, func, 5); + code = buf; + size = 6; + + break; + } + + if (memcmp(insn, code, size) == 0) + return; + + if (system_state == SYSTEM_BOOTING || modinit) + return text_poke_early(insn, code, size); + + text_poke_bp(insn, code, size, emulate); +} +``` + +`JMP` 情况下,我们需要准备好 trampoline 需要的新 jmp 指令。`text_gen_insn` 的调用实际上是 `text_gen_insn(JMP32_INSN_OPCODE, &__SCT__my__name, __F);`。该函数调用开了一个 5 字节的 buffer,这个 buffer 的第一个字节为 `JMP32_INSN_OPCODE`,后面四个字节为 `__SCT__my__name` 到 `__F` 的相对偏移量。然后 `code` 会指向这个 buffer。这个 buffer 中存放的就是新的 jmp 指令。 + +`RET` 情况下,我们需要准备好 trampoline 需要的新 ret 指令。首先检测 cpu 是否启用了 `X86_FEATURE_RETHUNK`。如果没有启用,`code` 指向 `retinsn`,`retinsn` 的内容为 `{ RET_INSN_OPCODE, 0xcc, 0xcc, 0xcc, 0xcc }`。根据 [Intel Software Developer’s Manual Volume 2 Instruction Set Reference][Intel],`RET_INSN_OPCODE` 即 `0xc3` 对应 ret 指令,`0xcc` 对应 int3 指令,执行时会产生 trap。所以新指令中实际有用的只有 ret 指令。如果 `X86_FEATURE_RETHUNK` 启用了,那么我们需要用 `text_gen_insn` 函数生成一条新的 jmp 指令,这个 jmp 指令的目的地是 `x86_return_thunk`,具体的返回操作在这个 thunk 里面完成。 + +然后我们比较 trampoline 的前五个字节是否和 `code` 指向的新指令内容一样,如果一样,trampoline 不需要更新;如果不一样,使用 `text_poke_bp` 函数或者 `text_poke_early` 函数进行具体的更新操作。 + +更新操作完成以后,trampoline `__SCT__my__name` 的前五个字节对应的就是新函数 `func_b`。 + +三个主要 API 已经分析完成,接下来我们分析一下剩下的 API。 + +### static_call_query + +`static_call_query` 在 `include/linux/static_call.h` 中定义。可以看到,`static_call_query` 读取了 key 中存储的函数指向。 + +```c +// include/linux/static_call.h:159,三个版本的定义一样 +#define static_call_query(name) (READ_ONCE(STATIC_CALL_KEY(name).func)) +``` + +### DEFINE_STATIC_CALL_NULL + +根据 `include/linux/static_call.h` 中的注释,`DEFINE_STATIC_CALL_NULL` 主要用来定义指向 NULL 的 static call。我们可以用 `DECLARE_STATIC_CALL_NULL(name, typename);` 定义一个指向 NULL 的 static call,如 `DECLARE_STATIC_CALL_NULL(my_static_call, void (*)(int));`。`typename` 即函数类型,当然把 `typename` 换成函数名称也是可以的。 + +``` +// include/linux/static_call.h:66 + + * Notes on NULL function pointers: + * + * Static_call()s support NULL functions, with many of the caveats that + * regular function pointers have. + * + * Clearly calling a NULL function pointer is 'BAD', so too for + * static_call()s (although when HAVE_STATIC_CALL it might not be immediately + * fatal). A NULL static_call can be the result of: + * + * DECLARE_STATIC_CALL_NULL(my_static_call, void (*)(int)); + * + * which is equivalent to declaring a NULL function pointer with just a + * typename: + * + * void (*my_func_ptr)(int arg1) = NULL; + * + * or using static_call_update() with a NULL function. In both cases the + * HAVE_STATIC_CALL implementation will patch the trampoline with a RET + * instruction, instead of an immediate tail-call JMP. HAVE_STATIC_CALL_INLINE + * architectures can patch the trampoline call to a NOP. +``` + +`DEFINE_STATIC_CALL_NULL` 在 `include/linux/static_call.h` 中定义,out-of-line 版本定义如下, + +```c +// include/linux/static_call.h:237 +#define DEFINE_STATIC_CALL_NULL(name, _func) \ + DECLARE_STATIC_CALL(name, _func); \ + struct static_call_key STATIC_CALL_KEY(name) = { \ + .func = NULL, \ + }; \ + ARCH_DEFINE_STATIC_CALL_NULL_TRAMP(name) +``` + +`DECLARE_STATIC_CALL` 在 `include/linux/static_call_types.h` 中定义,3 个版本的定义都一样: + +```c +// include/linux/static_call_types.h:37 +#define DECLARE_STATIC_CALL(name, func) \ + extern struct static_call_key STATIC_CALL_KEY(name); \ + extern typeof(func) STATIC_CALL_TRAMP(name); +``` + +假设我们像这样调用 `DEFINE_STATIC_CALL`:`DECLARE_STATIC_CALL_NULL(my_name, void (*)(int));`。那么实际上我们会得到: + +```c +extern struct static_call_key __SCK__my_name; +extern void __SCT__my_name(int arg); +struct static_call_key __SCK__my_name = { + .func = NULL, +}; + +ARCH_DEFINE_STATIC_CALL_NULL_TRAMP(my_name); +``` + +#### ARCH_DEFINE_STATIC_CALL_NULL_TRAMP + +`ARCH_DEFINE_STATIC_CALL_NULL_TRAMP` 在 `arch/x86/include/asm/static_call.h` 中定义,定义如下: + +```c +// arch/x86/include/asm/static_call.h:49 + +#ifdef CONFIG_MITIGATION_RETHUNK +#define ARCH_DEFINE_STATIC_CALL_NULL_TRAMP(name) \ + __ARCH_DEFINE_STATIC_CALL_TRAMP(name, "jmp __x86_return_thunk") +#else +#define ARCH_DEFINE_STATIC_CALL_NULL_TRAMP(name) \ + __ARCH_DEFINE_STATIC_CALL_TRAMP(name, "ret; int3; nop; nop; nop") +#endif +``` + +可以看到,`ARCH_DEFINE_STATIC_CALL_NULL_TRAMP` 主要调用 `__ARCH_DEFINE_STATIC_CALL_TRAMP` 制造 trampoline。`__ARCH_DEFINE_STATIC_CALL_TRAMP` 在上面已经分析过,主要作用就是将第二个参数对应的五字节指令以及三个验证字节制造成一个 trampoline。 + +如果内核配置中打开了 `CONFIG_MITIGATION_RETHUNK`,那么 trampoline 的第一条五字节指令是 `jmp __x86_return_thunk`,表示跳转到 `__x86_return_thunk` 这个 thunk,由这个 thunk 进行实际的返回操作;如果没有打开 `CONFIG_MITIGATION_RETHUNK`,那么 trampoline 的前五个字节是 `ret; int3; nop; nop; nop`,由 ret 指令进行返回操作。 + +#### 函数返回类型只能为 void + +使用 `DECLARE_STATIC_CALL_NULL` 声明 static call 时,函数类型的返回类型只能为 void,因为理论上 NULL 函数本来就什么都不用做,不需要返回值;函数类型的参数列表倒是无所谓,可以随意设置。 + + + +#### 不能使用 DEFINE_STATIC_CALL + +注意,如果要定义指向 NULL 的 static call,只能使用 `DEFINE_STATIC_CALL_NULL`,不能使用 `DEFINE_STATIC_CALL`。 + +### static_call_cond + +根据 `include/linux/static_call.h` 中的注释,`static_call_cond` 有「required value tests to avoid NULL-pointer dereferences」。 + +``` +// include/linux/static_call.h:87 + + * In all cases, any argument evaluation is unconditional. Unlike a regular + * conditional function pointer call: + * + * if (my_func_ptr) + * my_func_ptr(arg1) + * + * where the argument evaludation also depends on the pointer value. + * + * When calling a static_call that can be NULL, use: + * + * static_call_cond(name)(arg1); + * + * which will include the required value tests to avoid NULL-pointer + * dereferences. + * +``` + +对 out-of-line 版本的 `static_call_cond`,调用链如下: + +```c +// include/linux/static_call.h:251,三个版本定义一样 +#define static_call_cond(name) (void)__static_call(name) + +// include/linux/static_call_types.h:74,out-of-line 版本定义如下: +#define __static_call(name) __raw_static_call(name) + +// include/linux/static_call_types.h:43,out-of-line 和 inline 的定义一样 +#define __raw_static_call(name) (&STATIC_CALL_TRAMP(name)) +``` + +所以下面 API 使用例子中的 `static_call_cond(my_name)(arg);` 实际会展开为 `(void)(&__SCT__my_name)(arg);`。这是一个函数调用,函数调用返回值以后,值被转化为 void 类型。 + +```c +DECLARE_STATIC_CALL_NULL(my_name, void (*)(int)); +static_call_cond(my_name)(arg); +``` + +可以看到,其实 `static_call_cond` 展开以后,就比 `static_call` 多了 `(void)`。所以实际上,不管 static call 指向的函数是什么,`static_call_cond` 都会跳到对应的 trampoline 上,执行 ret 指令或 jmp 指令。`static_call_cond` 中并不存在「required value tests to avoid NULL-pointer dereferences」。 + +### DEFINE_STATIC_CALL_RET0 + +我们可以用 `DECLARE_STATIC_CALL_RET0(name, typename);` 定义一个返回 0 的 static call,`typename` 即函数类型,当然把 `typename` 换成函数名称也是可以的。 + +`DEFINE_STATIC_CALL_RET0` 在 `include/linux/static_call.h` 中定义,out-of-line 版本的定义如下: + +```c +// include/linux/static_call.h:244 +#define DEFINE_STATIC_CALL_RET0(name, _func) \ + DECLARE_STATIC_CALL(name, _func); \ + struct static_call_key STATIC_CALL_KEY(name) = { \ + .func = __static_call_return0, \ + }; \ + ARCH_DEFINE_STATIC_CALL_RET0_TRAMP(name) +``` + +`DECLARE_STATIC_CALL` 在 `include/linux/static_call_types.h` 中定义,3 个版本的定义都一样: + +```c +// include/linux/static_call_types.h:37 +#define DECLARE_STATIC_CALL(name, func) \ + extern struct static_call_key STATIC_CALL_KEY(name); \ + extern typeof(func) STATIC_CALL_TRAMP(name); +``` + +假设我们像这样调用 `DEFINE_STATIC_CALL_RET0`:`DEFINE_STATIC_CALL_RET0(my_name, void (*)(int));`。那么实际上我们会得到: + +```c +extern struct static_call_key __SCK__my_name; +extern void __SCT__my_name(int); +struct static_call_key __SCK__my_name = { + .func = __static_call_return0, +}; + +ARCH_DEFINE_STATIC_CALL_RET0_TRAMP(my_name); +``` + +#### __static_call_return0 + +`__static_call_return0` 在 `kernel/static_call.c` 中定义,定义如下: + +```c +// kernel/static_call.c:4 +long __static_call_return0(void) +{ + return 0; +} +``` + +实际上被记录在 key 中的,就是这个函数。 + +#### ARCH_DEFINE_STATIC_CALL_RET0_TRAMP + +`ARCH_DEFINE_STATIC_CALL_RET0_TRAMP` 在 `arch/x86/include/asm/static_call.h` 中定义,定义如下: + +```c +// arch/x86/include/asm/static_call.h:57 +#define ARCH_DEFINE_STATIC_CALL_RET0_TRAMP(name) \ + ARCH_DEFINE_STATIC_CALL_TRAMP(name, __static_call_return0) +``` + +可以看到,`ARCH_DEFINE_STATIC_CALL_RET0_TRAMP` 实际上调用 `ARCH_DEFINE_STATIC_CALL_TRAMP` 制造 trampoline。`ARCH_DEFINE_STATIC_CALL_TRAMP` 在上面已经分析过,主要作用就是制造一个能够跳转到 `__static_call_return0` 的 trampoline。 + +#### 函数签名可以任意指定 + +在上面的分析中,我们可以看到,`__SCT__my_name` 的函数签名可以由我们任意指定,但是实际函数 `__static_call_return0` 的函数签名是固定的:`long __static_call_return0(void)`。这样并不会有问题,我们可以实际分析一下。 + +我们可以在内核代码中找一个实例看看。`arch/x86/events/core.c` 中使用 `DEFINE_STATIC_CALL_RET0` 定义了一个 static call,相关代码如下。可以看到,定义好的 static call `x86_pmu_guest_get_msrs` 在 `perf_guest_get_msrs` 函数中通过 `static_call` 被调用。另外,`__SCT__x86_pmu_guest_get_msrs` 的函数签名是 `struct perf_guest_switch_msr *(*)(int *nr, void *data)`。 + +```c +// arch/x86/events/perf_event.h:752 +struct x86_pmu { + ... + struct perf_guest_switch_msr *(*guest_get_msrs)(int *nr, void *data); + ... +} + +// arch/x86/events/core.c:99 +DEFINE_STATIC_CALL_RET0(x86_pmu_guest_get_msrs, *x86_pmu.guest_get_msrs); + +// arch/x86/events/core.c:698 +struct perf_guest_switch_msr *perf_guest_get_msrs(int *nr, void *data) +{ + return static_call(x86_pmu_guest_get_msrs)(nr, data); +} +``` + +在 vmlinux 中可以找到对应的汇编代码,如下。第一步,`perf_guest_get_msrs` 的调用者将两个参数放到对应的寄存器中,接着执行指令 `call `,call 指令会把返回地址放在栈顶;第二步,进入 `perf_guest_get_msrs`,因为 `perf_guest_get_msrs` 中只有一个 tail call,所以不用 call 指令而用 jmp 指令;第三步,进入 trampoline `__SCT__x86_pmu_guest_get_msrs`;第四步,进入实际函数 `__static_call_return0`,xor 指令将返回值 0 放入 eax 寄存器中,然后由 `__x86_return_thunk` 进行 return 操作。 + +可以看到,在这个过程中,参数寄存器中的内容没有被用到,所以 static call 函数签名的参数部分可以随意设置。另外,就算 static call 函数签名的返回类型和 long 不一样,也没有关系。比如上面 static call 的函数签名返回类型是 `struct perf_guest_switch_msr *`,eax 中的 0 就意味着地址 0。 + +``` +ffffffff810071f0 : +ffffffff810071f0: f3 0f 1e fa endbr64 +ffffffff810071f4: e9 cf 03 f1 00 jmp ffffffff81f175c8 <__SCT__x86_pmu_guest_get_msrs> +ffffffff810071f9: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0] + +ffffffff81f175c8 <__SCT__x86_pmu_guest_get_msrs>: +ffffffff81f175c8: e9 d3 ea 2b ff jmp ffffffff811d60a0 <__static_call_return0> +ffffffff81f175cd: 0f b9 cc ud1 ecx,esp + +ffffffff811d60a0 <__static_call_return0>: +ffffffff811d60a0: f3 0f 1e fa endbr64 +ffffffff811d60a4: 31 c0 xor eax,eax +ffffffff811d60a6: e9 35 14 d4 00 jmp ffffffff81f174e0 <__x86_return_thunk> +ffffffff811d60ab: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0] +``` + +`include/linux/static_call.h` 中的注释也对这个问题进行了解释: + +``` +// include/linux/static_call.h:107 + + * DEFINE_STATIC_CALL_RET0 / __static_call_return0: + * + * Just like how DEFINE_STATIC_CALL_NULL() / static_call_cond() optimize the + * conditional void function call, DEFINE_STATIC_CALL_RET0 / + * __static_call_return0 optimize the do nothing return 0 function. + * + * This feature is strictly UB per the C standard (since it casts a function + * pointer to a different signature) and relies on the architecture ABI to + * make things work. In particular it relies on Caller Stack-cleanup and the + * whole return register being clobbered for short return values. All normal + * CDECL style ABIs conform. + * + * In particular the x86_64 implementation replaces the 5 byte CALL + * instruction at the callsite with a 5 byte clear of the RAX register, + * completely eliding any function call overhead. + * + * Notably argument setup is unconditional. +``` + +## 小结 + +本文主要分析了 out-of-line 版本 static call API 实现,架构相关部分分析了 x86 相关代码: + +- `DEFINE_STATIC_CALL` +- `DECLARE_STATIC_CALL` +- `static_call` +- `static_call_update` +- `static_call_query` +- `DEFINE_STATIC_CALL_NULL` +- `static_call_cond` +- `DEFINE_STATIC_CALL_RET0` + +## 参考资料 + +- [GNU Assembler Documentation][GNU] +- [Intel Software Developer’s Manual Volume 2 Instruction Set Reference][Intel] +- [Static Call 系列(3):如何避免 Meltdown 和 Spectre 并减少对性能的影响](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20221207-static-call-part3-analysis-static-call.md) +- [Static calls in Linux 5.10](https://blog.yossarian.net/2020/12/16/Static-calls-in-Linux-5-10) -- Gitee