鏈接腳本文件的寫法

對於.lds文件,它定義了整個程序編譯之後的連接過程,決定了一個可執行程序的各個段的存儲位置。雖然現在我還沒怎麼用它,但感覺還是挺重要的,有必要了解一下。
先看一下GNU官方網站上對.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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章