转自:http://blog.sina.com.cn/s/blog_9ddd009b0100z79p.html
ARM Architecture C 语言寻址解析——
从U-Boot relocation所展开的探索(一)
by LazyCatDesign
www.lazycatdesign.com
文章的名字有点长也有点拗口,但它却很好的表达了本文的主题和来历。这个主题将讨论和分析ARM架构上C语言对变量和函数的寻址方式,为什么要讨论这个主题?或者说为什么会想到去讨论这个主题?答案就在文章的副标题,没错,因为U-Boot。这段时间本人在移植U-Boot2011.12到自家的OMAP3平台上,早在ARM9时代就接触过U-Boot,当时没有仔细研究,兴趣所致,这次我读起了她的源代码,其中有一个特性引起我的注意,这个特性就是同一份U-Boot代码被加载在不同的内存空间运行,简单说就是U-Boot的运行基地址可以不同于其连接基地址(比如连接时指定的基地址处于Flash空间而运行时基地址处在RAM空间),这就带来一个问题——”CPU如何进行寻址?“。对于一个普通的完整的可运行的bin 文件,必须在生成它的时候(一般是在link阶段)指定其连接基地址,一般而言连接基地址等同于运行基地址(也等同于加载地址),如果运行地址与连接地址不同,则寻址必将发生错误,而U-Boot却可以被整体的从一个内存段搬运到另一个内存段,并顺利地运行,她是如何做到的呢?——答案是relocotion(重定位),那么U-Boot又是如何进行relocation的呢?好奇心的驱使,我决定弄个明白,于是就有了这个主题——ARM寻址解析。
这个主题分三个部分讲解(每一部分单独成章,本文为第一章):
- ARM Architecture C语言寻址方式解析。
- ARM Architecture C语言PIC寻址方式解析。
- U-Boot Relocation解析。
本系列文章使用如下工具和源码:
- host :ubuntu 10.04 LTS
- target:beagleboard
- cross toolchain:arm-2011.09-69-arm-none-eabi
- 例程:arm_pic
- U-Boot Src:u-boot-2011.12
如果读者具备以下经验,则读起来会舒服很多:
- 熟悉 ARM Architecture C 语言
- 熟悉ARM Architecture 汇编语言
- 会使用arm cross toolchain
- 了解System V ELF format
- 了解ELF for ARM Arch
- 有实际U-Boot开发经验
我们使用arm_pic作为解析例程,第一,二部分的探讨基本上都围绕arm_pic工程展开,arm_pic的连接基本地址在arm_pic.lds中被指定为0x40200000,这个地址是OMAP3的OnChip SRAM起始地址,入口地址为start.S中的”_start:”,随后进入main函数,细节很简单,具体请阅读源码,可以从 这里下载arm_pic。该工程由下列文件组成:
- Makeconfig(编译配置)
- Makefile(编译脚本)
- arm_pic.lds(连接脚本)
- start.S(汇编源文件)
- main.c(C源文件)
命令行进入arm_pic目录,make,得到以下文件:
- arm_pic(elf格式文件)
- arm_pic.bin(二进制镜像文件)
- arm_pic.dump(反汇编文件)
- arm_pic.map(Memory Map文件)
ARM Architecture C语言寻址方式解析
先分析arm_pic全局变量的内存分布,下图是arm_pic.dump文件中关于.rodata 数据段,.data 数据段以及.bss数据段的截图。
C变量名 数据段 地址 初始值
global_var1 .data 0x40200150 0x11111111
global_str .data 0x40200154 0x40200134
global_var2 .bss 0x4020015c 0x00000000
可以看到global_var1,global_str是已初始化的全局变量,被存放在.data数据段中,其中global_var1的初始值为0x11111111。global_str初始值为0x40200134,在main.c中,global_str被定义为一个指针,其初始值0x40200134应该是一个地址值,仔细观察可以发现,0x40200134地址的内存空间所存放的数据正是字符串”this is a test for arm_pic”(字符串为只读数据,存放在.rodata数据段),这与main.c中global_str的定义一致,既global_str指向字符串”this is a test for arm_pic”。global_var2是未初始化全局变量,存放在.bss数据段中,被默认初始化为0x00000000。好像标漏了一个变量global_data_length,不知读者看出它在哪里了吗?global_data_length是一个未被初始化的全局变量,所以它应该落在.bss数据段中,它的地址正是0x40200158。
接下来我们分析main函数,下图是arm_pic.dump文件中关于main函数的截图和说明。
先观察0x40200128,0x4020012c,0x40200130这三个内存空间的值,这三个内存空间的值分别是global_var1,global_var2,global_str三个全局变量的地址,这三个内存空间并不是由程序员分配或定义,而是gcc编译器自己产生,我们暂时人为的把这三个内存空间命名为Lable1,Lable2,Lable3,这类由编译器自己产生的用于存放变量地址的Lable非常重要(U-Boot通过修改这种Lable中的值完成relocation),再次强调,这里的Lable指的是0x40200128,0x4020012c,0x40200130
内存空间,其中的
值是
变量地址。从上图中可以看到,CPU对global_var1,global_var2,global_str三者的寻址方式相同,
CPU使用相对寻址的方式取得Lable的地址,从而取得相应的变量地址(Lable的内存值)。需要注意,因为ARM流水线的机制,每一道指令当前的pc值应该是当前指令地址值 + 8。接下来看foo函数调用,如0x40200110地址所示,其指令为“bl 40200098”,这就是调用foo函数的指令,"bl"是相对寻址跳转指令(target_address = pc + offset),这样我们可以得出结论,ARM Architecture C语言的函数调用指令使用的是相对寻址的bl指令,与被调用函数的绝对位置无关。OK,现在思考一个问题,arm_pic的运行基地址在连接时被指定为0x40200000,如果我们把她像U-Boot一样整体copy到另一个地址运行(比如0x80000000,offset = 0x80000000 - 0x40200000 = 0x3fe00000),这样行的通吗?不行,这样为出错。为什么?我们看一看copy后的内存分布,如下图所示:
main函数起始地址处于0x800000e4。因为使用相对寻址的"bl"指令进行函数调用,所以foo函数调用没问题,Lable1,Lable2,Lable3三者的寻址也没问题(因为使用的是基于pc的相对寻址),问题出在Lable里面的值,也就是global_var1,global_var2,global_str的地址,这三者的新地址应该是 原地址 + offset,于是有:
global_var1新地址:0x40200150 + 0x3fe00000 = 0x80000150
global_var2新地址:0x4020015c + 0x3fe00000 = 0x8000015c
global_var3新地址:0x40200154 + 0x3fe00000 = 0x80000154
从指令我们可以看出,对这三个变量进行寻址得到的是原来老的地址,寻址出错(我们期望的是能得到新的地址)。
“Lable内的的值(变量地址)在编译的时候已经被确定下来”——这就是寻址出错的原因。
这样的代码是不可能像U-Boot一样被运行在任意地址段的。那有没有一种可以运行在任意地址段的代码呢?有,这就是第二篇文章要讨论的内容——Position-Independent Code(PIC)。