1. 程式人生 > >連結指令碼檔案的寫法

連結指令碼檔案的寫法

對於.lds檔案,它定義了整個程式編譯之後的連線過程,決定了一個可執行程式的各個段的儲存位置。雖然現在我還沒怎麼用它,但感覺還是挺重要的,有必要了解一下。 先看一下對.lds檔案形式的完整描述:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}
secname和contents是必須的,其他的都是可選的。下面挑幾個常用的看看:
1、secname:段名 2、contents:決定哪些內容放在本段,可以是整個目標檔案,也可以是目標檔案中的某段(程式碼段、資料段等) 3、start:本段連線(執行)的地址,如果沒有使用AT(ldadr),本段儲存的地址也是start。GNU網站上說start可以用任意一種描述地址的符號來描述。 4、AT(ldadr):定義本段儲存(載入)的地址。
看一個簡單的例子:(摘自《2410完全開發》)
/* nand.lds */
SECTIONS { 
firtst 0x00000000 : { head.o init.} 
second 0x30000000 : AT(4096) { main.} 
}
    以上,head.o放在0x00000000地址開始處,init.o放在head.o後面,他們的執行地址也是0x00000000,即連線和儲存地址相同(沒有AT指定);main.o放在40960x1000,是AT指定的,儲存地址)開始處,但是它的執行地址0x30000000,執行之前需要從0x1000(載入處)複製到0x30000000(執行處),此過程也就用到了讀取Nand flash 這就是儲存地址和連線(執行)地址的不同,稱為載入時域和執行時域,可以在
.lds連線指令碼檔案中分別指定。 編寫好的.lds檔案,在用arm-linux-ld連線命令時帶-Tfilename來呼叫執行,如
arm-linux-ld Tnand.lds x.o y.o o xy.o。也用-Ttext引數直接指定連線地址,如
arm-linux-ld Ttext 0x30000000 x.o y.o o xy.o 既然程式有了兩種地址,就涉及到一些跳轉指令的區別,這裡正好寫下來,以後萬一忘記了也可檢視,以前不少東西沒記下來現在忘得差不多了。。。 ARM彙編中,常有兩種跳轉方法:b跳轉指令、ldr指令向PC賦值。 我自己經過歸納如下: (1)b step1 b
跳轉指令是相對跳轉,依賴當前PC的值,偏移量是通過該指令本身bit[23:0]算出來的,這使得使用b指令的程式不依賴於要跳到的程式碼的位置,只看指令本身。 (2)ldr pc, =step1 :該指令是從記憶體中的某個位置(step1)讀出資料並賦給PC,同樣依賴當前PC的值,但是偏移量是那個位置(step1)的連線地址(執行時的地址),所以可以用它實現從FlashRAM的程式跳轉。 (3)此外,有必要回味一下adr偽指令,U-boot中那段relocate程式碼就是通過adr實現當前程式是在RAM中還是flash中。仍然用我當時的註釋:
relocate: /* 把U-Boot重新定位到RAM */
    adr r0, _start /* r0是程式碼的當前位置 */ 
/* adr偽指令,彙編器自動通過當前PC的值算出 如果執行到_start時PC的值,放到r0中:
當此段在flash中執行時r0 = _start = 0;當此段在RAM中執行時_start = _TEXT_BASE(在board/smdk2410/config.mk中指定的值為0x33F80000,即u-boot在把程式碼拷貝到RAM中去執行的程式碼段的開始) */

    ldr r1, _TEXT_BASE /* 測試判斷是從Flash啟動,還是RAM */ 
/* 此句執行的結果r1始終是0x33FF80000,因為此值是又編譯器指定的(ads中設定,或-D設定編譯器引數) */
    cmp r0, r1 /* 比較r0和r1,除錯的時候不要執行重定位 */
    下面,結合u-boot.lds看看一個正式的連線指令碼檔案。這個檔案的基本功能還能看明白,雖然上面分析了好多,但其中那些GNU風格的符號還是著實讓我感到迷惑,好菜啊,怪不得連被3家公司鄙視,自己鄙視自己。。。
OUTPUT_FORMAT("elf32­littlearm", "elf32­littlearm", "elf32­littlearm")
  ;指定輸出可執行檔案是elf格式,32位ARM指令,小端
