转自:
有时候需要知道一个函数是被哪个函数调用的。比如,一个函数被成千上百个文件的函数调用,加入其中一个调用不对导致除了问题的话,要找出是那个地方调用的话,一个笨方法是找到每个调用的地方,加上打印信息,但这显然是不现实的。此外,有些调用的地方可能是以库的形式存在的,这样的话,就没有办法通过加打印信息找出来了。
一种较好的方法是,重新写一个同样接口的函数,里面打印出调用者函数的名字(甚至是 backtrace)让系统运行的时候,在调用原来函数的地方,自动调用我们重新写的那个函数。我们可以使用环境变量 LD_PRELOAD 来达到这个目的。做法是:先把我们自己写的函数编成一个共享库,然后在系统运行的时候,让 LD_PRELOAD指向这共享库。
man ld-linux 可以查到 这个环境变量的详细信息。简言之,它指向的共享库会被最优先装载进来
下面我们以函数 memcpy()为例说明。
我们重写的函数在文件 backtrace.c里面,如下:
01 | #define _GNU_SOURCE |
02 | #include <dlfcn.h> |
03 | #include <stdio.h> |
04 | #include <stdlib.h> |
05 |
06 | /* ... */ |
07 | static void * handle; |
08 | static void * (*mymemcpy)( void *, const void *, size_t ); |
09 |
10 | __attribute__ ((constructor)) void Initialize( void ) |
11 | { |
12 | char * error; |
13 | handle = dlopen( "/lib/i386-linux-gnu/libc-2.15.so" , RTLD_LAZY); |
14 | if (!handle) { |
15 | fprintf (stderr, "%s\n" , dlerror()); |
16 | exit (EXIT_FAILURE); |
17 | } |
18 | dlerror(); |
19 |
20 | *( void **)(&mymemcpy) = dlsym(handle, "memcpy" ); |
21 | if ((error = dlerror()) != NULL) { |
22 | fprintf (stderr, "%s\n" , error); |
23 | exit (EXIT_FAILURE); |
24 | } |
25 | } |
26 |
27 | __attribute__ ((destructor)) void Finalize( void ) |
28 | { |
29 | if (handle) |
30 | { |
31 | dlclose(handle); |
32 | } |
33 | } |
34 |
35 | void * memcpy ( void * dest, const void *src, size_t size) |
36 | { |
37 |
38 | if (mymemcpy) |
39 | { |
40 | (*mymemcpy)(dest, src, size); |
41 | } |
42 | /* .... */ |
43 | #if 1//DEBUG == 1 |
44 | // { |
45 | Dl_info dli; |
46 | /* this only works in a shared object context */ |
47 | dladdr(__builtin_return_address(0), &dli); |
48 | fprintf (stderr, "debug trace [%d]: %s " |
49 | "called by %p [ %s(%p) %s(%p) ].\n" , |
50 | getpid(), __func__, |
51 | __builtin_return_address(0), |
52 | strrchr (dli.dli_fname, '/' ) ? |
53 | strrchr (dli.dli_fname, '/' )+1 : dli.dli_fname, |
54 | dli.dli_fbase, dli.dli_sname, dli.dli_saddr); |
55 | dladdr(__builtin_return_address(1), &dli); |
56 | fprintf (stderr, "debug trace [%d]: %*s " |
57 | "called by %p [ %s(%p) %s(%p) ].\n" , |
58 | getpid(), strlen (__func__), "..." , |
59 | __builtin_return_address(1), |
60 | strrchr (dli.dli_fname, '/' ) ? |
61 | strrchr (dli.dli_fname, '/' )+1 : dli.dli_fname, |
62 | dli.dli_fbase, dli.dli_sname, dli.dli_saddr); |
63 | // } |
64 | #endif |
65 | /* .... */ |
66 | } |
链接地址
测试代吗如下(test5.c)
1 | int main( void ) |
2 | { |
3 | char arr[5]; |
4 | memcpy (arr, "haha" , 4); |
5 | printf ( "arr = %s\n" , arr); |
6 | return 0; |
7 | } |
用如下命令编译:
1 | gcc -fpic -shared -g backtrace.c -o libstrace.so -ldl |
1 | gcc -g test5.c -o test5 |
执行如下:
1 | LD_PRELOAD=./libstrace.so ./test5 |
2 | arr = haha |
所加的打印信息没有,看来重新写的那个函数没有被调用到。
是不是 memcpy函数根本就没有调用到呢?(比如,被编译器优化掉了)
下面看一下汇编语言,里面有没有对这个函数的调用:
01 | objdump -d -S test5 | grep -A10 memcpy |
02 | memcpy(arr, "haha", 4); |
03 | 8048449: 8d 44 24 17 lea 0x17(%esp),%eax |
04 | 804844d: c7 00 68 61 68 61 movl $0x61686168,(%eax) |
05 | printf("arr = %s\n", arr); |
06 | 8048453: 8d 44 24 17 lea 0x17(%esp),%eax |
07 | 8048457: 89 44 24 04 mov %eax,0x4(%esp) |
08 | 804845b: c7 04 24 50 85 04 08 movl $0x8048550,(%esp) |
09 | 8048462: e8 d9 fe ff ff call 8048340 <printf@plt> |
10 | return 0; |
11 | 8048467: b8 00 00 00 00 mov $0x0,%eax |
确实没有!
原因是 GCC出于效率上考虑,使用了内建的内存拷贝函数。
可以加上选项不用内建的函数:
1 | gcc -g -fno-builtin-memcpy test5.c -o test5 |
然后重新执行:
1 | $ LD_PRELOAD=./libstrace.so ./test5 |
2 | debug trace [8004]: memcpy called by 0x8048495 [ test5(0x8048000) (null)((nil)) ]. |
3 | debug trace [8004]: ... called by 0xb757f4d3 [ libc.so.6(0xb7566000) __libc_start_main(0xb757f3e0) ]. |
4 | arr = haha |
现在总算调到了。
但是,调用着函数名字还是没有打印出来。。
重新编译一下, 加上一个选项:
1 | gcc -g -export-dynamic -fno-builtin-memcpy test5.c -o test5 |
上面新加的选项还可以是-rdynamic
然后重新之执行:
1 | $ LD_PRELOAD=./libstrace.so ./test5debug trace [8103]: memcpy called by 0x8048625 [ test5(0x8048000) main(0x80485f4) ]. |
2 | debug trace [8103]: ... called by 0xb754b4d3 [ libc.so.6(0xb7532000) __libc_start_main(0xb754b3e0) ]. |
3 | arr = haha |
现在基本上大功告成了.
更进一步,还可以打印出整个调用链的 backstrace
man backtrace 给出了一个例子。这里就不重复了。