浅尝插桩技术

本文以Linux下调用库函数的一个例子来演示一下插桩技术。

插桩示例代码分析

示例代码很简单:

1
2
3
4
5
.
├── app.c
└── lib
├── rd3.h
└── librd3.so
librd3.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);
#endif
在应用程序app.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
$ gcc -o app app.c -I./lib -L./lib -lrd3 -Wl,--rpath=./lib
  • -I./lib: 指定编译时,在 lib 目录下搜寻头文件。
  • -L./lib: 指定编译时,在 lib 目录下搜寻库文件。
  • -lrd3: 指定要链接的 librd3.so 共享库。通常,库文件的名称会以 lib 开头,但在 -l 后面指定库时,不需要包含 lib 前缀。在这里,它链接名为 rd3 的库。
  • -Wl,--rpath=./lib:这个选项用于设置运行时库路径。-Wl 用于将选项传递给链接器,而 --rpath=./lib 指示链接器在运行时搜索库时查找位于当前目录下的 lib 子目录。

执行程序得到输出:

1
2
hello, 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);

#endif
1
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.crd3_wrap.c一起编译:

1
$ gcc -I./ -L./lib -Wl,--rpath=./lib -o app app.c rd3_wrap.c -lrd3

头文件的搜索路径不能错:必须在当前目录下搜索rd3.h,这样的话,app.c中的#include "rd3.h" 找到的才是我们新增的那个头文件 rd3.h

所以在编译指令中,第一个选项就是 -I./,表示在当前目录下搜寻头文件。

另外,由于在rd3_wrap.c文件中,使用#include "lib/rd3.h"来包含库中的头文件,因此在编译指令中,就不需要指定到lib 目录下去查找头文件了。

编译得到可执行程序app,执行一下:

1
2
3
4
before 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.h
rd3_wrap.h是被app.c引用的,内容如下:
1
2
3
4
#ifndef _RD3_WRAP_H_
#define _RD3_WRAP_H_
extern int __wrap_rd3_func(int, int);
#endif
rd3_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
$ gcc -I./lib -L./lib -Wl,--rpath=./lib -Wl,--wrap,rd3_func -o app app.c rd3_wrap.c -lrd3

注意:这里的头文件搜索路径仍然设置为-I./lib,是因为app.cinclude了这个头文件。

得到可执行程序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.c
rd3_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
$ gcc -shared -fPIC -I./lib -o librd3_wrap.so rd3_wrap.c

-fPIC表示生成位置无关的代码。 得到包装的动态库: librd3_wrap.so

编译可执行程序,需要链接包装库 librd3_wrap.so

1
$ gcc -I./lib -L./ -o app app.c -lrd3_wrap -ldl

-ldl 用于链接到名为 libdl 的共享库,这是 Linux 系统上用于处理动态链接的库。

一些常见的使用情况包括:

  • 动态加载共享库:如果您的程序需要在运行时加载共享库,例如通过 dlopen 函数,那么您需要链接到 libdl
  • 符号查找与解析:某些情况下,您可能需要在运行时查找并解析函数或变量的地址,这时也需要使用 libdl 中的函数,比如 dlsym
  • 处理动态链接器错误:在处理共享库加载或符号查找时,可能会出现错误,您可以使用 libdl 来处理这些错误,如 dlerror 函数。

得到可执行程序app,执行,得到的结果与先前一致。