ARM Architecture C 語言尋址解析—— 從U-Boot relocation所展開的探索(一)

ARM  Architecture C 語言尋址解析——
從U-Boot relocation所展開的探索(一)
by蔡於清
文章的名字有點長也有點拗口,但它卻很好的表達了本文的主題和來歷。這個主題將討論和分析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尋址解析。

這個主題分三個部分講解(每一部分單獨成章,本文爲第一章):
  1. ARM Architecture C語言尋址方式解析(GNU ARM Tool Chain with No PIC compile option)。
  2. ARM Architecture C語言尋址方式解析(GNU ARM Tool Chain with PIC compile option)。
  3. U-Boot Relocation解析。

本系列文章使用如下工具和源碼:
  1. host :ubuntu 10.04 LTS
  2. target:beagleboard
  3. cross toolchain:arm-2011.09-69-arm-none-eabi
  4. 例程:arm_pic
  5. U-Boot Src:u-boot-2011.12

如果讀者具備以下經驗,則讀起來會舒服很多:
  1. 熟悉 ARM Architecture C 語言
  2. 熟悉ARM Architecture 彙編語言
  3. 會使用arm cross toolchain
  4. 瞭解System V ELF format
  5. 瞭解ELF for ARM Arch
  6. 有實際U-Boot開發經驗

我們使用arm_pic作爲解析例程,第一,二部分的探討基本上都圍繞arm_pic工程展開,arm_pic的連接基本地址在arm_pic.lds中被指定爲0x40200000,這個地址是OMAP3的OnChip SRAM起始地址,入口地址爲start.S中的”_start:”,隨後進入main函數,細節很簡單,具體請閱讀源碼,可以從這裏下載arm_pic。該工程由下列文件組成:
  1. Makeconfig(編譯配置)
  2. Makefile(編譯腳本)
  3. arm_pic.lds(連接腳本)
  4. start.S(彙編源文件)
  5. main.c(C源文件)

命令行進入arm_pic目錄,make,得到以下文件:
  1. arm_pic(elf格式文件)
  2. arm_pic.bin(二進制鏡像文件)
  3. arm_pic.dump(反彙編文件)
  4. arm_pic.map(Memory Map文件)

ARM Architecture C語言尋址方式解析
(GNU ARM Tool Chain with No PIC compile option)


先分析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三者的尋址方式相同,都是通過基於pc的相對位置取得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函數調用沒問題,問題出在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)。

TO BE CONTINUE... ...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章