LD檔案詳解

Asp1rant發表於2024-06-26

一. LD 檔案的概念

ld 檔案通常指的是連結指令碼檔案,主要用於控制連結器(如 GNU 連結器 ld)的行為。連結器是將編譯後的目標檔案(object files)和庫檔案(libraries)結合起來生成可執行檔案或共享庫的工具。連結指令碼允許開發者精確地控制連結過程,例如定義記憶體佈局、設定節(section)的地址、指定符號的位置等。

基本概念

連結器將輸入檔案組合成一個輸出檔案。輸出檔案和每個輸入檔案都採用一種被稱為目標檔案格式的特殊資料格式。每個檔案被稱為目標檔案。輸出檔案通常被稱為可執行檔案,但在這裡我們也稱其為目標檔案。每個目標檔案都有一個節列表。我們有時將輸入檔案中的節稱為輸入節;類似地,輸出檔案中的節稱為輸出節。

目標檔案中的每個節都有一個名稱和一個大小。大多數節還關聯有一個資料塊,稱為節內容。一個節可能被標記為可載入,這意味著在執行輸出檔案時,其內容應該被載入到記憶體中。沒有內容的節可能是可分配的,這意味著應該在記憶體中預留一個區域,但不需要載入任何特定內容(在某些情況下,這段記憶體必須被清零)。既不可載入也不可分配的節通常包含某種除錯資訊。

每個可載入或可分配的輸出節都有兩個地址。第一個是VMA,即虛擬記憶體地址。這是節在執行輸出檔案時的地址。第二個是LMA,即載入記憶體地址。這是節被載入時的地址。在大多數情況下,這兩個地址是相同的。當資料節被載入到ROM中,然後在程式啟動時被複制到RAM中時,這兩個地址可能會不同(這種技術常用於在基於ROM的系統中初始化全域性變數)。在這種情況下,ROM地址是LMA,而RAM地址是VMA。

你可以使用帶有-h選項的objdump程式檢視目標檔案中的節。

每個目標檔案還有一個符號列表,稱為符號表。符號可以是已定義的或未定義的。每個符號都有一個名稱,每個已定義符號都有一個地址及其他資訊。如果你將一個C或C++程式編譯成目標檔案,那麼對於每個已定義的函式和全域性或靜態變數,你都會得到一個已定義符號。每個在輸入檔案中被引用的未定義函式或全域性變數將成為一個未定義符號。

你可以使用nm程式或帶有-t選項的objdump程式檢視目標檔案中的符號。

ld 檔案的作用

  1. 指定記憶體佈局:定義程式在記憶體中的佈局,包括程式碼段、資料段、堆疊等的位置和大小。
  2. 設定節地址:控制各個節(如 .text.data.bss 等)的起始地址和排列順序。
  3. 定義符號:可以定義新的符號或重定義已有符號的地址。
  4. 合併和排列節:控制不同目標檔案中的節如何合併和排列。
  5. 設定入口點:指定程式的入口點,即從哪裡開始執行。

ld 檔案的產生

手動編寫

開發者可以手動編寫 ld 連結指令碼檔案,根據需要定義連結過程的詳細資訊。以下是一個簡單的 ld 檔案示例:

SECTIONS
{
  /* 定義記憶體佈局 */
  . = 0x1000; /* 設定起始地址 */
  .text : { *(.text) } /* 將所有 .text 節合併到 .text 段 */
  . = 0x8000;
  .data : { *(.data) }
  .bss : { *(.bss) }
}

自動生成

在一些複雜的專案中,連結指令碼可能由構建系統(如 CMake)或構建工具鏈自動生成。這種情況下,開發者通常會編寫模板或規則,然後由構建系統根據具體的構建配置生成最終的 ld 檔案。

例如,在使用 CMake 時,開發者可以透過 CMakeLists.txt 檔案配置連結器指令碼的生成:

