引言
在《VxWorks引导启动过程》和《VxWorks启动之romStart剖析》中都留下了这样一个疑问,那就是bootrom中compressedEntry()和vxWorks_rom(Compress)中usrEntry()这两个桩函数到底是用来干什么的呢?由于桩函数封装的代码只有寥寥数行,我们很容易分析透彻。除了对usrInit()的简单封装外,核心就是对gp寄存器的赋值操作,也即完成gp的重定位。
到底什么是桩函数,它有什么作用?MIPS中的gp寄存器有什么特殊用途,为什么需要重定位?下文将围绕这两个问题展开。
桩函数(Stub)
MIPS中的gp寄存器类似在《嵌入式系统Boot Loader 技术内幕》中提到的弹簧床(trampoline)的概念,桩函数是一小段特殊的封装代码,是对入口函数的一层外部包裹(external wrapper)。
桩函数对于嵌入式螺旋式开发具有很重要的意义,这个界面的作用小结有三:
(1)向入口函数传递参数;
(2)处理入口函数返回情况,进行警告、错误或异常处理;
(3)宏控不同的目标系统和体系架构的特化处理,以实现代码的跨平台。
1.加载和存储:寻址方式
MIPS32中一条32位的指令是无法直接寻址32位的内存地址,加载(lw,load word)和存储(sw,store word)的机器指令数据域offset只支持16位编码。实际上,MIPS硬件只支持一种寻址方式,那就是“基址寄存器+16位有符号偏移量”。任何加载(存储)指令都类似如下格式:“lw $1,offset($2)”。可以使用任何寄存器作为目的操作数和源操作数,偏移量offset是一个有符号的16位数(-32768~32767),以上指令的效果为$1=$2+offset。
这样就带来一个问题,当我们意欲加载一个32位的常数或地址时,就得使用两条或多条原子机器指令组合完成。这将是一件非常沉闷枯燥的事情,所幸汇编器提供了合成指令。当我们使用li指令加载一个32位的立即数时,汇编宏处理器会自动识别立即数类型,并把li拆成两条或多条组合指令。这中宏汇编其实并不神奇,类似C语言中的宏块,内含逻辑判断测试指令和操作指令。通过这种合成指令,汇编器提供了简单的直接寻址方式和更多复杂的寻址方式,但均需分解扩展成原子指令序列完成。
下面来分析一下典型的立即数加载和内存变量访问两种情形:
(1)立即加载指令li(load immediate data)
在汇编语言或机器语言中,指令内部的常数称为立即数。
li是一条宏汇编合成指令,负责将32为任意整数加载到寄存器,汇编器会根据整数类型选用最高效的分解扩展方式,主要有四种:
<1>32位立即数在正负32K之间,则使用addiu和$0:
li $3, -5 => addiu $3, $0, -5
<2>如果高16位为0,可用ori和$0:
li, $4, 0x8000 => ori $4, $0, 0x8000
<3>如果低16位为0,可用lui:
li $5, 0x120000 => lui $5, 0x12
<4>上述情况都不符合,则使用lui/ori组合指令:
li $6, 0x12345 => lui $6, 0x1; ori $6, $6, 0x2345
(2)地址加载指令la(load address)
la也是一条宏汇编合成指令,提供了和li类似的加载地址操作。汇编器会根据addr的不同写法,选用最高效的基于原子机器指令lw的分解扩展方式,常用的扩展有以下三种:
<1>la $2, 4($3) => addiu $2, $3, 4
<2>la $2, addr => lui at, %hi(addr); addiu $2, at, %lo(addr)
例如“la t0, romStart”,加载内存变量romStart的值,该变量(函数)地址将在链接时计算得出。
<3>la $2, addr($3) => lui at, %hi(addr); addiu $2, at, %lo(addr); addu $2, $2, $3
(3)存储指令sw
可参考lw,查阅相关资料。
2.过度使用全局静态变量带来的效率问题
根据以上对寻址方式的分析可知,常规的加载指令“lw $2, addr”需要分解成两条指令“lui at, %hi(addr); lw $2, %lo(addr)(at)”来完成。如果程序中大量使用全局或静态数据,将会导致编译后的代码膨胀,且运行效率极低。
3.gp相对寻址GOT技术
早期的MIPS编译器提出了一种改进方法,并推广到了几乎所有的MIPS工具链。这就是gp相对寻址。这项技术需要编译器、汇编器、链接器和启动代码多方配合,将一些小变量(sdata,small data)和常量放入统一的内存区域,然后将寄存器gp($28)指向该域的中心。这个域就是全局偏移量表(Global Offset Table,GOT)。MIPS中的gp寄存器作为GOT的指针用来维护这张表。
4.gp相对寻址范围
为了创建gp相关(gp-relative)的引用,编译器在编译时必须知道一个数据最终将被链接到64KB内存地址的范围内。但实际上,编译器在编译时并不知道,只能进行猜测。一般做法是在gp范围内放置一点较小的全局变量(8字节或更小),具体大小将决定到底哪些变量可以通过gp访问。这个限制是由编译器/汇编器的“-G n”选项来控制的,“-G 0”表示不使用小数据段优化,默认是“-G 8”,表示小于或等于8 byte的数据将放入小变量区(.sdata),以便实现gp相对寻址高效快速访问。
5._gp的定义
特殊符号_gp是在链接脚本中定义的,将指向链接时期建立的GOT表中心,隶属于.data数据段,显然_gp也是基于RAM的链接地址。以VxWorks/MIPS为例,_gp是在$(TGT_DIR)/h/tool/gnu/ldscripts/link.ROM(link.RAM)的.data section的定义中界定的:
.data : AT(etext) /*link.RAM中没有AT指定*/ { wrs_kernel_data_start = .; _wrs_kernel_data_start = .; *(.data) *(.data.*) *(.gnu.linkonce.d*) SORT(CONSTRUCTORS) *(.data1) *(.eh_frame) *(.gcc_except_table) /*此处省略若干行KEEP指令*/ _gp = ALIGN(16) + 0x7ff0; /* set gp for MIPS startup code */ /* got*, dynamic, sdata*, lit[48], and sbss should follow _gp */ *(.got.plt) *(.got) *(.dynamic) *(.got2) *(.sdata) *(.sdata.*) *(.lit8) *(.lit4) . = ALIGN(16); }
可以通过MIPS工具链中的readelfmips或objdumpmips工具导出ELF文件中的符号表(Symbol table '.symtab'),符号_gp的Ndx(Elf_Sym::st_shndx)应该为.data section在Section Header Table中的索引,其符号值Value(Elf_Sym::st_value)应该是基于RAM的链接地址。
关于GOT(.got)、GOT/PLT(.got.plt)以及.dynamic等段表结构可参考《程序员的自我修养——链接、装载与库》中第7章<动态链接>。
6.gp相对寻址
从link.ROM可知,“follow _sp”的.sdata、.lit8、.lit4都将使用$gp寻址。
在使用任何加载或存储前,都应将_gp加载到gp寄存器中,例如在VxWorks BSP的romMipsInit.s中的指令“la gp,_gp”即完成这种加载。
在存取位于gp位置上下32KB范围内的数据时,只需要一条以gp作为基址寄存器的指令即可完成:
lw $2, addr => lw $2, addr-_gp(at)
使用gp相对寻址技术将大大缩小目标代码规模,有效提高执行效率。
7.gp重定位
《MIPS体系结构透视》5.9.3<启动一个应用程序>中说到“如果你的MIPS程序想使用全局指针寄存器gp加速对栈外变量的访问,你就需要做更多的初始化工作。”16.2<全局偏移表(GOT)组织>中提到“GOT指针必须在每个函数的开始时计算并加载到gp中。”在VxWorks中,将不同阶段的入口函数简单封装成桩函数,使用宏控针对MIPS体系对gp做必要的调整,以便后续能正确访问GOT。
桩函数对gp的重定位
在《VxWorks启动之romStart剖析》曾经分析过不同类型的映像各部分多次链接的数据段链接地址是不同的,因此从bootstrap到bootloader再到vxWorks,每进入一个新的执行阶段,可能就需要同步校正一次gp指针。
以下按照不同的映像类型结合执行阶段的切换来简单梳理一下VxWorks/MIPS中桩函数对gp的重定位。
1.bootrom中的桩函数compressedEntry()进行gp重定位
bootrom=bootstap program + compressedbootloader,ld bootloader(bootrom.Z.s)和ld bootrom两次指定的链接地址都不一样。因此,bootrom中的bootstap和bootloader中的gp不一样。
<1>bootstap program在romMipsInit.s中指定gp:
--------------------------------------------------------------
la gp, _gp # set globalptr from compiler
--------------------------------------------------------------
当然,这个时候_gp尚未重定位,指针_gp虽有值,但其所指蛮荒。bootstrap program本身需要访问的全局变量(inflate()/deflate()中涉及到的)直到romStart()中执行相关拷贝重定位后才能通过gp正确索引。
这里和《程序员的自我修养——链接、装载与库》中7.6.1<动态链接器的自举>所述情形相似:“自举代码首先会找到它自己的GOT”、“在GOT/PLT没有被重定位之前,自举代码不可以使用任何全局变量,也不可以调用函数”。
<2>解压缩后,在调用usrInit()进入bootloader之前,需要在bootConfig.c的桩函数compressedEntry()中重定位gp:
--------------------------------------------------------------
#if (CPU_FAMILY==MIPS)
WRS_ASM (".extern _gp;la $gp,_gp");
#endif
--------------------------------------------------------------
2.vxWorks_rom(Compress)中的桩函数usrEntry()进行gp重定位
vxWorks_rom(Compress)=bootstrap program +vxWorks(Compress),ld vxWorks和ldvxWorks_rom(Compress)两次指定的链接地址也不一样。因此,vxWorks_rom(Compress)中的bootstap和vxWorks中的gp不一样。
<1>bootstap program在romMipsInit.s中指定gp:
--------------------------------------------------------------
la gp, _gp # set globalptr from compiler
--------------------------------------------------------------
<2>在调用usrInit()进入VxWorks内核之前,需要在usrEntry.c的桩函数usrEntry()中重定位gp:
--------------------------------------------------------------
#if (CPU_FAMILY==MIPS)&& __GNUC__
__asm volatile (".extern _gp,0; la$gp,_gp");
#endif
--------------------------------------------------------------
3.loadable vxWorks在sysALib.s中进行gp重定位
loadable VxWorks无法自举,需配合bootloader使用。但ld bootrom、ld bootloader和ld vxWorks的链接地址都不一样,所以bootrom(_uncmp/_res)将VxWorks bootload到RAM_LOW_ADRS之后,在调用sysInit()->usrInit()启动内核之前,需要在sysAlib.s中再一次对gp进行重定位:
--------------------------------------------------------------
la gp,_gp /* set global ptrfrom cmplr */
--------------------------------------------------------------
4.驻留型无需桩函数进行gp重定位
bootrom_res和vxWorks_romResident这两类映像的.text代码段驻留在ROM/Flash中运行,一次链接完成 ,其中.data数据段(包含GOT)链接到RAM中。
bootstap program在romMipsInit.s中指定gp:
--------------------------------------------------------------
la gp, _gp # set globalptr from compiler
--------------------------------------------------------------
实际上这两类映像无需压缩,bootstrap program中本身没有涉及全局变量,所以当romStart()中将.data(+.bss)拷贝重定位到RAM中正确的链接地址启动VxWorks后,gp(_gp)已经正确了指向了数据段中的GOT表。
小结
分析桩函数usrEntry()和compressedEntry()可知,主要是增加了针对不同体系架构的特化处理,具体来说MIPS中有一个特殊的gp寄存器对GOT索引,需要根据不同阶段的链接地址(-Tdata指定或紧随-Ttext之后)来动态调整。调整动作很简单,就是在C函数中内嵌汇编代码“la gp, _gp”来更新gp寄存器。目前尚未研究ARM、PPC等其他体系架构的GOT索引机制,另外在这两个桩函数均有针对Intel I960体系架构进行调用sysInitAlt()的特化处理。
参考:
(1) 关于gp/GOT可参考《MIPS体系结构透视》2.2<寄存器>、9.4<寻址模式>和16.2<全局偏移表(GOT)组织>,在《程序员的自我修养——链接、装载与库》的7.3.3<地址无关代码>的类型三中提到了共享库PIC中的GOT策略。
(2) 关于ELF文件格式和结构,可直接查看Linux下的“/usr/include/elf.h”,也可参考《MIPS体系结构透视》9.5<目标文件和内存布局>、《程序员的自我修养——链接、装载与库》第3章<目标文件里有什么>和《深入Linux设备驱动程序内核机制》第1章<内核模块>等书籍资料。