这次我们来看一个新的例子
a.c的内容如下:
extern int shared; int main() { int a = 10; swap(&a, &shared); }
b.c的内容如下:
int first_var = 2;
int shared = 1; void swap(int* a, int* b) { int tmp; tmp = *a; *a = *b; *b = tmp; }
gcc -c a.c -o a.o
gcc -c b.c -o b.o
回顾之前的内容,我们先看一下a.o中的符号表:
readelf -a a.o
Bind类型为LOCAL的,我们都不需要看了,重点看3个GLOBAL的,其中main的Ndx不是UND
而是1,是啥意思来着。是指这个symbol所在段在段表中的索引为1,也就是text段
看到swap和shared是NOTYPE,也即未知类型的全局符号。
然后main的size是56指的是函数指令所占的字节数,Value表示该符号相对于代码段的起始位置的偏移量
readelf -a b.o
b.o中的shared 符号,在data段中偏移4个字节的位置,swap的size是76
我们是用链接器ld把a.o和b.o链接起来:
ld a.o b.o -e main -o ab:
-e: 表示将main做为程序入口,默认的入口是_start
链接完成后输出ab, 我们执行了一下ab, 发现产生了段错误,crash了。
但是我用gcc a.c b.c -o ab1, 却能够正常运行,比较两个输出文件的size, ld链接的结果只有1224字节,但是gcc的输出结果却有8220字节,让人不禁再次陷入了深思。为什么会crash, 以及链接后的结果为什么相差这么大,我们继续往下分析。
ulimit -c unlimited命令,开启core dump功能,并且不限制生成core dump文件的大小
我们这个调试的问题,后面再详细说,这里继续分析一下链接的相关问题
首先看一下38 + 4C = 0x84, 不错,text段的确被合并了。
然后看一下ab中的text的VMA和LMA, 现在有了具体的值,变成了0x00010094,
VMA表示这个段在内存中的运行地址,LMA表示这个text段的加载地址。
也就是说这段代码映像会被先加载到LMA, 然后运行前还要拷贝到VMA处(如果VMA不等于LMA的话)
【上述结论结合bootloader的相关知识点学习】
那么还有一个问题,为啥text段会被分配到0x00010094,data分配到0x00020118呢?
这涉及到操作系统的进程虚拟地址空间分配的规则。
就是在ld脚本中啦,ld脚本就规定程序和数据在bin文件里面是什么存储的,以及运行时在rom和ram中是怎么存储的文件。
这里就引入了链接脚本的概念,同时也是bootloader中会涉及到的概念,也就是说就是链接脚本中指定的这个地址。
ldd --help 中有一个-T 选项,就是读取链接脚本的,也验证了我们的说法
链接器在扫描和分配空间完成之后,会根据链接脚本中指定的位置,把对应的text和data段放上去。同时因为各个符号在段内的相对位置是固定的,这样只要给每个符号加上一个 相对text段首大偏移 + 自己所在段的偏移就可以得到每一个符号的绝对地址
比如,swap的地址就是 main + 0x38, 也就是 10094h + 38h = 100cc h
在完成空间和地址的分配步骤以后,链接器就进入了符号解析与重定位的步骤。
我们先使用objdump -d a.o 看一下在编译期间,编译器对外部符号的处理
首先可以看到,main的起始地址是0, 只有等到空间分配完成后,各个函数才会确定自己在虚拟地址空间中的位置
然后是bl跳转语句。关于bl指令,我目前没有仔细研究。我们重点对比,ab中的这个地方,就知道,对应的地址被修改了。
为了辅助链接器完成这个功能,在elf中还有一个叫重定位表的东西,我们在前面提起过.rel.text 和 .rel.data的段就是重定位表,也叫
重定位段。
objdump -r a.o
这个命令可以查看a.o中所有需要重定位的地方。
首先指明下面的待重定位符号在text段中,偏移为0x20的地方需要对一个叫swap的符号进行重定位,偏移为0x34的地方需要对shared这个符号
重定位。 与反汇编的代码段对照一下就可以看的更加清晰