set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/linker.ld)
add_executable(my_program main.c)
target_link_libraries(my_program ${LINKER_SCRIPT})

使用 ld 檔案

當編寫或生成了 ld 檔案後,連結器需要知道使用該檔案進行連結。通常透過命令列引數指定連結指令碼檔案:

gcc -o my_program main.o -T linker.ld

在這條命令中,-T linker.ld 命令列引數告訴 GCC 使用 linker.ld 檔案作為連結指令碼。

基本表示式

  1. 常量表示式:

    • 直接使用數值,例如:0x10004096
  2. 符號:

    • 符號可以是目標檔案中的符號或在ld指令碼中定義的符號。例如:_start_end
  3. 算術運算:

    • 可以對數值和符號進行算術運算,例如:_start + 0x1000SIZEOF(.text) + 4
  4. 邏輯運算和比較:

    • 可以進行邏輯運算和比較,例如:(SIZEOF(.text) > 1024) ? 0x1000 : 0x2000

特殊函式和運算子

  1. ALIGN:

    • 用於對齊地址,例如:ALIGN(0x1000)表示將當前地址對齊到0x1000的倍數。
  2. SIZEOF:

    • 返回某個節的大小,例如:SIZEOF(.text)
  3. ADDR:

    • 返回某個節的起始地址,例如:ADDR(.data)
  4. NEXT:

    • 返回當前地址,並將其對齊到指定的邊界,例如:NEXT(0x100)

例子

以下是一個簡單的ld指令碼示例,展示瞭如何使用這些表示式:

SECTIONS
{
  . = 0x1000;                 /* 設定連結起始地址 */
  .text : {
    *(.text)                  /* 將所有輸入檔案中的.text節合併到輸出檔案的.text節中 */
  }
  . = ALIGN(0x1000);          /* 對齊到0x1000位元組邊界 */
  .data : {
    *(.data)                  /* 將所有輸入檔案中的.data節合併到輸出檔案的.data節中 */
  }
  .bss : {
    *(.bss)                   /* 將所有輸入檔案中的.bss節合併到輸出檔案的.bss節中 */
  }
}

ENTRY(_start)                 /* 設定程式入口點 */

二. LD的命令

GNU 連結器 ld 支援多種命令和指令,這些命令和指令用於控制連結過程的各個方面,包括記憶體佈局、節的排列、符號定義等。以下是一些常用的 ld 連結指令碼命令和指令:

2.1 SECTIONS命令

在 GNU 連結器 (ld) 中,SECTIONS 命令是連結指令碼中最重要的部分之一。它定義了輸出檔案的記憶體佈局,並指定各個輸入檔案的哪些部分應該放在哪裡。透過 SECTIONS 命令,您可以精確控制程式的記憶體對映,這是嵌入式系統開發中必不可少的功能。

SECTIONS 基本語法

SECTIONS
{
  ...
}

SECTIONS 命令的花括號內,您可以定義多個段(sections),每個段描述了輸出檔案的一部分的佈局和內容。

段定義

每個段的定義通常包括段名、段屬性、段對齊方式、段內容和段放置規則。例如:

SECTIONS
{
  .text : ALIGN(4)
  {
    *(.text)
  } > FLASH

  .data : ALIGN(4)
  {
    *(.data)
    *(.rodata)
  } > RAM AT > FLASH

  .bss : ALIGN(4)
  {
    __bss_start__ = .;
    *(.bss)
    *(COMMON)
    __bss_end__ = .;
  } > RAM
}

詳細解釋

段名
.text :
  • .text 是段的名稱。通常的段名有 .text(程式碼段)、.data(初始化資料段)、.bss(未初始化資料段)等。
段屬性和對齊
ALIGN(4)
  • ALIGN(4) 指定段的開始地址需要對齊到 4 位元組邊界。
段內容
{
  *(.text)
}
  • 花括號內指定了段的內容。* 表示匹配所有輸入檔案的 .text 段,並將它們包含在這個輸出檔案的 .text 段中。
