1.链接地址
对于链接器,一种普遍情景是由多个子程序来构建一个程序,并生成一个链接好的起始地址为0的输出程序,各个子程序通过重定位在大程序中确定位置。具体来说:利用第一遍扫描得到的数据,链接器将相似段合并,计算出各个段在输出地址空间中的大小和位置。第二遍扫描会利用第一遍扫描中收集到的信息来控制实际的链接过程。它会读取输入文件中的段的数据和重定位信息,并且进行符号解析与重定位。符号解析将为符号引用替换数字地址,重定位调整代码中的地址。
链接地址实际上就是链接器对代码中的变量(数据)、函数(指令)等符号进行一个地址编排,赋予这些抽象的符号一个地址,然后在程序中通过地址访问相应变量和函数。要知道,在ELF文件中的汇编代码(机器指令)中,标号和符号已经不复存在,一切引用都不过是地址!
链接时通过-Ttext 80700000和-Tdata 80480000指令为ld指定代码段和数据段的链接基址。运行期间,代码指令和数据变量的地址都在相对-T指定的基址的某个偏移量处。这个地址实际上就是程序在进程中的虚拟地址(VMA)。
当这个程序被加载时,系统会选择一个加载地址,而链接好的程序会作为一个整体被重定位到加载地址。
2.位置无关代码——PIC(Position Independent Code)
嵌入式bootloader上电之后之所以能够正确执行,有个很重要的原因,就是最初执行的bootstrap代码被设计成地址无关的,即这个映象文件可以放在ROM或RAM的任何一个地址上运行起来。
一条机器指令由操作码域和操作数域构成,特定CPU体系架构支持的指令集(IA)是固定的,因此操作码总是与地址无关的——无论指令放在哪里,操作码op总能被正确译码。但是访问一个操作数往往是通过地址引用的,这里的地址包括函数或变量等符号的地址,而这些符号的地址在链接时已经由-T指定。一旦LMA不等于VMA,则运行期引用VMA处的数据,将会导致错位甚至跑飞。这里超前引用了VMA/LMA概念,请瞻前顾后。
在MIPS跳转/分支指令中,j系为绝对地址跳转指令,b系为相对PC分支指令。在链接阶段,j系指令后跟随的符号(例如函数romStart)已经被替换为函数符号的链接地址,可能为0x807014d0,即“jal romStart;”=>“jal 0x807014d0”(如MIPS跳转/分支指令所分析,指令的高两位取自PC)。在运行期,如果romStart函数没有装载到0x807014d0,比如仍在ROM/Flash中,此时如果直接跳转到romStart,程序可能跑飞。在这种绝对地址跳转中,代码或数据必须加载到正确的位置,具体来说必须重定位放到链接地址才能保证数据访问的正确性。这种放置位置决定绝对寻址的正确性的代码我们称之为位置相关代码。b系指令是基于PC寻址的,在PC值基础上加减某段偏移即可得到欲跳往的目标地址。b系指令所在的代码无论是放在RAM的0x8070xxxx地址处,还是放在ROM/Flash的0xbfc0xxxx处,当执行到b系指令时,总能根据当前PC跳转到正确的相对指令位置。这种无论放在哪里都能正常运行的代码我们称之为位置无关代码。
嵌入式bootloader是烧录到ROM/Flash中的,一般在ROM/Flash中执行Stage1阶段,引导带bootstrap代码必须是位置无关的。但后续需要将位置相关的内核代码(数据)从ROM拷贝到RAM中的正确位置才能正常启动运行,这种拷贝搬移使得代码能在预定位置正确运行的过程,我们称之为重定位(Relocation)。位置相关代码重定位到正确的地址(链接-T指定的VMA)后,之后的程序才能正确运行。可参考后续《VxWorks启动之romStart剖析》。
以下摘自《SeeMIPS Run (2rd)》部分章节关于PIC的描述:
(1) Chapter 13 -GNU/Linux from Eight Miles High
==============================================================
A library binary hasto beposition-independent code or PIC—it must run correctly wherever its code and data are positioned in virtual address space.
(2) Chapter 16 – Linux Application Code, PIC,and Libraries
==============================================================
the memory image ofthe code itself must work regardless of where the code is located in virtual memory. That is, the code must be position-independent code or PIC. It‘s not uncommon to describe code as “position-independent” just because the branch and call instructions are all PC-relative:
(3) MIPS Glossary
==============================================================
PC (program counter): Shorthand for the address of the instruction currently being executed by a CPU.
PC relative: An instruction is PC relative if it uses an address that is encoded as an offsetfrom the instruction’s own location. PC-relative branches within modules are convenient, because theyneed no fixing when the entire module is shifted in memory; this is a step toward PIC(fullposition-independent code).
position-independent code(PIC):Code that can execute correctly regardless of where it ispositioned in program address space—notably, this is required byLinux/MIPS applications.See Chapter 16. A weaker form of PIC can be produced by simply making sure all references are PC relative.
3.VMA和LMA
《程序员的自我修养》4.1.2 相似段合并
VMA表示VirtualMemory Address,即虚拟地址;LMA表示Load Memory Address,即加载地址。正常情况下这两个值是一样的,但是在有些嵌入式系统中,特别是在那些程序放在ROM/Flash的系统中时,LMA和VMA是不相同的。
《GNU-ld链接脚本浅析》2 基本概念
每个“可加载的”或“可分配的”输出section通常包含两个地址:VMA(virtualmemory address,虚拟内存地址或程序地址空间地址)和LMA(load memoryaddress,加载内存地址或进程地址空间地址)。通常VMA和LMA是相同的。
在目标文件中,loadable 或allocatable的输出section有两种地址:VMA(virtualMemoryAddress)和LMA(LoadMemory Address)。VMA是执行输出文件时section所在的地址,而LMA是加载输出文件时section所在的地址。一般而言,某section的VMA==LMA。但在嵌入式系统中,经常存在加载地址和执行地址不同的情况:比如将输出文件加载到开发板的flash中(由LMA指定), 而在运行时将位于flash中的输出文件复制到SDRAM中(由VMA指定)。
可这样来理解VMA和LMA,假设:
(1).datasection对应的VMA地址是0x08050000,该section内包含了3个32位全局变量, i、j 和k,值分别为1,2,3。
(2).textsection内包含由“printf(“j=%d”,j);”程序片段产生的代码。
链接时指定.data section的VMA为0x08050000,产生的printf指令是将地址为0x08050004处的4字节内容作为一个整数打印出来。
如果.data section 的LMA为0x08050000,显然结果是j=2,符合预期。
如果.data section 的LMA为0x08050004,显然结果是j=1,此时LMA=0x08050004处存放的是第一个变量i,指令中访问VMA=0x08050004即变量i。这种情形就是我在前文提到的“错位”,都是绝对地址惹的祸!
还可这样理解LMA:
.text section内容的开始处包含如下两条指令(inteli386指令是10字节,每行对应5字节):
jmp 0x08048285;绝对地址跳转
movl $0x1,%eax
如果.text section的LMA为0x08048280,那么在进程地址空间内0x08048280处为“jmp 0x08048285”指令,0x08048285处为“movl $0x1,%eax”指令。假设某指令跳转到地址0x08048280,显然它的执行将导致%eax寄存器被赋值为1。
如果.text section的LMA为0x08048285,那么在进程地址空间内0x08048285处为”jmp 0x08048285”指令,0x0804828a处为movl $0x1,%eax指令. 假设某指令跳转到地址0x08048285,显然它的执行又跳转到进程地址空间内0x08048285处, 造成死循环。
一种更坏的情况是,0x08048285处存放的是数据段,则jmp到此地址处为非法指令(全零的nop指令是合法的),板子可能直接当掉了。这种情形就是我在前文提到的“跑飞”,都是绝对地址惹的祸!
《GNU-ld链接脚本浅析》7 SECTIONS命令
输出section描述
输出section描述具有如下格式:
SECTION[ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
…
}[>REGION] [AT>LMA_REGION] [:PHDR :PHDR …] [=FILLEXP]
输出section的LMA:默认情况下,LMA等于VMA,但可以通过关键字AT()指定LMA。
用关键字AT()指定,括号内包含表达式,表达式的值用于设置LMA。如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围。
这个特性是为了便于建立ROM映像而设计的。比如,下面的连接脚本创建了三个输出节:
一个叫做‘.text’从地址‘0x1000’处开始,一个叫‘.mdata’,尽管它的VMA是’0x2000’,它会被载入到’.text’节的后面,最后一个叫做‘.bss’是用来放置未初始化的数据的,其地址从’0x3000’处开始。
符号’_data’被定义为值’0x2000’, 它表示定位计数器的值是VMA的值,而不是LMA。
例子:
SECTIONS { .text 0x1000 : { *(.text) _etext = . ; } .mdata 0x2000 : AT ( ADDR (.text) + SIZEOF (.text) ) { _data = . ; *(.data); _edata = . ; } .bss 0x3000 : { _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;} }
这个链接脚本产生的程序使用的运行时初始化代码会包含象下面所示的一些东西,以把初始化后的数据从ROM映像中拷贝到它的运行时地址中去。注意这节代码是如何利用好连接脚本定义的符号的。
程序如下:
extern char _etext, _data, _edata, _bstart, _bend; char*src = &_etext; char*dst = &_data; /*ROM has data at end of text; copy it. */ while(dst < &_edata) { *dst++= *src++; } /*Zero bss */ for (dst= &_bstart; dst< &_bend; dst++) *dst= 0;
.text段和.data段链接地址不一定是紧凑相连的,但是AT指令导致它们紧凑存放,即ROM中的_etext后存放的是数据段。上面的程序将处于ROM内的已初始化数据拷贝到该数据应在的位置(基于RAM的VMA地址),并将为初始化数据置零。
读者应该认真的自己分析以上连接脚本和程序的作用。
4.romInit.s中的RELOC分析
梳理完上面关于VMA/LMA和PIC的相关概念后,脑海里马上浮现的是VxWorks BSP中romInit.s中RELOC宏实现的一段重定位代码。大约一年前,我开始接触VxWorks的BSP/bootloader开发,第一眼看到RELOC时,就被“bal 9f;”和“la ra 9b”中的标号9深深地迷惑住。由于知识结构不完善,对编译链接了解甚少,相当长一段时间内,都没有弄明白RELOC的要义。直到最近得闲,在系统学习了编译链接的相关知识、整理相关笔记时,这些散落在脑海里的珍珠才终于串联起来了!
下面将从这个小巧的代码片段(code snippet)入手,实例分析VMA/LMA和PIC的实际运作体现。由于当前手头没有实测环境,我无从准确地复现从静态VMA到动态LMA过程中的一些流程细节。所幸的是《关于vxWorks的bootcode中的一段位置无关代码–RELOC》中给出了ICE单步执行序列,这足够用来分析本节议题。
4.1 源代码
以下MIPS32汇编代码出自VxWorks BSP中的romInit.s。
#define RELOC(toreg,address) \ bal 9f; \ 9:; \ la toreg, address; \ addu toreg, ra; \ la ra, 9b; \ subu toreg, ra ------------------------- RELOC(t0, romStart) /*这两句重新折算回RAM,似乎多余?*/ and t0,~0xFFF00000 or t0, ROM_TEXT_ADRS jal t0 #never returns - starts up kernel nop
4.2 ELF静态视图
假设我们在ld vxWorks_romCompress –T link.ROM时指定-Ttext 80700000(ROM_TEXT_ADRS),通过“objdump–d”得到链接生成的ELF文件的静态视图(Disassemblyof section .text)大体如下:
80700000<romInit>: ...... 807013c4: bal 807013cc 807013c8: nop 807013cc: lui $t0,0x8070 807013d0: addiu $t0,$t0,5328 807013d4: addu $t0,$t0,$ra 807013d8: lui $ra,0x8070 807013dc: addiu $ra,$ra,5068 807013e0: subu $t0,$t0,$ra 807013e4: lui $at,0xf 807013e8: ori $at,$at,0xffff 807013ec: and $t0,$t0,$at 807013f0: lui $at,0x8070 807013f4: or $t0,$t0,$at 807013f8: jalr $t0 807013fc: nop ...... 807014d0<romStart>:
其中第一句“80700000<romInit>:”即表明符号romInit被链接到地址0x80700000处;“807014d0<romStart>:”表明符号romStart被链接到地址0x807014d0处。
在预编译阶段,宏RELOC展开时,toreg被t0替换,address被符号romStart替换,宏ROM_TEXT_ADRS则被数值 0x80700000替换。“la toreg, addr”和“la ra, 9b”的宏扩展可参考《MIPS体系结构透视》9.4寻址模式,这里属于第三类“lw$2, addr”。
在链接重定位时,符号romStart被替换成连接地址0x807014d0,“la ra, 9b”中的标号9b(b表示back)被替换成 链接地址0x807013cc(参考下一节的宏展开)。参考《MIPS体系结构透视》8.2.3指令的详细清单可知“bal 9f;”宏扩展为“bgezal $zero, offs”,参考《MIPS体系结构透视》8.6.3编码方式和处理器的具体实现可知其中的offs即bgezal指令编码后16位broffset,其被修正为符号9f(f表示forward)与当前指令(PC)的相对位移,具体来说broffset=0x807013cc(标号9的链接地址)-0x807013c4(bal指令对应的PC)=8。
4.3 ROM/Flash运行期动态视图
在嵌入式芯片中,ROM/RAM往往统一编址(可参考具体芯片datasheet中关于memory map的章节),跳转到ROM和跳转到RAM没有任何区别,romStart()中的copyLongs()即执行ROM到RAM的拷贝。
在 MIPS中,ROM/Flash被编址映射到0xBFC00000(kseg1),这个地址也是重启入口向量。VxWorks ELF文件被ICE整体加载时,LMA为ROM/Flash的零地址(0xBFC00000),开始运行该程序时,PC指向这里,从0xBFC00000 处开始取指执行第一条指令。
以下为ICE仿真器单步执行RELOC序列:
/*bal 9f;*/ bfc013c4: bal 0xbfc013cc # ra=PC+8=0xbfc013cc,为标号9的flash地址 bfc013c8: nop # 延迟槽 /*la toreg, address;*/ bfc013cc: lui t0,0x8070 # t0= 0x80700000 bfc013d0: addiu t0,t0,5328 # t0= 0x807014d0,romStart符号的链接地址 /*addu toreg, ra;*/ bfc013d4: addu t0,t0,ra # t0= romStart链接地址 + ra(标号9的flash地址) /*la ra, 9b;*/ bfc013d8: lui ra,0x8070 # ra= 0x80700000 bfc013dc: addiu ra,ra,5068 # ra= 0x807013cc(标号9的链接地址) /*subu toreg, ra*/ bfc013e0: subu t0,t0,ra # t0= romStart链接地址 + 标号9的flash地址 - 标号9的链接地址 # t0= 0xbfc014d0(romStart在flash中的地址) /*and t0, ~0xFFF00000*/ bfc013e4: lui at,0xf # at= 0x000f0000 bfc013e8: ori at,at,0xffff # at= 0x000fffff bfc013ec: and t0,t0,at # t0= 0x000014d0 /*or t0, ROM_TEXT_ADRS*/ bfc013f0: lui at,0x8070 # at= 0x80700000 bfc013f4: or t0,t0,at # t0= 0x807014d0 /*jal t0*/ bfc013f8: jalr t0 # t0= 0x807014d0 bfc013fc: nop
左边第一列体现了加载域与运行域的变化,不变的是其中ELF链接时已经替换掉的一些绝对地址,变的是其中一些相对地址的适应调整。对比ELF静态视图和ICE动态视图中的bal指令,我们可以领悟一下基于PC relative的PIC实现要义。
对于bootloader型(bootrom.bin),bootstrap阶段romStart仍然驻留在ROM/Flash中执行,如果直接“jal romStart;”(“jal 0x807014d0”),无疑跑飞。RELOC(to, romStart)计算出romStart在ROM/Flash中的地址并将其保存到t0,然后“jal t0;”实现正确调用——“jal 0xbfc014d0;”。注意此类映像中RELOC之后无需and/or运算再次重定位。此时,在romStart中调用copyLongs函数也需要用ROM_OFFSET重定位。很多资料中说RELOC、ROM_OFFSET重定位实现了PIC,其实是通过一定的手段保证了它们在ROM/Flash中的正确调用,但与PIC概念本身倒不那么吻合。
对于vxWorks_romCompress类型,为执行速度考虑,romInit.s可能已经将包含romStart的非压缩bootstrap代码拷贝到RAM中,因此可以直接“jal romStart;”从ROM/Flash跳转到RAM。这种情形似乎无需RELOC到ROM再and/or折算回RAM。但是在romStart中还是需要继续调用copyLongs将剩余驻留在ROM/Flash中的部分拷贝重定位到RAM中,才能保证后续正常执行。
参考:
《链接器和加载器》 第1章 链接和加载
《程序员的自我修养——链接、装载与库》第4章 静态链接
《关于vxWorks的bootcode中的一段位置无关代码–RELOC》
《bootloader与linux中位置无关代码(PIC)的分析理解》
《U-Boot中关于TEXT_BASE,代码重定位,链接地址相关说明》
《ARM Architecture C 语言寻址解析—— 从U-Boot relocation所展开的探索(一)》
《ARM Architecture C 语言寻址解析—— 从U-Boot relocation所展开的探索(二)》