1,显示运行时链接的优点
使得模块组织更加灵活,可以在使用的时候再进行加载,不需要的时候进行释放。
另外,模块可以进行更新而不需要停止程序的运行。这对诸如Web服务器等需要长时间运行的程序非常重要。例如Web服务器的某个模块更新了,则仅仅需要通知Web服务器对该模块进行重新动态加载就可以了。
2,动态库与共享对象的区别
共享对象是动态链接器在程序开启的时候进行加载和链接的,这一系列的步骤都是对程序本身透明的。动态库则是通过一系列的动态链接器提供的API,具体有4个:dlopen,dlsym,dlerror,dlclose,程序可以通过这几个API对动态库进行操作。
它们的实现在/lib/libdl.so.2里边,相关声明和常量在<dlfcn.h>。
2,dlopen
void * dlopen(const char *filename ,int flag);
如果传入的文件参数是0,那么dlopen返回的是全局符号表的句柄。也就是说可以运行时找到全局符号表的任何符号,执行它们。
这有些类似高级语言(比如Java,嘿嘿还好我学过)的反射机制。全局符号表包含程序的可执行文件、动态加载的共享模块和dlopen打开的共享库并且使用了RTLD_GLOBAL方式的模块中的符号。
第二个是符号解析方式。RTLD_LAZY表示使用延迟绑定(PLT);RTLD_NOW模块加载时进行绑定,如果有绑定失败,则dlopen返回错误。上边两种必选一个。另外可以使用RTLD_GLOBAL来与上述两个“或”,表示被加载的模块的全局符号合并到进程的全局符号表,使得以后加载的模块可以使用这些符号。
dlopen返回加载的模块的句柄,在dlsym和dlclose时需要。加载失败返回NULL。如果模块已经加载,返回同一个句柄。如果依赖于其他模块,程序员需要手工加载依赖模块。
另外dlopen会执行.init段的代码以进行模块初始化。
3,dlsym()
核心部分,用以找到符号。
void *dlsym(void *handle.char *symbol);
一个是模块句柄,一个是符号名称。没找到返回NULL,否则返回符号的值。
值的意义:函数则是函数地址,变量则是变量地址,常量则是常量值。如果常量本就是NULL或者0.则需要通过dlerror判断是找到符号而符号值是NULL还是没有找到符号返回了NULL。dlerror返回NULL表示没有发生错误。
常量符号一般是编译链接器产生的,对外不可见。比如编译单元文件名。使用objdump -t查看,常量在符号表中类型是 *ABS*。
4,符号查找优先序列
对于ld加载的模块,符号如果存在冲突,则先装入的优先。dlopen装入的符号也可能与之前的模块重复。进行符号解析和重定位时,ld同样采取了装载序列。dlsym()对符号查找分为两种: 传入的文件参数是NULL的时候,此时使用的是全局符号表;dlsym是使用装载序列的;如果不是NULL,则以打开对象的符号表为根节点,进行依赖文件的广度优先遍历。
dlerror判断dlopen、dlsym和dlclose的上次调用是否成功。返回是char *,如果是NULL则无错;否则是错误信息的指针。
5,dlclose()
系统维持一个模块的加载计数,dlclose使得计数-1,如果为0,就真正被卸载。卸载先执行.finit代码,然后将符号从符号表去除,取消进程地址空间和模块的映射关系,关闭模块文件。
6,gcc中的-ldl
gcc -o RuntimeSimple RuntimeSimple.c -ldl
-ldl表示使用 DL(动态加载)库,位于/lib/libdl.so.2。
7,函数的执行
知道函数的地址,还必须知道函数签名才能执行该函数(签名中有返回值、参数的信息)。Java等中的反射机制,可以获取函数的额外信息,包括函数的参数、返回值。但是C/C++没有保存这些信息到可执行、共享库或者目标文件(当然从符号修饰中可以知道)。所以,一般开发人员必须知道函数的原型。
8,程序员自我修养288页有个程序,可以根据传入参数和符号,执行该符号的函数。
由于预先不知道要执行的函数的参数的个数和各个参数类型,所以使用了汇编,根据输入的参数顺序依次压栈,然后跳转到函数符号地址处执行。 这种方式就伪造了函数调用的堆栈,然后使用无参数的该函数的指针,调用该函数。