对于dlopen、dlsym,dlclose等这一些列的函数,可能大家几乎很少能用到,可能有的朋友见都没有见过,我也是在别的项目(sylar的hook模块)上遇到的,刚看到的时候,一头雾水,这是什么玩意儿?经过漫长的岁月,我终于了解一点了。
初识sylar的hook函数
sylar的hook了很多函数,像read, write, sleep, socket, connect等很多函数,感兴趣的,可以取看一下源码的实现。
什么是hook了?我的理解就是在原有的函数上在包装一层,且包装的这一层,和以前是一样的使用方法。比如linux下的read()函数,函数原型是这样的:
//标准库的
ssize_t read(int fd, void *buf, size_t count);
现在,我自己实现了一个read()函数,是这样的:
//自定义的
ssize_t read(int my_fd, void *buf, size_t count);
在我自己实现的read函数里面去调用标准库的read函数!这样当我在代码里面使用read函数,就像使用标准库的read函数,一点区别没有(函数名,参数,返回值都一样)。大概就像下面这样:
//自定义的实现
ssize_t read(int my_fd, void *buf, size_t count) {
//偷偷摸摸干点其他事
return read(my_fd, buf, count); //这里假装是标准库的read函数
}
你会发现,当我在代码里面实际使用的时候,根本没有差异,还是调用read函数,多舒服。
ps : 但是上面的代码直接编译,运行的话,最终就会无限套娃,直到爆栈,原因是return 的时候,调用的read函数,还是我们自己定义的read函数。
dlsym登场
为了更简单的说明,如下:
//add.h
int add(int, int);
//add.cpp
#include <iostream>
#include "add.h"
int add(int a, int b) {
std::cout << ".so add func" << std::endl;
return a + b;
}
将源文件编译成 动态链接库 :gcc -fPIC -shared add.cpp -o libadd.so
这样就得到了一个libadd.so的库,回到hook操作。
//test.cpp
#include <iostream>
#include "add.h"
int add(int a, int b) {
std::cout << "my add func" << std::endl;
return add(a, b);
}
int main() {
add(1, 2);
}
按照我们的想法,我自定义的add函数内部应该要去调用libadd.so里面的add函数,直接编译(gcc -g test.cpp -o test -L. -ladd),运行(./test)的话,就会一直输出 my add func。这肯定和我们所想要的是不一致的。使用gdb调试,发现它会一直自己调用自己。
使用ldd test命令你会发现,test他都不需要这个依赖项。怎么办?使用dlsym函数:
//test.cpp
#include <iostream>
#include <dlfcn.h>
#include "add.h"
int(*add_f)(int, int); //函数指针
int add(int a, int b) {
std::cout << "my add func" << std::endl;
return add_f(a, b);
}
int main() {
void* handle = dlopen("./libadd.so", RTLD_LAZY);
if(handle) {
add_f = int(*)(int, int)dlsym(handler, "add");
if(add_f) {
std::cout << "find to add func" << std::endl;
} else {
std::cout << "symbol err" << std::endl;
dlclose(handle);
return 0;
}
}
//调用add函数;
add(1, 2);
........
}
分析一下上面的,先声明了一个函数指针,返回值int, 两个参数都是int 。然后在main函数中,通过dlopen函数将libadd.so加载进来,返回一个void*的句柄。
然后就是最重要的一点,通过dlsym函数找到add函数运行时所在地址。如果找到了,add_f就会是这个函数的地址。就这样,找不到的话,可能就是第二个参数不对,第二个参数是链接符号,我这里写成"add",是不准确的,甚至不对的。在我的电脑上准确说是"_Z3addii",因为是cpp文件,如果是c文件,那就是"add";
最后调用add(1, 2),就会输出my add func 和.so add func 。这样,hook就算实现完成了,调用add函数,就会去调用libadd.so里面的add函数。类似的,调用read函数,就会去调用标准库的read函数。
其实这中间还有一下动态链接器的链接过程没说,后面在补充。
链接器的哪些事
有下面这样的一种情况,有3的动态库, libadd1.so、libadd2.so、libadd3.so 这3个库都都有一个一模一样的add函数,就是函数体不一样。并且在编译链接的时候,我都给链接进去了,像这样(gcc test.cpp -o test -ladd2 -ladd1 -ladd3)你觉得应该使用谁的add函数了,答案是:谁先 -l 则使用谁的。我先-ladd2,则使用libadd2.so的add函数。
假如我的test.cpp里面也有一模一样的add函数了,直接公布答案吧,使用test.cpp里面的add。大家可以直接实验,至少我的是这样的。
所以链接器先在本地源文件里面找有没有这个符号,有则使用,没有则取动态库里面找,库找的顺序是按照编译时 -l 指定的顺序找的。
自己真笨
发现写着写着,自己都不知道这么写了,写了一坨屎,越写越懵。
关于dlsym等一些列函数,还有很多使用的方法和诀窍,我笨,我不知道这么写了。
后续如果有时间,写一篇正式的文章吧,不想写这样的白话文了。
秘密武器
查看动态库里面有哪些链接的符号
1.nm -D 库名
2.objdump -T 库
3.readelf -s 库
查看程序运行时动态库加载的顺序
1.LD_DEBUG=libs 程序名
查看程序的动态库依赖项
1.ldd 程序名