OUTPUT_ARCH(arm)
  ;指定輸出可執行檔案的平臺為ARM
ENTRY(_start)
  ;指定輸出可執行檔案的起始程式碼段為_start.
SECTIONS
{
        . = 0x00000000 ; 從0x0位置開始
        . = ALIGN(4) ; 程式碼以4位元組對齊
        .text : ;指定程式碼段
        {
          cpu/arm920t/start.(.text) ; 程式碼的第一個程式碼部分
          *(.text) ;其它程式碼部分
        }
        . = ALIGN(4) 
        .rodata : { *(.rodata) } ;指定只讀資料段
        . = ALIGN(4);
        .data : { *(.data) } ;指定讀/寫資料段
        . = ALIGN(4);
        .got : { *(.got) } ;指定got段, got段式是uboot自定義的一個段, 非標準段
        __u_boot_cmd_start = . ;把__u_boot_cmd_start賦值為當前位置, 即起始位置
        .u_boot_cmd : { *(.u_boot_cmd) } ;指定u_boot_cmd段, uboot把所有的uboot命令放在該段.
        __u_boot_cmd_end = .;把__u_boot_cmd_end賦值為當前位置,即結束位置
        . = ALIGN(4);
        __bss_start = .; 把__bss_start賦值為當前位置,即bss段的開始位置
        .bss : { *(.bss) }; 指定bss段
        _end = .; 把_end賦值為當前位置,即bss段的結束位置
}
本文中的所有程式碼版本都是基於ST的SpearPlus開發板的。

xloader是在系統上電之後,執行完ROM中的frimware後最先開始執行的使用者程式,它的體積很小,執行的功能也很簡單,主要是對系統時鐘以及外部SDRAM進行初始化,初始化完成之後就檢查Flash中的uboot image是否準備好,如果準備好了就將Flash中的uboot image根據image header中指定的load address載入到外部SDRAM中,然後就跳轉到uboot執行程式碼。

這裡,我試圖從頭開始,在原始碼級別上來分析整個系統的引導過程。

像Xloader或者uboot之類的程式,並不像我們平常寫的應用程式那樣,程式的入口函式直接找main函式就行。對於這種系統程式,在最開始看程式碼,尤其是要找到最開始執行的程式碼的位置的時候,最好的一個方法就是找到整個工程的.lds檔案,也就是連結指令碼檔案(linker loader script)。它定義了整個工程在編譯之後的連結過程,以及各個輸入目標檔案中的各個段在輸出目標檔案中的分佈。詳細的關於lds檔案的介紹可以參考 gnu的線上文件:http://sourceware.org/binutils/docs/ld/index.html。其中的第三節Linker Script對連結指令碼檔案進行了介紹。

現在,我們首先開看一看xloader.lds的程式碼:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(XLOADER_ENTRY)
SECTIONS
{
    . = 0x00000000;
    . = ALIGN(4);
    .text    :
    {
      ./obj/init.o    (.text)
      *(.text)
    }

    .rodata . :
        {
                *(.rodata)
        }

        . = ALIGN(4);


    .data : { *(.data) }
    . = ALIGN(4);
    .got : { *(.got) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) }
    _end = .;
}

下面,我們對這一段程式碼逐句進行分析。

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
在GNU的文件中,是這麼定義的:
OUTPUT_FORMAT(default, big, little),在連結的時候,如果使用了-EB的命令列引數,則使用這裡的big引數指定的位元組序,如果使用了-EL的命令列引數,則使用這裡的little引數指定的位元組序,如果沒有使用任何命令列引數,則使用這裡的default引數指定的位元組序。
由xloader.lds中的定義可見,不管在連結的時候使用了何種命令列引數,輸出的目標檔案都是使用elf32-littlearm方式的位元組序。

OUTPUT_ARCH(arm)
在GNU的文件中,是這麼定義的:
OUTPUT_ARCH(bfdarch),也就是指定了目標的體系結構,在這裡,SpearPlus內部使用的處理器核是arm926ejs的,因此體系結構也就是arm。