儲存器區域
> FLASH
  • > FLASH 指定把段放在 FLASH 儲存區域。儲存區域通常在連結指令碼的 MEMORY 命令中定義。
AT 指令
AT > FLASH
  • AT 指令指定段的載入地址。例如,在 RAM 中執行的 .data 段可以從 FLASH 中載入。

綜合示例解釋

SECTIONS
{
  .text : ALIGN(4)
  {
    *(.text)
  } > FLASH

  .data : ALIGN(4)
  {
    *(.data)
    *(.rodata)
  } > RAM AT > FLASH

  .bss : ALIGN(4)
  {
    __bss_start__ = .;
    *(.bss)
    *(COMMON)
    __bss_end__ = .;
  } > RAM
}
  1. .text

    • 名稱:.text
    • 對齊:4 位元組
    • 內容:所有輸入檔案的 .text
    • 儲存器區域:FLASH
  2. .data

    • 名稱:.data
    • 對齊:4 位元組
    • 內容:所有輸入檔案的 .data.rodata
    • 儲存器區域:RAM
    • 載入地址:FLASH
  3. .bss

    • 名稱:.bss
    • 對齊:4 位元組
    • 內容:
      • __bss_start__ 符號表示 .bss 段開始地址
      • 所有輸入檔案的 .bss 段和 COMMON
      • __bss_end__ 符號表示 .bss 段結束地址
    • 儲存器區域:RAM

常用命令和表示式

符號定義

在段內可以定義符號,這些符號可以用於表示地址和大小。例如:

__start_text = .;
  • . 表示當前地址計數器。
SIZEOFADDR
  • SIZEOF(section):返回段的大小。
  • ADDR(section):返回段的地址。

例如:

__data_size = SIZEOF(.data);
__data_start = ADDR(.data);
FILL

用於指定段的填充模式,例如:

.text : FILL(0x90)
{
  *(.text)
} > FLASH
  • FILL(0x90).text 段填充為 0x90

2.2 賦值命令

好的,在 GNU 連結器 (ld) 的連結指令碼中,賦值命令用於定義和控制符號的值。下面分為幾個部分詳細介紹賦值語句、HIDDENPROVIDEPROVIDE_HIDDEN

1. 賦值語句

賦值語句是連結指令碼中最基本的操作之一,用於給符號賦值。

語法
symbol = expression;
  • symbol 是符號的名稱。
  • expression 是要賦給符號的值,可以是常數、地址、當前地址計數器(.),或其他符號的值。
示例
SECTIONS
{
  .text : {
    start_of_text = .;
    *(.text)
    end_of_text = .;
  } > FLASH
}

在這個例子中:

  • start_of_text 被賦值為 .text 段的開始地址。
  • end_of_text 被賦值為 .text 段的結束地址。

2. HIDDEN

HIDDEN 關鍵字用於將符號的可見性設定為隱藏,這樣符號就只在本模組內可見,不會暴露給外部模組。這在控制符號範圍和避免命名衝突時非常有用。

語法
HIDDEN(symbol = expression);
示例
SECTIONS
{
  .text : {
    HIDDEN(hidden_symbol = .);
    *(.text)
    HIDDEN(hidden_end = .);
  } > FLASH
}

在這個例子中:

  • hidden_symbolhidden_end 是隱藏的符號,只在本模組內可見。

3. PROVIDE

PROVIDE 關鍵字用於有條件地定義符號。如果符號在其他地方已經定義,則 PROVIDE 不會重新定義它。

語法
PROVIDE(symbol = expression);
  • symbol 是符號的名稱。
  • expression 是要賦給符號的值。
示例
SECTIONS
{
  .text : {
    PROVIDE(__start_of_text = .);
    *(.text)
    PROVIDE(__end_of_text = .);
  } > FLASH
}

在這個例子中:

  • __start_of_text__end_of_text 將在沒有其他定義的情況下被賦值。

