从U-Boot relocation所展开的探索(二)
ARMArchitecture C语言PIC寻址方式解析
承前文所述,可不可以产生一种可以运行在任意地址段的代码呢?可以,这种代码被称之为Position-IndependentCode,简称 PIC(windows DLL,Linux ShareObject,这两者就是典型的PIC文件)。那么如何产生PIC呢?可以通过为编译器指定编译选项产生,比如:
arm-none-eabi-gcc -c -o -fpicmain.o main.c
又比如:
arm-none-eabi-gcc -c -o -fpiemain.o main.c
这样编译产生的目标文件包含了PIC所需要的信息, -fpic,-fpie是gcc的PIC编译选项。ld也有PIC连接选项 -pie,要获得一个完整的PIC可运行文件,连接目标文件时必须为ld指定-pie选项,比如:
arm-none-eabi-ld -Tarm_pic.lds main.o -o arm_pic -pie
PIC可运行文件的一个最重要特点就是——这种文件里包含一个Global OffsetTable,简称GOT。每一个GOT Entry记录了一个对象的地址(对象可以是全局变量或函数),CPU从GOT中读取GOTEntry从而获得全局变量的地址。
下面讨论指定了 -fpic编译选项和 -pie连接选项所产生的代码是如何寻址的。
-
arm_pic.bin(二进制镜像文件)
-
arm_pic.dump(反汇编文件)
-
arm_pic.map(Memory Map文件)
从arm_pic.dump可见,出现了.got数据段,这一份GOT包含6个Entry,基地址为0x402001d0,6个Entry标示出6个全局变量的地址。
接下来通过main函数分析PIC寻址,main函数反汇编代码如下图所示:
与上一篇文章分析的汇编代码不同,现在Lable里面存放的已经不是地址值,而是偏移量(offset)。global_var1的寻址经过如下3个步骤(global_str的寻址方式也是如此):
-
r3通过Lable1取得GOT Base相对pc的offset,从而确定GOT Base的地址;
-
r2通过Lable2取得global_var1 GTO Entry相对于GOT Base的offset;
-
r2 + r3累加(Base + offset)得到global_var1 GTOEntry的地址,从而取得global_var1的地址;
每一个变量的地址最终都是从GOT中获得,这就是PIC的寻址方式,也是它的核心,得到GOT的基地址,就能修改变量的地址从而对变量进行relocation(重定位)。
OK,跟上一篇文章所讨论一样,我们把arm_pic整体copy到0x80000000,同时,将GOT中每一个entry的内容累加上偏移量 0x3fe00000,这样所有C全局变量的地址都被调整到新的地址(修改变量地址的这一操作被称之为relocation),main函数和foo函数中的变量寻址不会出错了,那么程序是不是就能正常运行了呢?还不行,为什么?GOT中保存的仅仅是C的变量地址和函数地址,但不要忘了,我们的工程不仅仅有C代码,还有汇编代码!那么汇编代码的对象是如何relocation呢?我们如何得到汇编代码中需要relocation的对象信息呢?答案在.rel.dyn数据段和.dynsym数据段,这部分将在下一篇文章中分析,同时也将具体分析U-Boot2011.12如何relocation。
从U-Boot relocation所展开的探索(三)
by蔡于清
ARMArchitecture C语言PIC寻址方式解析(续)
继续讨论上一篇文章的问题,GOT所记录的是C的对象地址,但一个工程中往往还存在汇编代码定义的变量(事实上汇编语言中并不存在“变量”这一说法,这些所谓的“变量”更准确的讲是一个"Lable",C语言的指针变量名,比如main.c中定义的global_str,对汇编来说也是一个Lable,在接下来的文章里我们统一用Lable称呼这两者),如何对汇编的Lable进行relocate呢?如何得到需要relocate的Lable信息呢?命令行进入arm_pic目录,运行命令arm-none-eabi-readelf-r arm_pic,得到以下输出:
“arm-none- eabi-readelf”是一个分析elf文件的工具,使用“-r”选项可以列出所有需要relocate的Lable信息。观察上图“offset” 列的数据,配合arm_pic.dump,我想读者应该发现,0x40200020是Lable“_undefined_instruction”的地址,0x40200024是Lable"_software_interrupt"的地址(这两个Lable是在汇编文件start.S中定义的),没错,所有在汇编语言中定义的Lable,都可以在这个输出中查看到它们的地址,更甚者,C语言GOTentry的地址也都被列出。那么上面输出的这些数据保存在elf文件中的哪个section呢?——它们被保存在.rel.dynsection(.rel.dyn section由连接时为ld指定-pie选项产生),如下图所示:
.rel.dyn section由若干.rel.dyn entry组成,一个entry记录了一个Lable的信息(ELFstandard定义.rel.dyn entry 为ELF32_Rel结构体,具体内容请参考ELF standard和ELF for the ARMArchitecture),如上图所示,第一个entry记录了Lable"_undefined_instruction"的信息,其地址为0x40200020,relocatetype为R_ARM_RELATIVE。无论汇编语言定义的Lable或是C语言定义的Lable,只要得到该Lable的.rel.dynentry,就可以对其进行relocation。
如上一篇文章所讲,arm_pic被整体copy到0x80000000地址后,将GOT中每一个entry的内容累加上偏移量0x3fe00000,这样还不能让其正常地从新地址中运行(修改GOTentry只解决了C变量的relocation),完美的做法是——遍历.rel.dyn section,将每一个entry所指地址中的值加上偏移量,这样无论汇编定义的Lable,还是C定义的Lable,所有的Lable都被relocation,U-Bootrelocation使用的就是这种方法。
U-Boot 2011.12relocation分析
事实上u-boot 2011.12并没有使用到“-fpic”或“-fpie”编译选项,只用到“-pie”连接选项,指定了“-pie”连接选项所产生的elf文件包含了.rel.dynsection,U-Boot 2011.12 relocation只需借助.rel.dyn提供的信息就足够了。
上图的代码可在 /u-boot-2011-12/arch/arm/cpu/armv7/start.S中找到,这段代码用于完成U-Boot 2011.12 relocation。“fixloop”循环遍历.rel.dynsection,“fixabs”和"fixrel"分别 relocate"R_ARM_ABS32"和“R_ARM_RELATIVE”两种类型的Lable,其中,“R_ARM_ABS32”类型的 Lable在relocate的时候还需要引用到.dynsymsection的数据,而“R_ARM_RELATIVE”的处理则简单许多,只需在原地址值的基础上加上offset即可。
现在还有以下这3个问题:
-
.dynsym section包含什么数据?
-
"R_ARM_ABS32"和“R_ARM_RELATIVE”的区别是什么?
-
"R_ARM_ABS32"和“R_ARM_RELATIVE” relocate的具体细节是什么?
我想这3个问题就留给读者自己去研究吧:)
全文完