天天看點

《嵌入式Linux開發實用教程》——1.4 映像檔案的生成和運作define GPMCON ((volatile unsigned long)0x7F008820)define GPMDAT ((volatile unsigned long)0x7F008824)define GPMPUD ((volatile unsigned long)0x7F008828)

本節書摘來異步社群《嵌入式linux開發實用教程》一書中的第1章,第1.4節,作者:朱兆祺 ,李強 ,袁晉蓉 ,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

嵌入式linux開發實用教程

德國罕見的科學大師萊布尼茨,在他的手迹裡留下這麼一句話:“1與0,一切數字的神奇淵源。這是造物的秘密美妙的典範,因為,一切無非都來自上帝。”二進制0和1兩個簡單的數字,構造了神奇的計算機世界,對人類的生産活動和社會活動産生了極其重要的影響,并以強大的生命力飛速發展。在嵌入式系統移植過程中,不管檔案數量多麼龐大,經過編譯工具的層層處理後,最終生成一個可以加載到存儲器内執行的二進制映像檔案(.bin)。本節内容将會探讨映像檔案的生成過程,以及它在儲存設備的不同位置對程式運作産生的影響,為本書後文嵌入式系統的移植打下堅定的基礎。

gnu提供的編譯工具包括彙編器as、c編譯器gcc、c++編譯器g++、連結器ld、二進制轉換工具objcopy和反彙編的工具objdump等。它們被稱作gnu編譯器集合,支援多種計算機體系類型。基于arm平台的工具分别為arm-linux-gcc、arm-linux-g++、arm-linux-ld、arm-linux-objcopy和arm-linux-objdump。arm-linux交叉編譯工具鍊的制作方法已經詳細介紹過了,編譯程式直接使用前面制作好的工具鍊。

gnu編譯器的功能非常強大,程式可以用c檔案、彙編檔案編寫,甚至是二者的混合。如圖1.3所示是程式編譯的大體流程,源檔案經過預處理器、彙編器、編譯器、連結器處理後生成可執行檔案,再由二進制轉換工具轉換為可用于燒寫到flash的二進制檔案,同時為了調試的友善還可以用反彙編工具生成反彙編檔案。圖中雙向箭頭的含義是,當gcc增加一些參數時可以互相調用彙編器和連結器進行工作。例如輸入指令行“gcc –o main.c”後,直接就得到可執行檔案a.out(elf)。

《嵌入式Linux開發實用教程》——1.4 映像檔案的生成和運作define GPMCON ((volatile unsigned long)0x7F008820)define GPMDAT ((volatile unsigned long)0x7F008824)define GPMPUD ((volatile unsigned long)0x7F008828)

程式編譯大體上可以分為編譯和連結兩個步驟:把源檔案處理成中間目标檔案.o(linux)、obj(windows)的動作稱為編譯;把編譯形成的中間目标檔案以及它們所需要的庫函數.a(linux)、lib(windows)連結在一起的動作稱為連結。現用一個簡單的test工程來分析程式的編譯流程。麻雀雖小,五髒俱全,它由啟動程式start.s、應用程式main.c、連結腳本test.lds和makefile四個檔案構成。test工程中的程式通過操作單闆上的led燈的狀态來判定程式的運作結果,它除了用于理論研究之外,沒有其他的實用價值。

1.編譯

在編譯階段,編譯器會檢查程式的文法、函數與變量的聲明情況等。如果檢查到程式的文法有錯誤,編譯器立即停止編譯,并給出錯誤提示。如果程式調用的函數、變量沒有聲明原型,編譯器隻會抛出一個警告,繼續編譯生成中間目标檔案,待到連結階段進一步确定調用的變量、函數是否存在。

start.s檔案的内容如程式清單1.1所示,檔案中的_start函數,為程式能夠在c語言環境下運作做了最低限度的初始化:将s3c6410處理外設端口的位址範圍告知arm核心,關閉看門狗,清除bss段,初始化棧。初始化工作完畢後,跳轉到main()。start.s是用彙編語言編寫的代碼檔案,檔案中定義了一個watchdog宏,用于寄存器的指派。在彙編檔案中出現#define宏定義語句,對于初學者可能會有些迷惑。

程式清單1.1 start.s中彙編代碼

/*

* this is a part of the test project

  author: liqiang date: 2013/04/01

  licensed under the gpl-2 or later.

*/