4. PROVIDE_HIDDEN

PROVIDE_HIDDEN 結合了 PROVIDEHIDDEN 的功能,用於有條件地定義一個隱藏的符號。如果符號在其他地方已經定義,則不會重新定義它。

語法
PROVIDE_HIDDEN(symbol = expression);
  • symbol 是符號的名稱。
  • expression 是要賦給符號的值。

示例

SECTIONS
{
  .text : {
    PROVIDE_HIDDEN(__hidden_start = .);
    *(.text)
    PROVIDE_HIDDEN(__hidden_end = .);
  } > FLASH
}

在這個例子中:

  • __hidden_start__hidden_end 將在沒有其他定義的情況下被賦值,並且這些符號是隱藏的,只在本模組內可見。

2.3 MEMORY 命令

在 GNU 連結器 (ld) 的連結指令碼中,MEMORY 命令用於定義目標系統的記憶體佈局。它能夠指定不同記憶體區域的大小、起始地址和屬性。這對於嵌入式系統等對記憶體佈局有嚴格要求的應用非常重要。

MEMORY 命令的基本語法

MEMORY
{
  name (attr) : ORIGIN = origin, LENGTH = length
  ...
}
  • name 是記憶體區域的名稱。
  • (attr) 是可選的記憶體區域屬性,用於指定該區域的特性(如只讀、只寫等)。
  • ORIGIN 是記憶體區域的起始地址。
  • LENGTH 是記憶體區域的長度。

記憶體區域的屬性

記憶體區域的屬性用括號括起來,可以包含以下字元:

  • r:表示該區域可讀。
  • w:表示該區域可寫。
  • x:表示該區域可執行。

示例

MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

在這個示例中:

  • FLASH 區域從地址 0x08000000 開始,長度為 256K,該區域是隻讀且可執行的(rx)。
  • RAM 區域從地址 0x20000000 開始,長度為 64K,該區域是可讀、可寫和可執行的(rwx)。

使用 MEMORY 定義的區域

定義了記憶體區域之後,可以在 SECTIONS 命令中使用這些區域來分配段。

示例

MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS
{
  .text : {
    *(.text)
  } > FLASH

  .data : {
    *(.data)
  } > RAM

  .bss : {
    *(.bss)
  } > RAM
}

在這個示例中:

  • .text 段被放置在 FLASH 區域。
  • .data.bss 段被放置在 RAM 區域。

2.4 其它命令

節命令

  1. . (dot)

    • 當前地址位置,可以用來設定節的起始地址。
    • 示例:
      SECTIONS
      {
        . = 0x1000;
        .text : { *(.text) }
      }
      
  2. KEEP

    • 確保指定的節或符號不會被垃圾回收。
    • 示例:
      SECTIONS
      {
        .text : { KEEP(*(.init)) *(.text) }
      }
      
  3. ALIGN

    • 對齊當前地址。
    • 示例:
      . = ALIGN(4);
      
  4. FILL

    • 用於填充未定義的空間。
    • 示例:
      .text : { *(.text) } FILL(0x90)
      

符號命令

  1. ASSERT

    • 檢查條件是否滿足,如果不滿足則報錯。
    • 示例:
      ASSERT(_end <= 0x20008000, "Not enough RAM");
      
  2. EXTERN

    • 宣告外部符號,確保連結器不會刪除它們。
    • 示例:
      EXTERN(_start)
      

檔案命令

  1. INCLUDE

    • 包含另一個連結指令碼檔案。
    • 示例:
      INCLUDE "common.ld"
      
  2. SEARCH_DIR

    • 新增庫檔案搜尋路徑。
    • 示例:
      SEARCH_DIR(/usr/local/lib)
      