ENTRY(XLOADER_ENTRY)
在GNU的文件中,是這麼定義的:
ENTRY(symbol)
There are several ways to set the entry point. The linker will set the entry point by trying each of the following methods in order, and stopping when one of them succeeds:
    * the `-e' entry command-line option;
    * the ENTRY(symbol) command in a linker script;
    * the value of the symbol start, if defined;
    * the address of the first byte of the `.text' section, if present;
    * The address 0. 
也就是說,ENTRY(XLOADER_ENTRY)定義了整個程式的入口處,也就是在標號XLOADER_ENTRY處。整個程式將從這裡開始執行。

接下來的部分,是對整個輸出目標檔案中各個段的儲存位置的定義。
在GNU的文件中,是這麼定義的:
SECTIONS
     {
       sections-command
       sections-command
       ...
     }
對於其中的每一個sections-command,其完整的定義如下:
The full description of an output section looks like this:

     section [address] [(type)] :
       [AT(lma)] [ALIGN(section_align)] [SUBALIGN(subsection_align)]
       {
         output-section-command
         output-section-command
         ...
       } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

Most output sections do not use most of the optional section attributes.
The whitespace around section is required, so that the section name is unambiguous. The colon and the curly braces are also required. The line breaks and other white space are optional. 
下面來看看xloader.lds中SECTIONS的定義:
SECTIONS
{
    /* location counter設定為0x00000000,其實由於在Makefile中的
     * 連結選項中使用了-Ttext $(TEXT_BASE),而TEXT_BASE=0xD2800B00
     * 因此,此處的設定其實是沒有作用的,程式碼執行的時候將執行在TEXT_BASE地址
     */
    . = 0x00000000;
    . = ALIGN(4);    /* 四位元組對齊 */
    /* 將所有輸入目標檔案中的.text段即程式碼段放在此處,並且,輸出目標檔案中的.text段中的
     * 開頭部分存放init.o的.text段。也就是執行的第一條程式碼,也就是XLOADER_ENTRY
     * 標號對應的程式碼就在init.o當中
     */
    .text    :
    {
      ./obj/init.o    (.text)
      *(.text)
    }

    /* 緊接著.text段,存放所有輸入目標檔案中的.rodata段,也就是
     * 只讀資料段。此處注意.rodata後跟著的.,這個.表示當前location counter,
     * 對應於上述完整描述sections中的[address]
     * 此處表示.rodata段緊接著.text段存放,而不用任何對齊
     */
    .rodata . :
        {
                *(.rodata)
        }

        . = ALIGN(4);    /* 四位元組對齊 */


    /* 將所有輸入目標檔案中的.data讀寫資料段儲存在此處
     * 所有全域性手動初始化的變數儲存在該段中,並且在輸出目標檔案中已經分配了儲存空間
     */
    .data : { *(.data) }
    . = ALIGN(4);    /* 四位元組對齊 */
    /* .got段是GLOBAL OFFSET TABLE,具體的作用還沒有搞清楚 */
    .got : { *(.got) }

    . = ALIGN(4);    /* 四位元組對齊 */
    /* .bss段的開始,所有全域性未初始化變數的大小等資訊儲存在該段中
     * 但是在輸出的目標檔案中並不為這些變數分配儲存空間,
     * 而是交給作業系統在初始化的時候分配記憶體,然後緊跟在.data段後面並初始化為零
     * 另外,此處還定義了兩個標號分別表示.bss的開始和結束(也是整個目標檔案的結束)
     */
    __bss_start = .;
    .bss : { *(.bss) }
    _end = .;
}

從這裡,我們能夠得到的最關鍵的資訊是:整個程式的入口在標號XLOADER_ENTRY處,並且該標號定義在init.o目標檔案中,因為整個最終的連結之後的目標檔案中,位於最開頭的就是init.o目標檔案。
於是,我們可以根據這個線索來繼續追蹤整個的引導過程了。


參考文章:
對.lds連線指令碼檔案的分析
http://blog.csdn.net/tony821224/archive/2008/01/18/2051755.aspx
Documentation for binutils 2.18--ld
http://sourceware.org/binutils/docs/ld/index.html
.bss段和.data段的區別
http://www.w3china.org/blog/more.asp?name=FoxWolf&id=29997
什麼是bss段
http://blog.csdn.net/bobocheng1231/archive/2008/02/23/2115289.aspx