clang15下getauxval与ifunc配合触发段错误的修复及Workaround咨询
我最初写了一段代码,通过use_embed变量在标准memcpy和自定义my_memcpy之间切换:
#include <stdio.h> #include <string.h> static int use_embed = 1; void *my_memcpy (void *dst, const void *src, size_t len) { printf("aaaaa\n"); return dst; } static void * (*resolve_memcpy (void))(void *, const void *, size_t) { if(use_embed == 1) { return memcpy; } else { return my_memcpy; } } void *memcpy2 (void *, const void *, size_t) __attribute__ ((ifunc ("resolve_memcpy"))); #define memcpy memcpy2 // extern void *memcpy (void *, const void *, size_t); int main() { int source = 5; int dest = 6; memcpy(&dest, &source, sizeof(source)); printf("%d %d\n", use_embed, dest); return 0; }
之后我想通过getauxval检测系统特性来选择函数版本,写出了下面的代码:
#include <stdio.h> #include <string.h> #include <sys/auxv.h> #ifndef HWCAP2_MTE #define HWCAP2_MTE (1 << 18) #endif #ifndef HWCAP_SVE #define HWCAP_SVE (1 << 22) #endif #ifndef AT_HWCAP2 #define AT_HWCAP2 26 #endif #ifndef AT_HWCAP #define AT_HWCAP 16 #endif /// 检查当前环境是否支持MTE static int mte_supported(void) { return (getauxval(AT_HWCAP2) & HWCAP2_MTE); } /// 检查当前环境是否支持SVE static int sve_supported(void) { return (getauxval(AT_HWCAP) & HWCAP_SVE); } void *my_memcpy (void *dst, const void *src, size_t len) { printf("aaaaa\n"); return dst; } static void * (*resolve_memcpy (void))(void *, const void *, size_t) { if(mte_supported() != 0) { return my_memcpy; } if(sve_supported() != 0) { return my_memcpy; } return my_memcpy; } void *memcpy2 (void *, const void *, size_t) __attribute__ ((ifunc ("resolve_memcpy"))); #define memcpy memcpy2 // extern void *memcpy (void *, const void *, size_t); int main() { int source = 5; int dest = 6; memcpy2(&dest, &source, sizeof(source)); printf("%d\n", dest); return 0; }
用clang15编译这段代码时触发了段错误,原因和PLT相关:getauxval@plt段的第一个跳转既不指向_dl_runtime_resolve,也不指向getauxval的读访问点。
我的问题:
- 如何修复这段代码?ifunc机制应该预见到这类场景,肯定有对应的解决方案,但我还不清楚。
- 如果ifunc没有解决方案,PLT层面有没有可行的workaround?比如
LD_BIND_NOW这类方法。
补充信息
clang --version clang version 15.0.7 (https://github.com/llvm/llvm-project 8dfdcc7b7bf66834a761bd8de445840ef68e4d1a) Target: x86_64-unknown-linux-gnu Thread model: posix cat /proc/version Linux version 5.14.0-162.6.1.el9_1.x86_64 (mockbuild@dal1-prod-builder001.bld.equ.rockylinux.org) (gcc (GCC) 11.3.1 20220421 (Red Hat 11.3.1-2), GNU ld version 2.35.2-24.el9) #1 SMP PREEMPT_DYNAMIC Fri Nov 18 02:06:38 UTC 2022
问题1:修复代码的正确方式
段错误的核心原因是:ifunc的 resolver 函数在程序启动早期执行,此时动态链接器还没完成PLT的延迟绑定初始化,直接调用getauxval这类需要PLT跳转的函数会触发非法访问。
GNU ifunc机制明确要求:resolver函数必须是**无副作用、仅依赖静态信息或直接读取ELF辅助向量(不通过libc包装函数)**的代码。所以不能直接调用getauxval,而是要自己直接读取辅助向量。
修改方法:绕过libc的getauxval包装,直接从进程的辅助向量链表中读取AT_HWCAP和AT_HWCAP2的值。代码如下:
#include <stdio.h> #include <string.h> #include <stdint.h> #ifndef HWCAP2_MTE #define HWCAP2_MTE (1 << 18) #endif #ifndef HWCAP_SVE #define HWCAP_SVE (1 << 22) #endif #ifndef AT_HWCAP2 #define AT_HWCAP2 26 #endif #ifndef AT_HWCAP #define AT_HWCAP 16 #endif // 直接读取ELF辅助向量,避免调用libc的getauxval static uint64_t get_auxval(unsigned long type) { extern char **environ; unsigned long *auxv = (unsigned long *)environ; // 跳过环境变量,找到辅助向量起始位置 while (*auxv++ != 0); // 遍历辅助向量 while (1) { unsigned long tag = *auxv++; unsigned long val = *auxv++; if (tag == 0) break; // AT_NULL结束 if (tag == type) return val; } return 0; } /// 检查当前环境是否支持MTE static int mte_supported(void) { return (get_auxval(AT_HWCAP2) & HWCAP2_MTE); } /// 检查当前环境是否支持SVE static int sve_supported(void) { return (get_auxval(AT_HWCAP) & HWCAP_SVE); } void *my_memcpy (void *dst, const void *src, size_t len) { printf("aaaaa\n"); return dst; } static void * (*resolve_memcpy (void))(void *, const void *, size_t) { if(mte_supported() != 0) { return my_memcpy; } if(sve_supported() != 0) { return my_memcpy; } return memcpy; // 修正原代码的错误,最后返回标准memcpy } void *memcpy2 (void *, const void *, size_t) __attribute__ ((ifunc ("resolve_memcpy"))); #define memcpy memcpy2 int main() { int source = 5; int dest = 6; memcpy(&dest, &source, sizeof(source)); printf("%d\n", dest); return 0; }
关键改动点:
- 实现自定义的
get_auxval,直接遍历辅助向量,不依赖libc的PLT绑定 - 修正resolver函数的返回逻辑,原代码错误地全返回
my_memcpy,最后应回退到标准memcpy
编译命令:clang -o ifunc_memcpy ifunc_memcpy.c,运行即可正常工作。
问题2:PLT层面的workaround
如果确实无法修改resolver代码,有两种临时workaround:
启用立即绑定:运行程序时设置
LD_BIND_NOW=1,强制动态链接器在程序启动前完成所有PLT绑定,这样resolver调用getauxval时PLT已经初始化完成:LD_BIND_NOW=1 ./your_program静态编译:使用
-static参数编译,直接把libc的代码链接到程序中,避免PLT延迟绑定:clang -static -o ifunc_memcpy_static ifunc_memcpy.c注意:静态编译会导致程序体积变大,且某些系统特性可能受影响。
内容的提问来源于stack exchange,提问作者calvin