其他命令

  1. PHDRS

    • 用於定義程式頭表(Program Header Table),通常用於生成 ELF 檔案。
    • 示例:
      PHDRS
      {
        text PT_LOAD FILEHDR PHDRS;
        data PT_LOAD;
      }
      
  2. VERSION

    • 用於定義版本指令碼,管理符號的可見性。
    • 示例:
      VERSION
      {
        GLOBAL {
          main;
          foo;
        };
        LOCAL {
          *;
        };
      }
      
  3. LOADADDR

    • 用於獲取特定節的載入地址。載入地址是指該節在執行檔案中被載入到記憶體中的地址。
    • 示例:
      LOADADDR(section)
      

以下是一些更加高階和特定用途的命令和指令,繼續補充前面的列表:

高階節命令

  1. SORT

    • 按名稱或地址對節進行排序。
    • 示例:
      .text : { SORT(*)(.text) }
      
  2. SORT_BY_NAME

    • 按名稱對節進行排序。
    • 示例:
      .text : { SORT_BY_NAME(*)(.text) }
      
  3. SORT_BY_ALIGNMENT

    • 按對齊方式對節進行排序。
    • 示例:
      .text : { SORT_BY_ALIGNMENT(*)(.text) }
      
  4. BLOCK

    • 用於對齊整個節組。
    • 示例:
      .text : { BLOCK(4) { *(.text) } }
      

高階符號命令

  1. DEFINED

    • 檢查符號是否已定義。
    • 示例:
      SECTIONS
      {
        .text : { *(.text) }
        .data : {
          _data_start = .;
          *(.data)
          _data_end = .;
        }
        ASSERT(DEFINED(_data_start), "Data start not defined");
      }
      
  2. PROVIDE_HIDDEN

    • 定義一個隱藏符號,僅在符號未被定義時生效。
    • 示例:
      PROVIDE_HIDDEN(__stack_size = 0x2000);
      

高階檔案命令

  1. GROUP

    • 將多個檔案視為一個檔案進行處理。
    • 示例:
      GROUP(lib1.a lib2.a)
      
  2. INPUT

    • 指定輸入檔案。
    • 示例:
      INPUT(main.o utils.o)
      
  3. OUTPUT

    • 指定輸出檔名。
    • 示例:
      OUTPUT("output.elf")
      

高階記憶體命令

  1. REGION_ALIAS
    • 為記憶體區域建立別名。
    • 示例:
      MEMORY
      {
        RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
      }
      REGION_ALIAS("RAM_ALIAS", RAM);
      

高階除錯命令

  1. MAP
    • 生成連結對映檔案,顯示連結過程的詳細資訊。
    • 示例:
      OUTPUT_FORMAT("elf32-littlearm")
      OUTPUT_ARCH(arm)
      ENTRY(_start)
      SECTIONS
      {
        .text : { *(.text) }
        .data : { *(.data) }
        .bss : { *(.bss) }
      }
      MAP("output.map")
      

高階控制命令

  1. FORCE_COMMON_ALLOCATION

    • 強制分配 COMMON 符號。
    • 示例:
      FORCE_COMMON_ALLOCATION
      
  2. NOLOAD

    • 指定節不應載入到記憶體中。
    • 示例:
      SECTIONS
      {
        .bss (NOLOAD) : { *(.bss) }
      }
      
  3. OVERLAY

    • 定義重疊區域,用於共享記憶體。
    • 示例:
      SECTIONS
      {
        .overlay1 : {
          *(.text1)
        } > RAM
        .overlay2 : {
          *(.text2)
        } > RAM
        .overlay3 : {
          *(.text3)
        } > RAM
      }
      

動態連結命令

  1. DYNAMIC

    • 定義動態節。
    • 示例:
      DYNAMIC
      {
        .dynamic : { *(.dynamic) }
      }
      
  2. VERSION

    • 定義動態連結版本。
    • 示例:
      VERSION
      {
        v1.0 {
          global: foo;
          local: *;
        };
      }
      

完整的 GNU ld 連結指令碼命令和指令可以參考 GNU Binutils 文件