why
最近被问到了一个之前没思考过的问题。memcpy这类内存函数总所周知是cpu-feature bound的,即memcpy会根据cpu特性的实现多个版本。例如各个cpu支持的向量指令不同。现在问题来了,每一次调用memcpy时都要先通过cpuid或者xgetbv等指令先检测支持的指令集,再选择采用哪个版本吗?如果memcpy的数据量较大,那么检测时间或许可以忽略不计,但是数据量较小时就不能忍受了。通常,在程序的运行期间,一个函数引用对应的实现是不会改变的,因此只要解析一次函数调用即可,后续调用不必再解析。可以看出,该需求和plt很类似。
what
如果函数func有多个版本,为了避免每次调用func时都进行一遍版本选择,可以将函数func的符号类型定义为STT_GNU_IFUNC
,并且为func实现相应的函数解析器。之后在elf装载时,会进行函数解析,将选择的版本记录下来,以后每一次调用func都会自动跳转到对应的版本。这就是ifunc。
对该类函数的引用是通过R_*_IRELATIVE
重定位间接处理的,该重定位返回运行解析器的结果,即返回指向所选实现的函数指针。
接下来以一个具体的例子来说明整个过程。
第一步,编译glibc
为了更方便调试,手动编译glibc,加入调试信息
1 2 3 4 5 6
| #!/bin/bash mkdir -p build && cd build rm -rf ./* CFLAGS="-g -g3 -ggdb -gdwarf-4 -O2" CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -O2" ../configure --enable-debug --prefix=$HOME/glibc make -j$(($(nproc) - 1)) make install
|
第二步,编写/编译测试用例
以如下代码为例,跟踪一下memcmp函数的重定位过程
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <string.h> #include <stdlib.h>
int main() { char a[12] = "Hello world"; char b[12] = "Hello homes";
if (rand()%2 == 1) return memcmp(a, b, 10);
return 0; }
|
1 2
| gcc try_mem.c -o try_mem -g patchelf --set-interpreter $(HOME)/glibc/lib/ld-linux-x86-64.so.2 --set-rpath $(HOME)/glibc/lib try_mem # 设置链接器和库搜索路径为刚才编译的debug版本的glibc
|
第三步,gdb跟踪
1、先start到main函数入口
2、step到memcmp调用处
可能需要多试几次,因为rand()的结果是随机的。为什么不直接调用memcmp,而是在rand()%2时才调用memmcp呢?因为我们都知道由于plt的存在,在函数第一次被调用时才会真正执行重定位,那么为了避免编译器分析出memcmp这个函数调用一定会被执行导致在加载时直接重定位,我们就加了一个判断条件。
可以看到,调用memcmp其实是调用转到memcmp对应的plt表项。
3、进入memcmp 的plt表项
memcmp的plt表项会查看对应的got.plt表项,got.plt表项中存放的是函数的真正地址。
但是在第一次重定位之前,函数对应的got.plt表项应该是resolver函数地址的,因此第一次查询plt时,实际是执行resolver,将函数的真正地址存放到对应的.got.plt表项中后,之后再调用函数时,直接转到该地址处即可。
4、跳转到memcmp函数的resolver
???为什么直接进到memcmp的具体实现了,这说明resolver已经执行过了,什么时候执行的,resolver又是怎么实现的?
5、寻找resolver的调用
既然resolver已经执行过了,并且将函数的真实地址放在.got.plt表项中,那么我们可以watch该.got.plt表项,在第3步可以看到该.got.plt表项的地址是*0x555555557fc8
通过watch .got.plt表项,可以找到该.got.plt表项被修改的位置:sysdeps/x86_64/dl-machine.h:412
(commit:ae515ba530be76d6627740ddc33a3a63f8c7e4f9)。
再往上翻阅代码,可以发现,真正执行resolver的位置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
static inline void __attribute__((always_inline)) elf_machine_rela(struct link_map *map, struct r_scope_elem *scope[], const ElfW(Rela) *reloc, const ElfW(Sym) *sym, const struct r_found_version *version, void *const reloc_addr_arg, int skip_ifunc) { ElfW(Addr) *const reloc_addr = reloc_addr_arg; const unsigned long int r_type = ELFW(R_TYPE) (reloc->r_info);
... { struct link_map *sym_map = RESOLVE_MAP (map, scope, &sym, version, r_type); ElfW(Addr) value = SYMBOL_ADDRESS (sym_map, sym, true);
if (sym != NULL && __glibc_unlikely (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC) && __glibc_likely (sym->st_shndx != SHN_UNDEF) && __glibc_likely (!skip_ifunc)) { ... value = ((ElfW(Addr) (*) (void)) value) (); }
|
那么我们在value = ((ElfW(Addr) (*) (void)) value) ();
这一行处打上断点,就可以知道memcmp对应的resolver的具体实现了,如下所示。
进入resolver,如下所示
这里很奇怪:gdb无法显示resolver对应的源码。
不过没关系,我们仍然可以找到对应的源码:每个ifunc函数都必须实现对应的resolver,因此resolver必定在glibc源码中。这里我们已经有resolver对应的汇编了,这就好办了,objdump glibc,找到汇编对应的源码位置即可,如下所示。
可以看到,memcmp的resolver位于sysdeps/x86_64/multiarch/ifunc-memcmp.h:38
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| static inline void * IFUNC_SELECTOR (void) { const struct cpu_features *cpu_features = __get_cpu_features ();
if (X86_ISA_CPU_FEATURE_USABLE_P (cpu_features, AVX2) && X86_ISA_CPU_FEATURE_USABLE_P (cpu_features, MOVBE) && X86_ISA_CPU_FEATURE_USABLE_P (cpu_features, BMI2) && X86_ISA_CPU_FEATURES_ARCH_P (cpu_features, AVX_Fast_Unaligned_Load, )) { if (X86_ISA_CPU_FEATURE_USABLE_P (cpu_features, AVX512VL) && X86_ISA_CPU_FEATURE_USABLE_P (cpu_features, AVX512BW)) return OPTIMIZE (evex_movbe);
if (CPU_FEATURE_USABLE_P (cpu_features, RTM)) return OPTIMIZE (avx2_movbe_rtm);
if (X86_ISA_CPU_FEATURES_ARCH_P (cpu_features, Prefer_No_VZEROUPPER, !)) return OPTIMIZE (avx2_movbe); }
return OPTIMIZE (sse2); }
|
通过上述代码可以得知,memcmp的resolver是根据指令集测试的结果来选择具体的函数实现的。
how
readelf -s elf
可以查看elf的所有符号。
可以看到,strncpy和memcpy等字符串处理函数和内存处理函数的符号类型都是STT_GNU_IFUNC
。
以下是一个简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <stddef.h> #include <stdio.h>
void foo_c(unsigned *data, size_t len) { printf("%s\n", __FUNCTION__); } void foo_sse42(unsigned *data, size_t len) { printf("%s\n", __FUNCTION__); } void foo_avx2(unsigned *data, size_t len) { printf("%s\n", __FUNCTION__); }
int cpu_has_sse42(void) { return 0; }
int cpu_has_avx2(void) { return __builtin_cpu_supports("avx"); #if defined(__AVX2__) return 1; #endif return 0; }
void foo(unsigned *data, size_t len) __attribute__((ifunc ("resolve_foo")));
static void *resolve_foo(void) { if (cpu_has_avx2()) return foo_avx2; else if (cpu_has_sse42()) return foo_sse42; else return foo_c; }
int main(void) { foo(NULL, 0);
return 0; }
|
TODO
- 为什么gdb无法显示memcmp的resolver对应的源码?
- 为什么明明有plt section,_dl_start函数仍然在memcmp第一次执行前就把.got.plt装载好,即提前执行了resolver?