工作中常常遇到这样一种场景,出于调试需要,需要重载编译器或者运行库内建的一些函数,如 malloc、free 之类。
一、简单重载
最简单直接的方式就是重新定义:
1 2 3 4 | void* malloc(size_t size) { printf("my malloc"); return 0; } |
这样就会覆盖 glibc 内建的 malloc 函数。
二、符号别名
另外一种方式是使用 gcc 的 __attribute__ 关键字来为函数创建别名。
1 2 3 4 5 6 | void* malloc(size_t) __attribute__((alias("my_malloc"))); void* my_malloc(size_t size) { printf("%s\n", __func__); return 0; } |
上述代码的含义是,为 my_malloc 函数创建了一个函数别名 malloc。这样一来,gcc 内建的 malloc 也就不再可用,所有调用到 malloc 的地方都将调用 my_malloc,也就达到了覆盖的目的。
三、LD_PRELOAD
LD_PRELOAD 是动态连接器支持的一个环境变量,可以指定共享库在程序运行时首先被装载,包括 C 语言运行库 glibc。
利用这个特性,如果在某个共享库里重载 glibc 内建的函数,然后使用 LD_PRELOAD 指定让其首先被装载,这样也能达到目的。
四、wrap 函数
以上几种方式的弊端在于,如果符号是对外可见的,那么所有模块的 malloc 函数都将被覆盖;如果符号不可见的话,只有模块内部才会使用重载的版本。如果我们期望某些模块使用重载的版本,而另一些模块使用编译器或者运行库内建的版本呢?
例如可执行程序 a.out 动态链接共享库 liba.so 和 libb.so,在 liba.so 里有对 gcc 编译器内建函数 malloc 的重载,我们期望 liba.so 和 a.out 都使用重载的 malloc,而 libb.so 使用 gcc 的版本。如果 liba.so 里面的 malloc 对外可见,那么 link 过程中,libb.so 和 a.out 都将使用重载的这一份;而如果不可见的话,a.out 将会使用 gcc 的这一份。
有人可能会想到,在每个需要重载的模块内都重载一遍 malloc,然后将其符号隐藏。这样确实可以达到目的,但是显得很笨拙,也会因重复的符号定义增大程序的体积。正确的方法是使用 ld 的 wrap 功能。
1 2 3 4 5 6 7 | // malloc.c -> libmallo.so #include <stdio.h> void* __wrap_malloc(size_t size) { printf("%s\n", __func__); return NULL; } |
1 2 3 4 5 6 7 8 9 10 11 12 | // liba.c -> liba.so #include <stdio.h> #include <stdlib.h> void funcA() { void* ptr1 = malloc(sizeof(int)); if (ptr1 == NULL) { printf("%s malloc failed!\n", __func__); } else { printf("%s malloc success!\n", __func__); } } |
1 2 3 4 5 6 7 8 9 10 11 12 | // libb.c -> libb.so #include <stdio.h> #include <stdlib.h> void funcB() { void* ptr1 = malloc(sizeof(int)); if (ptr1 == NULL) { printf("%s malloc failed!\n", __func__); } else { printf("%s malloc success!\n", __func__); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // test.c -> a.out #include <stdio.h> #include <stdlib.h> extern void funcA(); extern void funcB(); int main(int argc, const char* argv[]) { funcA(); funcB(); void* ptr1 = malloc(sizeof(int)); if (ptr1 == NULL) { printf("%s malloc failed!\n", __func__); } else { printf("%s malloc success!\n", __func__); } return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | CC := gcc CFLAGS := -g libmalloc.so: malloc.c $(CC) ${CFLAGS} -shared -fPIC $< -o $@ -fno-builtin-malloc -fvisibility=default -Wl,--wrap=malloc liba.so: liba.c $(CC) ${CFLAGS} -shared -fPIC $< -o $@ -Wl,--wrap=malloc libb.so: libb.c $(CC) ${CFLAGS} -shared -fPIC $< -o $@ a.out: test.c liba.so libb.so libmalloc.so $(CC) ${CFLAGS} -pie -fPIE $< -o $@ -L./ -lb -la -lmalloc -Wl,--wrap=malloc .PHONY: all clean rebuild all: a.out clean: rm -rf *.o *.a *.so a.out core rebuild: clean all |
链接器会将 wrap 参数所指定的符号替换成重载的版本。这样可以对不同的共享库指定或者不指定 wrap 目标,来达成上述目的。
1 2 3 4 5 | $ make all gcc -g -shared -fPIC liba.c -o liba.so -Wl,--wrap=malloc gcc -g -shared -fPIC libb.c -o libb.so gcc -g -shared -fPIC malloc.c -o libmalloc.so -fno-builtin-malloc -fvisibility=default -Wl,--wrap=malloc gcc -g -pie -fPIE test.c -o a.out -L./ -lb -la -lmalloc -Wl,--wrap=malloc |
按照如上方式编译运行,结果如下:
1 2 3 4 5 6 | $ ./a.out __wrap_malloc funcA malloc failed! funcB malloc success! __wrap_malloc main malloc failed! |
liba.so 和 a.out 都使用了重载的 malloc, 而 lib.so 使用的是 gcc 内置的版本。
发表回复