int main()

{

  static int flag = 12;

  gpmcon = 0x1111; / 輸出模式 /

  gpmpud = 0x55;  / 使能下拉 /

  gpmdat = 0x0f;  / 關閉led /

  if(12 == flag)

    gpmdat = 0x00;

  else

    gpmdat = 0x0f;

  while(1);

  return 0;

}<code>`</code>

将上面兩個源碼檔案處理成中間目标檔案,分别輸入如下指令行:

entry(_start)

sections

    . = 0x50000000;

    . = align(4);

    .text : {

        start.o (.text)

        * (.text)

    }

    .data : {

        * (.data)

    bss_start = .;

    .bss : {

        * (.bss)

    bss_end = .;

(1)text段代碼段(text segment),通常是用來存放程式執行代碼的記憶體區域。這塊區域的大小在程式編譯時就已經确定,并且記憶體區域通常屬于隻讀,某些架構也允許代碼段為可寫,即允許修改程式。在代碼段中,也有可能包含一些隻讀的常數變量,例如字元串常量。

(2)data段資料段(data segment),資料段是存放已經初始化不為0的靜态變量的記憶體區域,靜态變量包括全局變量和局部變量,它們與程式有着相同的生存期。

(3)bss段bss segment,bbs段與data段類似,也是存放的靜态變量的記憶體區域。與data段不同的是,bbs段存放的是沒有初始化或者初始化為0的靜态變量,并且bbs段不在生成的可執行二進制檔案内。bss_start表示這塊記憶體區域的起始位址,bss_end表示結束位址,它們由編譯系統計算得到。未初始化的靜态變量預設為0,是以程式開始執行的時候,在bss_start到bss_end記憶體中存儲的資料都必須是0。

(4)其他段,上面3個段是編譯系統預定義的段名,使用者還能通過.section僞操作自定義段,在後面的移植過程中會發現,linux核心源碼中為了合理地排布資料實作特定的功能,定義了各種各樣的段。

在主控端上輸入以下指令行,完成中間的目标檔案的連結和可執行二進制檔案的格式轉換。

50000000 &lt;_start&gt;: / 代碼段起始位置程式的運作位址為0x5000_0000/

50000000:  e3a00207   mov  r0, #1879048192  ; 0x70000000

50000004:  e3800013   orr  r0, r0, #19  ; 0x13

50000008:  ee0f0f92   mcr  15, 0, r0, cr15, cr2, {4}

5000000c:  e59f0030   ldr  r0, [pc, #48]  ; 50000044

50000010:  e3a01000   mov  r1, #0  ; 0x0

50000014:  e5801000   str  r1, [r0]

50000018 : / 清除bss段 /

50000018:  e59f0028   ldr  r0, [pc, #40]  ; 50000048

5000001c:  e59f1028   ldr  r1, [pc, #40]  ; 5000004c

50000020:  e3a03000   mov  r3, #0  ; 0x0

50000024:  e1500001   cmp  r0, r1

50000028:  0a000002   beq  50000038

5000002c :

5000002c:  e4803004   str  r3, [r0], #4

50000030:  e1500001   cmp  r0, r1

50000034:  1afffffc   bne  5000002c

50000038 :

50000038:  e3a0da02   mov  sp, #8192  ; 0x2000 / 初始化sp /

5000003c:  eb000003   bl  50000050 / 跳轉至mian() /

50000040 :

50000040:  eafffffe   b  50000040

50000044:  7e004000   .word  0x7e004000

50000048:  500000e0   .word  0x500000e0

5000004c:  500000e0   .word  0x500000e0

50000050 : / main()/

50000050:  e52db004   push  {fp}    ; (str fp, [sp, #-4]!)

50000054:  e28db000   add  fp, sp, #0  ; 0x0

50000058:  e3a0247f   mov  r2, #2130706432  ; 0x7f000000

5000005c:  e2822b22   add  r2, r2, #34816  ; 0x8800

50000060:  e2822020   add  r2, r2, #32  ; 0x20

50000064:  e3a03c11   mov  r3, #4352  ; 0x1100

50000068:  e2833011   add  r3, r3, #17  ; 0x11

5000006c:  e5823000   str  r3, [r2] / gpmcon = 0x1111 /

50000070:  e3a0347f   mov  r3, #2130706432  ; 0x7f000000

50000074:  e2833b22   add  r3, r3, #34816  ; 0x8800

50000078:  e2833028   add  r3, r3, #40  ; 0x28

5000007c:  e3a02055   mov  r2, #85  ; 0x55

50000080:  e5832000   str  r2, [r3] / gpmpud = 0x55 /

50000084:  e3a0347f   mov  r3, #2130706432  ; 0x7f000000

50000088:  e2833b22   add  r3, r3, #34816  ; 0x8800

5000008c:  e2833024   add  r3, r3, #36  ; 0x24

50000090:  e3a0200f   mov  r2, #15  ; 0xf

50000094:  e5832000   str  r2, [r3] / gpmdat = 0x0f /

50000098:  e59f3038   ldr  r3, [pc, #56]  ; 500000d8  / 讀取flag變量存儲位址 /

5000009c:  e5933000   ldr  r3, [r3]/ 讀取flag變量的值 /

500000a0:  e353000c   cmp  r3, #12  ; 0xc

500000a4:  1a000005   bne  500000c0

500000a8:  e3a0347f   mov  r3, #2130706432  ; 0x7f000000

500000ac:  e2833b22   add  r3, r3, #34816  ; 0x8800

500000b0:  e2833024   add  r3, r3, #36  ; 0x24

500000b4:  e3a02000   mov  r2, #0  ; 0x0

500000b8:  e5832000   str  r2, [r3]

500000bc:  ea000004   b  500000d4

500000c0:  e3a0347f   mov  r3, #2130706432  ; 0x7f000000

500000c4:  e2833b22   add  r3, r3, #34816  ; 0x8800

500000c8:  e2833024   add  r3, r3, #36  ; 0x24

500000cc:  e3a0200f   mov  r2, #15  ; 0xf

500000d0:  e5832000   str  r2, [r3]

500000d4:  eafffffe   b  500000d4

500000d8:  500000dc   .word  0x500000dc

disassembly of section .data:

500000dc : / flag變量的位址為0x5000_00dc,值為12 /

500000dc:  0000000c   .word  0x0000000c<code>`</code>

