目标文件的三种形式;
linux中采用可执行可连接格式的文件ELF表示目标文件,下图为可重定位的目标文件的条目表示:
.text:已编译程序的机器代码
.rodata:只读数据
.data:已初始化的全局和静态变量
.bss:未初始化的全局和静态变量
.symtab:一个符号表,存放程序中定义和引用的函数和全局变量的信息
.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置
.rel.data:被模块引用或定义的所有全局变量的重定位信息。
.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及C程序源文件,编译时使用 -g选项才能获得这个表
.line:C源程序中的行号和.text节 中机器指令之间的映射,只有以-g选项调用编译器驱动程序时才能得到这张表
.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以 null 结尾的字符串序列。
每个可重定位目标模块m都有一个符号表,它包含模块m定义和引用的符号信息。在链接器的上下文有三种不同的符号:
注意:
static可以隐藏全局变量和函数名字,使其只对本模块或者本文件可见。
局部变量被栈管理,链接器对此类符号不感兴趣
不同模块的同名静态局部变量,编译器会向汇编器输出两个不同名字局部链接器符号-
上图为.symtab中的符号表,每个符号都会被分配到目标文件的某个节中,由section字段表示,每个节在节头部表中都有一个索引,用于表示该节。有三个伪节,在节头部表中没有条目:
ABS代表不该被重定位的符号
UNDEF代表未定义的符号,即在本目标模块中引用,但在其他地方定义的符号
COMMON表示还未被分配位置的未初始化的数据目标
只有可重定位的目标文件才存在伪节,可执行的目标文件没有。其中COMMON和.bss的区别:
COMMON 未初始化的全局变量
.bss 未初始化的静态变量,初始化为0的全局或静态变量
编译器翻译到某个模块时,遇到一个弱全局符号,不知道其他模块是否也定义了该符号,所以编译器把该符号分配至COMMON,把符号的解释权留给链接器。静态符号的构造是唯一的,所以可以分配至 .data 或 .bss
链接器解析符号引用的方法是将引用与他输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对于全局变量,如歌符号定义不明确,则会报错。重载函数例外,因为C++会将参数信息添加至符号中。
编译时,编译器向汇编器输出每个全局符号(强符号和弱符号),汇编器隐含地编码至可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
根据强弱符号定义,Linux链接器处理多重定义的符号名的规则如下:
可以使用 -fno-common 编译选项,编译时,遇到多重定义符号,则触发一个错误
未定义的符号集合U,已定义的符号集合D,重定位的符号集合E:
静态库的放置顺序至关重要,如果库之间不存在依赖,那么放置在结尾即可(链接器从左只有扫描输入文件),如果存在依赖,被依赖的库放在最后,例如 foo.c 调用了 liby.a 和 libz.a, 这两个库又调用了 libx.a 则:
gcc foo.c liby.a libz.a libx.a
如果 foo.c 调用了 libx.a, libx.a 调用了 liby.a,而 liby.a 又调用了 libx.a ,这种情况比较极端,:
gcc foo.c libx.a liby.a libx.a
链接器完成符号解析之后,进入重定位步骤,在这个步骤将合并模块,为每个符号分配运行时地址:
当汇报器生成一个目标模块时,数据和代码最终在内存中位置是未知的,对于最终位置未知的目标引用,它会生成一个重定位条目,告诉链接器在合并成可执行文件时修改这个引用。代码的可重定位条目在 .rel.text 中,已初始化数据的重定位条目在 .rel.data 中。
offset:被修改的引用的节偏移
symbol:被修改的引用指向的符号
type:如何修改新的引用
addend:符号常数,一些类型重定位需要使用它对被修改引用的值做偏移调整
基本的重定位类型:
这两种类型都支持小型代码模型,大于2GB的程序可以用-mcmodel=medium / large 标志来编译。
可执行文件可以被加载到内存,下图为一个程序头部表,其中起始地址 vaddr 满足 v a d d r % a l i g n = o f f % a l i g n vaddr \% align = off \% align vaddr%align=off%align
linux x86-64系统中,代码段从地址0x400000处开始,在程序头部表引导下,加载器将可执行文件的片复制到代码段和数据段,接下来加载器跳转到程序的入口点(_start函数的地址,这个函数在系统目标文件ctrl.o中定义),_start函数调用启动函数__libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并在需要时把控制返回给内核。
注意:地址空间布局随机化(ASLR)可以帮助克服某些类型的缓冲区溢出攻击,ASLR可以将基数,库,堆和堆栈放在进程地址空间中的任意随机位置,这使攻击程序很难预测下一条指令的内存地址。
静态库更新时需要重新编译,动态库的出现解决了这一问题,在程序运行时可以被动态链接,该过程由动态链接器执行。
可执行目标文件包含一个.interp节,其包含了动态链接器的路径名,动态链接器本身就是一个共享目标(Linux系统中的ld-linux.so)。加载器会加载和运行这个动态链接器,然后动态链接器执行重定位:
此时共享库的位置就固定了。Linux提供了 dlopen 函数运行程序在运行时加载和链接共享库
flag 包括:
RTLD_GLOBAL:库中的解析的定义变量在随后的其它的链接库中变得可以使用。
RTLD_NOW:链接器立即解析对外部符号的引用
RTLD_LAZY:链接器推迟符号解析知道执行来自库中的代码
dlsym输入参数:已经打开的共享库的句柄和一个symbol的名字,如果符号存在就返回地址,否则返回null
返回一个字符串,表示以上函数最近发生的错误,没有错误就返回null
无论在内存何处加载一个目标模块(包括共享目标模块),其数据段和代码段的距离总是保持不变,因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。为了实现-fpic生成与位置无关的代码,在数据段开始的地方创建一个全局偏移量表(GOT),在GOT中,每个被这个目标模块引用的全局数据目标都有一个 8 字节的条目。编译器还为GOT中的每个条目生成一个重定位记录。加载时,动态链接器会重定位GOT中的每个条目。
GNU通过延迟绑定完成PIC函数的调用,将过程地址的绑定推迟到第一次调用时,第一次的调用过程的运行开销很大,但是其后的每次调用都只会花费一条指令和一个间接的内存引用。延迟绑定需要:GOT和过程链接表(PLT),如果一个目标模块调用定义在共享库中的函数,那么它就由自己的GOT和PLT,GOT是数据段的一部分,PLT是代码段的一部分。
过程:
对于链接后的代码,程序会先跳到PLT[2],后面通过GOT[4]间接跳转将控制转移到addvec
库打桩:它允许截获对共享库函数的调用,取而代之执行自己的代码。该机制可以追踪某个库函数的调用次数,验证和追踪他的输入和输出值,甚至替换成一个完全不同的实现
基本思想:给定一个目标函数,创建一个包装函数,他的原型和目标函数完全一样,使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数。
打桩可以发生在编译时、链接时、程序加载和运行时。
gcc -DCOMPILETIME -c mymalloc.c
gcc -I. -o intc int.c mymalloc.o
本地的malloc.h使用预处理器将包装函数替换掉了目标函数,加入-I. 参数预处理器会先搜索当前目录下的malloc.h头文件,打桩完成
Linux静态链接器支持使用 –wrp f 标志进行链接时打桩,即对符号 f 的引用解析成 __wrap_f,还有把对符号__real_f 的引用解析成 f。
gcc -DLINKTIME -c mymalloc.c
gcc -c int.c
gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o
-Wl,option标志把option传递给链接器,option中的每个逗号替换成一个空格。
如果能访问可执行目标文件就可以进行运行时打桩,该机制基于动态链接器的LD_PRELOAD环境变量实现。如果该变量设置成一个共享库的路径名的列表(以空格或者逗号分隔),那么加载执行一个程序时,需要解析未定义的符号引用,会先访问LD_PRELOAD中的库。该机制甚至可以对libc.so进行打桩。
dlsym的参数 RTLD_NEXT 可以在对函数实现所在动态库名称未知的情况下完成对库函数的替代,上述源代码中的 malloc 函数中的 fput 会调用 malloc 这样就陷入了递归循环,导致程序运行失败,需要做出以下修改:
static int hooked = 0;
void *(*mallocp)(size_t size);
void *malloc(size_t size){
if (hooked && mallocp) {
return mallocp(size);
}
hooked = 1;
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc");
if((error = dlerror()) != NULL){
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size);
printf("malloc(%d)=%p\n", (int)size, ptr);
hooked = 0;
return ptr;
}
使用以下命令编译执行即可。
gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
gcc -o intr int.c
LD_PRELOAD="./mymalloc.so" ./intr
AR:创建静态库,插入、删除、列出和提取成员
STRINGS:列出一个目标文件可以打印的字符串
STRIP:从目标中删除符号表信息
NM:列出一个目标文件的符号表中定义的符号
SIZE:列出目标文件中节的名字和大小
READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息,包含SIZE和NM的功能
OBJDUMP:反汇编
LDD:列出一个可执行文件在运行时所需的共享库