本文以Linux下调用库函数的一个例子来演示一下插桩技术。
插桩示例代码分析
示例代码很简单: 1
2
3
4
5.
├── app.c
└── lib
├── rd3.h
└── librd3.solibrd3.so
是一个动态库,其源代码librd3.c
如下所示: 1
2
3
4
5
6#include <stdio.h>
int rd3_func(int a, int b) {
printf("hello, world\n");
return a + b;
}$ gcc -shared -o librd3.so librd3.c
可以获得librd3.so
。 1
2
3
4
5
6// lib/rd3.h
#ifndef _RD3_H_
#define _RD3_H_
extern int rd3_func(int, int);
#endifapp.c
中,调用了动态库中的这个函数:
app.c
代码如下: 1
2
3
4
5
6
7
8
9
10#include <stdio.h>
#include <stdlib.h>
#include "rd3.h"
int main(int argc, char *argv[])
{
int result = rd3_func(1, 1);
printf("result = %d \n", result);
return 0;
}
1 |
|
-I./lib
: 指定编译时,在lib
目录下搜寻头文件。-L./lib
: 指定编译时,在lib
目录下搜寻库文件。-lrd3
: 指定要链接的librd3.so
共享库。通常,库文件的名称会以lib
开头,但在-l
后面指定库时,不需要包含lib
前缀。在这里,它链接名为rd3
的库。-Wl,--rpath=./lib
:这个选项用于设置运行时库路径。-Wl
用于将选项传递给链接器,而--rpath=./lib
指示链接器在运行时搜索库时查找位于当前目录下的lib
子目录。
执行程序得到输出: 1
2hello, world
result = 2
在编译阶段插桩
对函数进行插桩,基本要求是:不应该对原来的文件(app.c
)进行额外的修改。
由于app.c
文件中,已经include "rd3.h"
了,并且调用了其中的rd3_func(int, int)
函数。
所以我们需要新建一个假的 rd3.h
提供给app.c
,并且要把函数rd3_func(int, int)
重导向到一个包装函数,然后在包装函数中去调用真正的目标函数,如下图所示:
重导向函数可以使用宏来实现。
包装函数:新建一个C
文件,在这个文件中,需要 #include "lib/rd3.h"
,然后调用真正的目标文件。
完整的文件结构如下: 1
2
3
4
5
6
7.
├── app.c
├── lib
│ ├── librd3.so
│ └── rd3.h
├── rd3.h
└── rd3_wrap.c
最后两个文件是新建的:rd3.h
, rd3_wrap.c
,它们的内容如下: 1
2
3
4
5
6
7
8
9
10
11
12// rd3.h
#ifndef _LIB_WRAP_H_
#define _LIB_WRAP_H_
// 函数“重导向”,这样的话 app.c 中才能调用 wrap_rd3_func
#define rd3_func(a, b) wrap_rd3_func(a, b)
// 函数声明
extern int wrap_rd3_func(int, int);
#endif1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// rd3_wrap.c
#include <stdio.h>
#include <stdlib.h>
// 真正的目标函数
#include "lib/rd3.h"
// 包装函数,被 app.c 调用
int wrap_rd3_func(int a, int b)
{
// 在调用目标函数之前,做一些处理
printf("before call rd3_func. do something... \n");
// 调用目标函数
int c = rd3_func(a, b);
// 在调用目标函数之后,做一些处理
printf("after call rd3_func. do something... \n");
return c;
}app.c
和 rd3_wrap.c
一起编译:
1 |
|
头文件的搜索路径不能错:必须在当前目录下搜索rd3.h
,这样的话,app.c
中的#include "rd3.h"
找到的才是我们新增的那个头文件 rd3.h
。
所以在编译指令中,第一个选项就是 -I./
,表示在当前目录下搜寻头文件。
另外,由于在rd3_wrap.c
文件中,使用#include "lib/rd3.h"
来包含库中的头文件,因此在编译指令中,就不需要指定到lib
目录下去查找头文件了。
编译得到可执行程序app
,执行一下: 1
2
3
4before call rd3_func. do something...
hello, world
after call rd3_func. do something...
result = 2
在链接阶段插桩
GNU 的链接器功能是非常强大的,它提供了一个选项:--wrap f
,可以在链接阶段进行插桩。
这个选项的作用是:告诉链接器,遇到f
符号时解析成__wrap_f
,在遇到__real_f
符号时解析成f
,正好是一对!
我们就可以利用这个属性,新建一个文件rd3_wrap.c
,并且定义一个函数__wrap_rd3_func(int, int)
,在这个函数中去调用__real_rd3_func
函数。
只要在编译选项中加上-Wl,--wrap,rd3_func
, 编译器就会:
- 把
app.c
中的rd3_func
符号,解析成__wrap_rd3_func
,从而调用包装函数; - 把
rd3_wrap.c
中的__real_rd3_func
符号,解析成rd3_func
,从而调用真正的函数。
这几个符号的转换,是由链接器自动完成的!
按照这个思路,一起来测试一下。
文件目录结构如下: 1
2
3
4
5
6
7.
├── app.c
├── lib
│ ├── librd3.so
│ └── rd3.h
├── rd3_wrap.c
└── rd3_wrap.hrd3_wrap.h
是被app.c
引用的,内容如下: 1
2
3
4#ifndef _RD3_WRAP_H_
#define _RD3_WRAP_H_
extern int __wrap_rd3_func(int, int);
#endifrd3_wrap.c
的内容如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#include <stdio.h>
#include <stdlib.h>
#include "rd3_wrap.h"
// 这里不能直接饮用 lib/rd3.h 中的函数了,而要由链接器来完成解析。
extern int __real_rd3_func(int, int);
// 包装函数
int __wrap_rd3_func(int a, int b)
{
// 在调用目标函数之前,做一些处理
printf("before call rd3_func. do something... \n");
// 调用目标函数,链接器会解析成 rd3_func。
int c = __real_rd3_func(a, b);
// 在调用目标函数之后,做一些处理
printf("after call rd3_func. do something... \n");
return c;
}rd3_wrap.c
中,不能直接去 include "rd3.h"
,因为lib/rd3.h
中的函数声明是int rd3_func(int, int);
,没有__real
前缀。
编译一下:
1 |
|
注意:这里的头文件搜索路径仍然设置为-I./lib
,是因为app.c
中include
了这个头文件。
得到可执行程序app
,执行,得到的结果与先前一致。
在执行阶段插桩
在编译阶段插桩,新建的文件rd3_wrap.c
是与app.c
一起编译的,其中的包装函数名是wrap_rd3_func
。
app.c
中通过一个宏定义实现函数的"重导向":rd3_func -> wrap_rd3_func
。
我们还可以直接"霸王硬上弓":在新建的文件rd3_wrap.c
中,直接定义rd3_func
函数。
然后在这个函数中通过dlopen, dlsym
系列函数来动态的打开真正的动态库,查找其中的目标文件,然后调用真正的目标函数。
当然了,这样的话在编译app.c
时,就不能连接lib/librd3.so
文件了。
文件目录结构如下: 1
2
3
4
5
6.
├── app.c
├── lib
│ ├── librd3.so
│ └── rd3.h
└── rd3_wrap.crd3_wrap.c
文件的内容如下: 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#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
// 库的头文件
#include "rd3.h"
// 与目标函数签名一致的函数类型
typedef int (*pFunc)(int, int);
int rd3_func(int a, int b)
{
printf("before call rd3_func. do something... \n");
//打开动态链接库
void *handle = dlopen("./lib/librd3.so", RTLD_NOW);
// 查找库中的目标函数
pFunc pf = dlsym(handle, "rd3_func");
// 调用目标函数
int c = pf(a, b);
// 关闭动态库句柄
dlclose(handle);
printf("after call rd3_func. do something... \n");
return c;
}
1 |
|
-fPIC
表示生成位置无关的代码。 得到包装的动态库: librd3_wrap.so
。
编译可执行程序,需要链接包装库 librd3_wrap.so
:
1 |
|
-ldl
用于链接到名为 libdl
的共享库,这是 Linux 系统上用于处理动态链接的库。
一些常见的使用情况包括:
- 动态加载共享库:如果您的程序需要在运行时加载共享库,例如通过
dlopen
函数,那么您需要链接到libdl
。 - 符号查找与解析:某些情况下,您可能需要在运行时查找并解析函数或变量的地址,这时也需要使用
libdl
中的函数,比如dlsym
。 - 处理动态链接器错误:在处理共享库加载或符号查找时,可能会出现错误,您可以使用
libdl
来处理这些错误,如dlerror
函数。
得到可执行程序app
,执行,得到的结果与先前一致。