從test.dis反彙編檔案中可知,test.bin包含了代碼段和資料段,并沒有包含bss段。我們知道,bbs記憶體區域的資料初始值全部為零,區域的起始位置和結束位置在程式編譯的時候預知。很容易想到在程式開始運作時,執行一小段代碼将這個區域的資料全部清零即可,沒必要在test.bin包含全為0的bss段。編譯器的這種機制有效地減小了鏡像檔案的大小,節約了磁盤容量。

main()函數的核心功能是驗證flag變量是否等于12,現在追蹤下這個操作的實作過程。要想讀取flag的值,必須知道它的存儲位置,首先執行指令“ldrr3, [pc, #56]”得到flag變量的位址(指針)。pc與56相加合成一個位址,它是相對pc偏移56産生的。pc+56位址處存放了flag變量的指針0x5000_00dc,讀取出來存放到r3寄存器。然後執行指令“ldrr3, [r3]”,将記憶體0x5000_00dc位址處的值讀出,這個值就是flag,并覆寫r3寄存器。最後,判斷r3寄存器是否等于12。flag變量的位址在連結階段已經被配置設定好了,固定在0x5000_00dc處,但是從代碼中,我們沒有找到對flag變量賦初值的語句,盡管在main函數已經用c語句“flag = 12”對它賦初值。

現提供一個驗證程式效果的簡單方法:将s3c6410處理器設定為sd卡啟動方式,使用sd_writer軟體将test.bin燒寫至sd卡中,然後将sd卡插入單闆的卡槽,複位啟動即可。實際上,啟動的時候test.bin被加載到内部sram中,sram映射到0位址處。這個簡單方法可以用來驗證一些裸闆程式,方法實作的原理和sd_writer軟體用法現在不展開讨論,目前隻要會使用即可。複位後,led并沒有點亮。

如果每次編譯都要重複輸入編譯指令,操作起來很麻煩,為此test工程中建立了一個makefile檔案,内容如下:

ldr r0, =watchdog<code>`</code>

如果使用ldr僞指令将一個函數标号讀取到pc,這是一條與位置有關的跳轉指令,執行的結果是跳轉到函數的運作位址處。

2.運作位址與加載位址

試想一下,當系統上電複位的時候,如果test.bin剛好位于0x5000_0000位址(flag的初值12位于0x5000_00dc),pc指向0x5000_0000位址,那麼這段代碼按照上述flag變量的讀取步驟,能夠準确無誤地得到結果。但是,如果test.bin位于0位址(flag的初值12位于0xdc,led不亮時的情況),pc指向0位址,程式依然從0x5000_00dc位址讀取flag變量,實際上它的初值位于0xdc。這時從c語言的角度看,出現一個flag不等于它的初值的現象(期間沒有改變flag)。出現錯誤的原因是在程式中使用了位置相關的變量,但運作位址與加載位址不一緻(加載位址為0,運作位址為0x5000_0000)。由此,能夠容易了解運作位址和加載位址的含義:

加載位址是系統上電啟動時,程式被加載到可直接執行的存儲器的位址,也就是程式在ram或者flash rom中的位址。因為有些存儲媒體隻能用來存儲資料不能執行程式,例如sd卡和nand flash等,必須把程式從這些存儲媒體加載到可以執行的位址處。運作位址就是程式在連結時候确定的位址,比如fortest.lds連結腳本指定了程式的運作位址為0x5000_0000,那麼連結器在為變量、函數等配置設定位址的時候就會以0x5000_0000作為參考。當加載位址和運作位址不相等時,必須使用與位置無關碼把程式代碼從它的加載位址搬運至運作位址,然後使用“ldr pc, =label”指令跳轉到運作位址處執行。

在嵌入式系統底層程式設計中,c語言和彙編兩種程式設計語言的使用最廣泛。c語言開發的程式具有可讀性高,容易修改、移植和開發周期短等特點。但是,c語言在一些場合很難或無法實作特定的功能:底層程式需要直接與cpu核心打交道,一些特殊的指令在c語言中并沒有對應的成分,例如關閉看門狗、中斷的使能等;被系統頻繁調用的代碼段,對代碼的執行效率要求嚴格的時候。事實上,cpu體系結構并不一緻,沒有對内部寄存器操作的通用指令。彙編語言與cpu的類型密切相關,提供的助記符指令能夠友善直接地通路硬體,但要求開發人員對cpu的體系結構十分熟悉。在早期的微處理器中,由于處理器速度、存儲空間等硬體條件的限制,開發人員不得不選用彙編語言開發程式。随着微處理器的發展,這些問題已經得到很好的解決。如果依然完全使用彙編語言編寫程式,工作量會非常大,系統很難維護更新。大多數情況下,充分結合兩種語言的特點,彼此互相調用,以約定規則傳遞參數,共享資料。

1.彙編函數與c語言函數互相調用

c程式函數與彙編函數互相調用時必須嚴格遵循atpcs(armthumb procedure call standard)。函數間約定r0、r1和r2為傳入參數,函數的傳回值放在r0中。gnu arm編譯環境中,在彙程式設計式中要使用.global僞操作聲明改彙程式設計式為全局的函數,可被外部函數調用。在c程式中要被彙程式設計式調用的c函數,同樣需要用關鍵字extern聲明。

程式清單1.4是從archarmcpuarm1176start.s檔案(u-boot)中截取的代碼片段,relocate_code函數用于重定位代碼。它在c程式中,通過relocate_code(addr_sp, id, addr)被調用。變量addr_sp、id和addr分别通過寄存器r0、r1和r3傳遞給彙程式設計式,實作了c函數和彙編函數資料的共享。

程式清單1.4 代碼重定位函數

armv6 up and smp safe atomic ops. we use load exclusive and

store exclusive to ensure that these are atomic. we may loop

to ensure that the update happens.

static inline void atomic_add(int i, atomic_t *v)

{  

  unsigned long tmp;  

  int result;

  __asm__ __volatile__("@ atomic_addn"

"1:  ldrex  %0, [%3]n"

"  add  %0, %0, %4n"

"  strex  %1, %0, [%3]n"

"  teq  %1, #0n"

"  bne  1b"  

  : "=&amp;r" (result), "=&amp;r" (tmp), "+qo" (v-&gt;counter)  

  : "r" (&amp;v-&gt;counter), "ir" (i)  

  : "cc");

asm語句最常用的格式為:

繼續閱讀