最近为了理解elf格式规范中的各种重定位类型,晕了。跑出去玩了几天,终于为每种重定位类型,找到了对应的case。elf规范总共定义了10种重定位类型,之所以需要这么多种不同类型的重定位信息,是由于如下原因:
① 硬件对变量和函数的寻址方式不同,寻找变量要求绝对地址,寻找函数要求相对地址;
② 不同场合下,程序员对最终可执行文件或动态库的期望不一样(位置无关、动态库函数重定位延迟),从而加了不同的编译选项(比如-fPIC、-Ox等);
③ C语言的static、extern特性,导致不同特性的变量或函数地址可以被确定的时机不同;
④ 内核加载可执行文件,约定从固定地址0x80480000开始,但加载.so的起始地址无法约定(一个可执行程序只有一个main(),但可能依赖多个动态库)。
疑问:那整个系统中,可执行程序也不只一个呀,都约定从相同的起始地址加载,不会冲突吗?
因为每个进程访问的都是虚拟地址,由内核在背后负责将不同进程的相同虚拟地址,映射到不同的实际物理地址(属于内核范畴,不理解没关系,不影响对本贴关键内容的理解)。
静态链接/动态链接简单理解
.c文件中的代码最终被执行,需要经历如下过程:
① 编译:词法解析 → 语法解析 → 静态链接
② 加载:加载可执行文件 → 可执行文件启动或执行时,加载依赖的.so文件 → 动态链接
本帖仅关注静态链接、动态链接过程,静态链接与动态链接区别:
① 静态链接处于将1个或多个.o文件“拼凑”成可执行文件阶段,处理对象是文件,文件中的代码区没有只读属性,链接过程中可以直接修改;动态链接处于可执行文件或.so文件已被加载到内存阶段,处理对象是内存,内核为代码区所在的内存区域设置了只读属性,如果代码区有内容需要重定位,需要在编译或静态链接时,事先准备一个间接位置(加载到内存不会被设置只读属性),动态链接是对该间接位置进行重定位。
② 通过下图可以看出,静态链接将.o的各个节“撕开”,属性相同的节“拼凑”为可执行文件的段;动态链接是将“整个”.so文件安排在与可执行文件镜像相独立的位置(图中最简化了.o、.so、可执行文件的内容,用于说明静态链接与动态链接的区别,它们的内容远远不止.data、.text)。
另外,.so文件还涉及到位置无关(-fPIC)、延迟加载的选择(应该是跟优化级别有关),接下来即将详细总结。
主要利用两个技巧:
① 在程序编写阶段,虽然不知道以下两条指令真正执行后ebx寄存会得到什么值,但能确定它的含义是当时eip寄存器的值,那么跟这条指令相对位置固定的运行时地址,在逻辑上都能在编译阶段“获知”:
call L1
L1: pop ebx
② 那么,在.so文件中相对于指令区域确定位置生成一个.got表,.so被执行时.got表的绝对地址也是可以“获知”的。这样,就可以用.got表项的绝对地址,覆盖原本在指令区域的重定位处,而.got表中存放将来才能确定的最终重定位的符号地址。
① 假设进程A先将libc.so映射到自己的一块虚拟空间,当首次访问这块区间时发生缺页异常,分配物理页面并读入内容,然后建立映射。接着,进程B也将libc.so映射到自己的一块虚拟空间,首次访问这块区间仍然会发生缺页异常,但与其建立映射的物理页面,就不用再重新分配读入了。从而,物理内存只需要一份.so的内容,就可以供A、B两个进程使用。
② 思维敏锐的可能会发现一个问题:.so文件中如果有全局变量,被多个进程共享,不是会相互干扰吗?
COW(写时复制):内核为虚拟页面、物理页面都设置了一些属性,比如如果对某个虚拟页面进行写操作,就重新分配一个物理页面,复制内容并重新建立映射(为.so数据区分配的页面,就具有这样的属性)。
③ 各个进程将.so文件映射到自己的虚拟空间,数据区、代码区的相对位置,仍然保持和刚链接过后一致,所以在代码区向.got的重定位计算仍然有效,只不过动态链接器为不同进程向.got表初始化全局变量的地址时,要向.got表进行写操作,导致每个进程有一个.got副本。
6、b处两条指令执行后,ecx寄存器会得到.got表加载地址,为什么?
① 前面已经说明过R_386_PC32重定位类型,7处经过这种类型重定位后,执行时会跳转到__x86.get_pc_thunk.cx,得到b处指令的加载地址(CPU没有提供直接获取当前ip的指令,所以利用call会将返回地址压栈的特点);
② R_386_GOTPC,提示链接器创建.got表,并修改d处的值,保证执行时用它加ecx寄存器可以得到.got表地址(可以通过R_386_GLOB_DAT类型分析过程,编译得到的.so验证):
通过①可能确定,执行过6处指令后ecx得到的b处指令的加载地址,拿什么和它相加可以得到.got表位置呢?
+A:从ecx所指位置往后推2字节(机器码“81 c1”),就到了被重定位处(重定位项中的offset/规范文档中的P);
+G-P:再向后推.got表相对此处的距离,就到.got表了。 注意:$0x2只是作为链接器计算重定位值的A,在执行时就被G-P-2覆盖了,不要疑惑为什么要从ecx减2,它的含义根本就不是减数。
① 532、537处(对应.o文件中6、b处)指令,确实可以将.got表位置计算到ecx寄存器中(不过是结束位置,后面指令取.got表项地址时,用的是负偏移,可能不同编译器不一样吧,用开始位置、结束位置计算,道理是一样的);
② g1、g2的重定位类型变成R_386_GLOB_DAT,它是用于告诉动态链接器,在确定g1、g2地址时,放到它们的.got表项里(0x1fe8、0x1ff4)。