我们通常编写的文本程序是由ASCII字符组成,但是一个可执行程序是由二进制数字组成,从ASCII——>二进制文件,经历了
- 预处理:进行头文件和宏定义的替换
- 编译:由编译器把高级语言代码编译为汇编代码
- 汇编:由汇编器把汇编代码翻译成二进制代码,也即是.o文件
- 连接:由连接器把多个.o文件连接成可执行文件;可分为编译时链接,加载时链接(程序被加载到内存中执行时),运行时链接(由应用程序来执行时)。
静态链接
静态链接器(gcc执行时)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的,可以加载运行的可执行目标文件作为输出。
静态库:把目标文件中的一个打包为单独一个文件,称为静态库。当构造一个输出可执行程序时,他只复制静态库中被应用程序引用的目标木块。在链接时,应用程序只复制被应用程序引用的目标模块(一个链接库中有多个函数,只需复制被引用的即可),减少了可执行文件在磁盘和内存中的大小。
静态库的缺点:
- 如果静态库有更新,必须了解到他的最新版本然后显示的将程序与更新的库连接。
- 他们的代码会被复制到进程的文本段中,大量的进程在运行的话会造成内存的浪费
连接器的任务:目标文件是字节块的集合。它包含了代码与数据,还有其他连接器加载器的数据结构,连接器是把这些块连接起来,确定被连接块运行时的位置,修改代码和数据块中的位置。总的来说:
- 符号解析:变量或函数。为了让每个符号引用和一个符号定义关联。
- 重定位:通过把符号定义与一个内存地址关联起来,重定位这些节,修改这些符号的引用,使他们指向内存的位置。
符号和符号表
每个可重定位模块都有一个符号表(它由汇编器构造,使用编译器输出到汇编语言.s文件中的符号),包含
- 由该模块定义并能被其他模块一用的全局符号(非static函数和全局变量)
- 由其他模块定义并被该模块引用的全局符号(对应于其他模块的非static函数和全局变量)
- 由该模块定义并且只能被该模块引用的局部符号(该模块中static函数和全局变量)
本地连接器符号和本地程序变量是不同的,.symtab中的符号不包含对应于本地非静态程序变量的任何符号,这些符号运行时在栈中被管理。
编译器向汇编器输出每个全局符号,汇编器把他们放到符号表条目中,函数和已经初始化的全局变量为强符号,未初始化的全局变量为弱符号。
- 不允许有多个同名强符号。
- 如果多个强符号和弱符号同名,选择强符号(模块A中:int x=10;模块B中:int x;x=11; 如果在一个模块里未被初始化,那么连接器将选择另一个模块中的定义,这不会产生警告,连接器感受不到多个x的定义)。
- 如果多个弱符号同名,选择任意其中一个
符号解析
当编译器遇到不在该模块定义的符号(变量或函数),假设该符号在其他模块定义,生成一个连接器符号表条目,并把它交给连接器处理,如果连接器在如果连接器在其它模块中找不到该符号的定义,会输出一个错误并终止程序。
连接器在符号解析阶段从左到右按照他们在编译器驱动程序命令行上的出现顺序来扫描可重定位目标文件(E,这个集合中的文件会被形成可执行文件)和存档文件( .c文件翻译为.o文件)。连接器维护可重定位目标文件,未解析的符号集合(U),在前面输入中已定义的符号集合(D)
- 对于命令行上的文件f,如果是目标文件,则添加到E中,修改D和U反应f中符号的定义和引用,继续下一个文件
- 如果是个存档文件,则匹配u中未解析的符号和存档文件成员定义的符号,存档文件的成员m定义了一个符号来解析U中的一个引用,则把m加到e中,修改u和d反应m中的符号定义和引用。对存档文件中所有成员目标都依次进行这个过程,直到u和d不再变化。任何不包含在E中的成员目标文件都简单的被丢弃。连接器继续处理下一个输入文件。
- 如果扫描完命令行的输入后,u为非空,则连接器输出一个错误并终止程序,否则合并重定位e中的目标文件。构建输出的可执行文件。
重定位
完成符号解析后,代码中的每个符号引用和一个符号定义(输入一个输入目标文件模块的符号条目表)关联起来,此时连接器就知道目标模块代码和数据确切大小。然后可以开始重定位,为每个符号分配运行时地址。
- 重定位节和符号定义。将所有类型的节(比如所有目标模块中.data)合并为一个新聚合节。然后连接器将运行时的地址赋给新的聚合节,赋给输入模块的每个节,赋给输入模块的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时内存地址。
- 重定义节中的符号引用。连接器修改代码节和数据节中的每个符号引用,使他们指向正确的运行时地址。依赖于重定义目标模块中重定位条目的数据结构
重定位条目
当汇编器生成一个目标模块时,他不知道数据和代码最终放在内存的什么位置,他也不知道模块引用的外部定义或函数的全局变量的位置。所以无论何时汇编器遇到对最终位置未知的目标引用时,他就会生成一个重定义条目。告诉连接器生成执行文件时如何修改这个条目。重定位代码条目放在.rel.text中,以初始化的数据放在.rel.data中。
目标文件
有三种形式:
- 可重定位目标文件:包含二进制代码和数据,可以在编译时与其他可重定位文件合并共建一个可执行文件。(由编译器和汇编器生成)
- 可执行目标文件:包含二进制代码和数据,可直接加载到内存执行。(由连接器生成)
- 共享目标文件:特殊的可重定位文件,可以在加载时或运行时被动态加载到内存并连接。(由编译器和汇编器生成)
目标模块:一个字节序列;目标文件:以文件形式存放在磁盘上的目标模块。
可重定位目标文件
ELF头包含了16字节开始的序列,剩下的是EFL头大小,目标文件类型,机器类型,节头部表的文件偏移,节头目表中的条目大小和数量。
可执行目标文件
成语一开始是ASCII文本文件,此时被转化为二进制文件,而且这个二进制文件包含加载程序到内存并运行它所需要的信息。
ELF头标书文件的整体格式还包含程序的入口点(程序需要运行时执行的第一条指令的地址)。可执行文件的连续片(chunk)被映射到连续的内存段。
加载可执行目标文件
当在shell中输入./programName时,shell解析到/判断不是内置命令(如果是内置命令时会搜索/usr /usr/lib ...)而是一个可执行文件,调用常驻内存的加载器(通过execve调用加载器)的操作系统代码来调用他。将可执行程序的代码和数据从磁盘复制到内存,在程序头部表的引导下加载器将可执行文件的片(chunk)复制到代码段和数据段,跳转到程序的第一条指令或入口点来运行。
linux的每个程序都运行在一个进程的上下文中,有自己的虚拟地址空间。当一个shell运行时,父进程shell生成一个子进程,他是父进程的一个复制。子进程通过execve系统调用调用加载器,加载器删除现有的虚拟内存段,创建新的代码段数据段堆栈,新堆栈被初始化为0,通过将虚拟地址空间的页映射到可执行文件的页面大小chunk,新的代码段和数据段被初始化为可执行文件的内容,最后跳转到_start,最终调用程序的main函数,除了头部的一些信息,加载过程没有任何数据从磁盘复制到内存,知道CPU引用的第一个虚拟页时才被复制。利用页面调度算法将他从磁盘复制到内存。
共享目标文件
共享库是个目标模块,在运行加载时,可以被加载到任意内存地址,并和一个在内存中的程序连接起来。这成为动态链接由动态链接器的程序来执行。
-fpic指示编译器生成与位置无关代码,-shared指示连接器创建一个共享目标文件。
当创建可执行文件时,静态执行一些连接,然后在程序加载时,完成动态链接过程,此时动态库中没有任何代码和数据被复制到可执行文件中,连接器复制了一些重定位和符号表信息,其中的.interp节包含动态链接器的路径名,然后执行下列完成重定位:
- 重定位libc.so的文本段和数据段到某个内存段
- 重定位libvector.so的文本段和数据段到某个内存段
- 重定位执行程序中对动态库的符号定义和引用
最后将控制权给程序,此后共享库的位置不再变。
注意:
调用动态库的时候有几个问题会经常碰到,有时,明明已经将库的头文件所在目录 通过 “-I” include进来了,库所在文件通过 “-L”参数引导,并指定了“-l”的库名,但通过ldd命令察看时,就是死活找不到你指定链接的so文件,这时你要作的就是通过修改 LD_LIBRARY_PATH或者/etc/ld.so.conf文件来指定动态库的目录。通常这样做就可以解决库无法链接的问题了。
在linux下可以用export命令来设置这个值,在linux终端下输入:
LD_LIBRARY_PATH=newdirs:$LD_LIBRARY_PATH.(newdirs是新的路径串)
然后再输入:export ,即会显示是否设置正确,export方式在重启后失效,所以也可以用 vim /etc/bashrc ,修改其中的LD_LIBRARY_PATH变量。
从引用程序中加载和链接共享库
#include<dlfcn.h> /*下面是打开一个动态连接库句柄指针*/ void *dlopen(const char * filename,int flag); /*下面是返回动态连接库中的函数地址*/ void dlsym(void *handle,char *symbol); /*下面是关闭打开的动态连接库*/ int dlclose(void *handle); /*下面是用来检测是否连接成功*/ const char *dlerror(void);
与位置无关代码
把代码加载到内存的任何位置而无需连接器修改。加载而无需重定位的代码称为与位置无关代码,这样无数进程可以共享代码段的单一副本。
PIC数据引用
无论在内存何处加载一个目标模块(包含共享目标模块),数据段和代码段的距离总保持不变,因此代码段中的任何指令和数据段中的任何变量运行时都是一个常量。
在数据段开始的地方创建一个GOT(全局偏移量表),在此表中,每个被这个模块引用的全局数据目标都有一个8字节条目,每个条目还有个重定位记录。连接器会重定位GOT中的每个条目,使它包含正确的目标地址。
PIC函数调用
延迟绑定是避免一个库中成百上千个函数,在连接时需要大量重定位不需要的函数,把地址的绑定延迟到第一次调用该过程时。
库打桩机制
给一个需要打桩的目标函数创建个包裹函数,它的原型与目标函数一样,使用某种打桩机制欺骗系统调用某种包裹函数而不是目标函数。
编译时打桩
正是有-I.所以会进行打桩,告诉预处理器在搜索系统目录之前先在当前目录中malloc.h查找。
链接时打桩
链接时打桩通过在链接时传递标志 -wl, --wrap f
给链接器,告诉链接器把符号 f
和 __real_f
解析为 __wrap_f
,实现替换。同样,实现替换的函数
#ifdef LINKTIME #include<stdio.h> #include<malloc.h> //std malloc //试了直接调用malloc,编译链接ok,但是运行时core void *__real_malloc(size_t size); void __real_free(void *ptr); void *__wrap_malloc(size_t size) { void *ptr = __real_malloc(size); printf("[debug] malloc size %d ", (int)size); return ptr; } void __wrap_free(void *ptr) { __real_free(ptr); printf("[debug] free %p ", ptr); } #endif
编译代码,-Wl,option标志把option传递给链接器,其中的每个都好替换为空格。
gcc -DLINKTIME -c mymalloc.c gcc -Wl,--wrap,malloc -Wl,--wrap,free -o out main.c mymalloc.o
运行时打桩
以上两种需要有源文件的情况下实现,而对于运行时打桩,只需要可以访问执行文件,利用动态链接器的LD_PRELOAD
环境变量实现。
当加载程序时,解析未定义的引用时,动态链接器会先搜索LD_PRELOAD
指定的库,然后才搜索其他,因此,通过把自己实现的动态库设置到这个环境变量,动态链接器加载时搜索的该库内有对应实现的函数,就会直接使用该函数而不会再搜索其他系统库。
实现自己的动态库,包含需要替代的函数
#ifdef RUNTIME #define _GNU_SOURCE #include<stdio.h> #include<stdlib.h> #include<dlfcn.h> void *malloc(size_t size) { void *(*mallocp)(size_t size); char *error; // 查找标准库的实现 mallocp = dlsym(RTLD_NEXT, "malloc"); if ((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } void *ptr = mallocp(size); printf("[debug] malloc size %d ", (int)size); return ptr; } void free(void *ptr) { void (*freep)(void *ptr); char *error; freep = dlsym(RTLD_NEXT, "free"); if ((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } freep(ptr); printf("[debug] free %p ", ptr); } #endif
编译代码
all:out out: main.c mymalloc.o gcc -o out main.c ## 编译共享库 mymalloc.o: mymalloc.c gcc -DRUNTIME --share -fpic -o mymalloc.so mymalloc.c -ldl .PHONY : clean run run: # 指定运行时加载的库 #setenv LD_PRELOAD "./mymalloc.SO"; ./out; unsetenv LD_PRELOAD ## 设定环境 export LD_PRELOAD="./mymalloc.so"; ./out; unset LD_PRELOAD ## 其他任何的可执行程序都可以打桩 export LD_PRELOAD="./mymalloc.so"; uptime; unset LD_PRELOAD clean: @rm -rf out *.so