您的位置:首页 > 编程语言

位置无关代码(PIC)在 Bootloader 中的应用

2012-08-25 18:50 609 查看
华东师范大学 作者: 黄振华 李外云 刘锦高

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

引 言

基于位置无关代码PIC( PositionOIndependent Code) 的程序设计在嵌入式应用系统开发中具有重要的作用,尤其在裸机状态下开发Bootloader 程序及进行内核初始化设计;利用位置无关的程序设计方法还可以在具体应用中用于构建高效率动态链接库,因而深入理解和熟练掌握位置无关的程序设计方法,有助于开发人员设计出结构简单、清晰的应用程序。本文首先介绍位置无关代码的基本概念和实现原理,然后阐述基于ARM
汇编位置无关的程序设计方法和实现过程,最后以Bootloader 程序设计为例,介绍了位置无关程序设计在Bootloader 程序设计中的作用。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

位置无关代码及程序设计方法

1. 基本概念与实现原理



应用程序必须经过编译、汇编和链接后才变成可执行文件,在链接时,要对所有目标文件进行重定位( reloca2tion) ,建立符号引用规则,同时为变量、函数等分配运行地址。当程序执行时,系统必须把代码加载到链接时所指定的地址空间,以保证程序在执行过程中对变量、函数等符号的正确引用,使程序正常运行。在具有操作系统的系统中,重定位过程由操作系统自动完成。

在设计Bootloader 程序时,必须在裸机环境中进行,这时Bootloader 映像文件的运行地址必须由程序员设定。通常情况下,将Bootloader 程序下载到ROM 的0x0 地址进行启动,而在大多数应用系统中,为了快速启动,首先将Bootloader 程序拷贝到SDRAM 中再运行。一般情况下,这两者的地址并不相同,程序在SDRAM 中的地址重定位过程必须由程序员完成。实际上,由于Bootloader 是系统上电后要执行的第一段程序,Bootloader
程序的拷贝和在这之前的所有工作都必须由其自身来完成,而这些指令都是在ROM 中执行的。也就是说,这些代码即使不在链接时所指定的运行时地址空间,也可以正确执行。这就是位置无关代码,它是一段加载到任意地址空间都能正常执行的特殊代码。

位置无关代码常用于以下场合:

程序在运行期间动态加载到内存;
程序在不同场合与不同程序组合后加载到内存(如共享的动态链接库) ;
在运行期间不同地址相互之间的映射( 如Boot2loader 程序) 。

虽然在用GCC 编译时,使用 -fPIC 选项可为 C 语言产生位置无关代码,但这并不能修正程序设计中固有的位置相关性缺陷。特别是汇编语言代码,必须由程序员遵循一定的程序设计准则,才能保证程序的位置无关性。

2. ARM处理器的位置无关程序设计要点
ARM 程序的位置无关可执行文件PIE ( Position2 Independent Executable) 包括位置无关代码PIC 和位置无关数据PID(Position2Independent Data) 两部分。
PID 主要针对可读写数据段( . data 段) ,其中保存已赋初值的全局变量。为实现其位置无关性,通常使用寄存器R9 作为静态基址寄存器,使其指向该可读写段的首地址,并使用相对于基址寄存器的偏移量来对该段的变量进行寻址。这种方法常用于为可重入程序的多个实例产生多个独立的数据段。在程序设计中,一般不必考虑可读写段的位置无关性,这主要是因为可读写数据主要分配在 SDRAM 中。
PIC 包括程序中的代码和只读数据( . text 段) ,为保证程序能在ROM 和SDRAM 空间都能正确运行(如裸机状态下的Bootloader 程序) ,必须采用位置无关代码程序设计。下面重点介绍PIC 的程序设计要点。PIC 遵循只读段位置无关ROPI ( Read-Only Position Independence) 的ATPCS ( ARM-Thumb Procedure Call Standard) 的程序设计规范:

(1) 程序设计规范1

引用同一ROPI 段或相对位置固定的另一ROPI 段中的符号时,必须是基于PC 的符号引用,即使用相对于当前PC 的偏移量来实现跳转或进行常量访问。
①位置无关的程序跳转。
在ARM 汇编程序中,使用相对跳转指令B/ BL 实现程序跳转。指令中所跳转的目标地址用基于当前PC 的偏移量来表示,与链接时分配给地址标号的绝对地址值无关,因而代码可以在任何位置进行跳转,实现位置无关性。另外,还可使用ADR 或ADRL 伪指令将地址标号值读取到PC 中实现程序跳转。这是因为ADR 或ADRL 等伪指令会被编译器替换为对基于PC 的地址值进行操作, 但这种方式所能读取的地址范围较小,并且会因地址值是否为字对齐而异。但在ARM
程序中,使用LDR 等指令直接将地址标号值读取到PC 中实现程序跳转不是位置无关的。例如:

LDR   PC , = main
上面的LDR 汇编伪指令编译后的结果为:

LDR   PC , [ PC , OFFSET_ TO_L POOL ]
…
L POOL  DCD main
可见, 虽然LDR 是把基于PC 的一个存储单元 L POOL 的内容加载到PC 中,但该存储单元中保存的却是链接时所决定的main 函数入口的绝对地址,所以main函数实际所在的段不是位置无关。

②位置无关的常量访问。
在应用程序中,经常要读写相关寄存器以完成必要的硬件初始化。为增强程序的可读性,利用EQU 伪指令对一些常量进行赋值,但在访问过程中, 必须实现位置无关性。下面以PXA270 的GPIO 初始化介绍位置无关的常量访问方法。

GPIO_BASE    EQU  0x40e00000    ;GPIO 基址寄存器地址
GPDR0       EQU  0x00c        ;相对于GPIO 基址寄存器的偏移量
init_ GPDR0  EQU  0xfffbfe00    ;寄存器GPDR0 初值
LDR  R1 , = GPIO_BASE
LDR  R0 , = init_GPDR0
STR  R0 , [ R1 , # GPDR0 ]
上述汇编代码段经编译后的结果为:

LDR  R1 , [ PC , OFFSET_ TO_GPIO_BASE]
LDR  R0 , [ PC , OFFSET_ TO_init_GPDR0 ]
STR  R0 , [ R1 , # 0xc ]
…
GPIO_BASE    DCD  0x40e00000
GPDR0       DCD  0x00c
init_ GPDR0  DCD  0xfffbfe00
可见,LDR 伪指令实际上使用基于PC 的偏移量来对符号常量GPIO_BASE 和init _ GPDR0 进行引用,因而是位置无关的。

由此可以得出如下结论:使用LDR 伪指令将一个常量读取到非PC 的其他通用寄存器中可实现位置无关的常量访问;但将一个地址值读取到PC 中进行程序跳转时,跳转目标则是位置相关的。
(2) 程序设计规范2

其他被ROPI 段中的代码引用的必须是绝对地址,或者是基于可读写位置无关( RWPI) 段的静态基址寄存器的可写数据。
使用绝对地址只能引用被重定位到特定位置的代码段中的符号,通过在位置无关代码中引入绝对地址,可以让程序跳转到指定位置。例如,假设Bootloader 的阶段1将其自身代码拷贝到链接时所指定的SDRAM 地址空间后,当要跳转到阶段2 的C 程序入口时,可以使用指令“LDR  PC , = main”跳转到程序在SDRAM 中的main 函数入口地址开始执行。这是因为程序在编译链接时给main 函数分派绝对地址,系统通过将main
函数的绝对地址直接赋给PC 实现程序跳转。如果使用相对跳转指令“B  main”,那么只会跳转到启ROM 内部的main 函数入口。

位置无关代码在Bootloader 设计中的应用

在使用GNU 工具开发Bootloader 时,程序在链接时会通过一个链接脚本(linker script) 来设定映像文件的内存映射。一个简单的链接脚本结构如下:
OU TPU T_ARCH(arm)
EN TRY(_start)
SECTIONS {
  .= BOOTADDR ;        /*Bootloader 的起始地址*/
  _ _boot_start = .;
  .text   AL IGN(4): {/* 代码段. text */
   *( .text)
  }
  .data    AL IGN(4): {/* 数据段. data */
   *( .data)
  }
  .got    AL IGN(4):  {/* 全局偏移量表.got 段 */
   *( .got)
  }
  _ _boot_end = .;	   /* Bootloader 映像文件的结束地址 */
  .bss  AL IGN(16): {   /* 堆栈段. bss */
   _ _bss_start = .;
   *( .bss)
   _ _bss_end = .;
  }
}

这里不再介绍链接脚本的语法。需要指出的是,链接脚本中所描述的输出段地址为虚拟地址VMA ( Virtual Memory Address) 。这里的“虚拟地址”仅指映像文件执行时,各输出段所重定位到相应的存储地址空间,与内存管理无关。因此,上面的链接脚本实际上指定了Bootloader 映像在执行时,将被重定位到BOOTADDR 开始的存储地址空间,以保证在相关位置对符号进行正确引用,使程序正常运行。
ARM 处理器复位后总是从0x0 地址取第1 条指令,因此只需把BOOTADDR 设置为0 ,再把编译后生成的可执行二进制文件下载到ROM 的0x0 地址开始的存储空间,程序便可正常引导;但是,一旦在链接时指定映像文件从0x0 地址开始,那么Bootloader 就只能在0x0 地址开始的ROM 空间内运行,而无法拷贝到SDRAM 空间运行实现快速引导。当然,对PXA270 等具有MMU 功能的微处理器来说, 虽然可以先将Bootloader
映像整个拷贝到SDRAM 中,再使用MMU 功能将SDRAM 空间映射到0x0 地址,进而继续在SDRAM 中运行;但这样一方面会使得Bootloader 的设计与实现复杂化,另一方面在一些必须屏蔽MMU 功能的应用中(例如引导arm2linux 系统),无法使用MMU 进行地址重映射。
利用ARM 的基于位置无关的程序设计可以解决上述问题。只需在程序链接时, 将BOOTADDR 设置为SDRAM 空间的地址(一般情况下利用SDRAM 中最高的1 MB 存储空间作为起始地址) ,这样ARM 处理器上电复位后Bootloader 仍然可以从地址0 开始执行,并将自身拷贝到指定的_ _ boot _ start 起始的SDRAM 中运行。实现上述功能的链接脚本所对应的启动代码架构如下:
. section . text
. globl _start
_start :
B  reset       /*复位异常*/
…                 /*其他异常处理代码*/
reset :    
…                 /* 复位处理程序*/
copy_boot :             /* 拷贝Bootloader 到SDRAM */
LDR  R0 , = 0x0
LDR  R1 , = _ _boot_start
LDR  R2 , = _ _boot_end
1 :  LDRMIA  R0 !, { R3 - R10 }
STRMIA  R1 !, { R3 - R10 }
CMP  R1, R2
BL T   1b
clear_bss :    
…                   /*清零. bss 段*/
BL   init_Stack   /*初始化堆栈*/
LDR  PC, = main    /*跳转到阶段2 的C 程序入口 */
end

程序入口为_start ,即复位异常,所有其他异常向量都使用相对跳转指令B 来实现,以保证位置无关特性。在完成基本的硬件初始化后,利用链接脚本传递过来_ _boot_start 和_ _boot_end 的参数,将Bootloader 映像整个拷贝到指定的SDRAM 空间,并清零. bss 段,初始化堆栈后,程序将main 函数入口的绝对地址赋给PC ,进而跳转到SDRAM 中继续运行。程序在跳转到main 函数之前,所有的代码都在ROM 中运行,因而必须要保证代码的位置无关性,所以在调用初始化GPIO、存储系统和堆栈等子程序时,都使用相对跳转指令来完成。
使用位置无关设计Bootloader 程序有如下优点:

①简化设计,方便实现系统的快速引导。
位置无关代码可以避免在引导时进行地址映射,并方便地跳转到SDRAM 中实现快速引导。

②实现复位处理智能化。
由于位置无关代码可以被加载到任意地址空间运行,因此其运行时的当前地址与链接时所指派的地址并不一定相同。利用这一特性,可以在复位处理程序中使处理器进入SVC 模式并关闭中断后加入如下代码,便可根据当前运行时的地址进行不同的复位处理:

ADR  R0, _start         /*读取当前PC 附近的_start 标号所在指令地址*/
LDR  R1, = _ _boot_start  /*读取Bootloader 在SDRAM的起始地址*/
CMP  R0, R1
BEQ  clear_bss
上述代码中的ADR 指令读取的_start 标号地址由指令的执行地址决定。若是从SDRAM 中的Bootloader 启动,则上述比较结果相等,程序直接跳转到clear_bss 标号地址处执行, 这样可以避免存储系统的重新初始化和Bootloader 的拷贝过程; 若是上电或硬件复位, 程序从ROM 启动,则上述比较结果不等,程序便进行包括系统初始化和Bootloader
拷贝等过程的全面复位处理操作。

③便于调试。
Bootloader 的调试通常也是一个繁琐的过程,使用位置无关代码,则可以将映像文件加载到SDRAM 中进行调试,这既能真实地反映程序从ROM 中进行系统引导的情况,又可以避免频繁烧写程序存储器。

结 论
本文所介绍的基于位置无关的程序设计是通过基于PC 或基址寄存器的符号引用规范来实现的。这种方法在实际系统开发中应用广泛,既能用于引导程序的设计,也可用于一般的应用程序或嵌入式共享库的开发。而在Bootloader 的设计中引入位置无关代码,可以使程序结构更为简单清晰,并能避免地址重映射并从SDRAM 进行快速系统引导;引用位置无关的设计方法使Bootloader
的复位处理功能更为灵活,还使得在SDRAM 中和在ROM 中进行程序调试具有相同的效果.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: