My code works, I don’t know why.

國王的耳朵是驢耳朵

GNU LD 手冊略讀 (2): Chapter 3.6 SETCIONS

| Comments

上一篇 下一篇 回總目錄

本篇目錄

SETCIONS命令

其實一開始是為了看懂這個命令才會想看linker script的。如果接觸過很小型的Embedded OS就會發現很多都是自幹linker script;而這些scripts主要的描述命令就是SETCIONS命令。

好了,廢話少說,進入主題。SETCIONS命令命令的功用是

  • 告訴linker怎麼把輸入object檔案中的SETCIONS命令對應到輸出object檔案中的sections
  • 告訴loader object檔案中的sections要放到記憶體那些地方

SECTIONS命令長這樣子:

1
2
3
4
5
6
SECTIONS
{
  sections-command
  sections-command
  ...
}

望文生義地猜測可以這樣理解: 輸出object有一些大方向的規範,並且分為不同的section,每個section有他自己的規範。

sections-command可以分為下面幾種功能

要注意的事,如果你自幹的linker script沒有描述輸出object檔案的setcion的話,linker會

  • 讀輸入object檔案section時,如果該section第一次出現,就在輸出object檔案中加入同樣名稱的section,直到處理完所有的輸入object檔案
  • 第一個吃到的輸入object檔案section將當作位址0的起始點

輸出object檔案的section描述

輸出object檔案的section描述格式
1
2
3
4
5
6
7
8
9
10
section [address] [(type)] :
  [AT(lma)]
  [ALIGN(section_align) | ALIGN_WITH_INPUT]
  [SUBALIGN(subsection_align)]
  [constraint]
  {
      output-section-command
      output-section-command
      ...
  } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

其中output-section-command的功能有

  • 設定symbol的值
  • 描述輸入object檔案中的section要怎麼放到輸出object檔案的setcion
  • 輸出object檔案的setcion的資料存放格式如alignment等
  • 其他

這邊很多術語需要先搞清楚,先列出來,希望之後可以看到解答

  • type
  • region
  • AT(lma)
  • lma_region

輸出object檔案的section 命名

  • 必須符合你要輸出object檔案binary format規定。

輸出object檔案section 命令: address欄位

address是section的一個optional欄位,使用的記憶體空間為VMA。如果沒有指定的話,linker會依下面的方式設定輸出object檔案section 的VMA。該VMA會遵循section 的alignment規範。

  • 有設定region的話就從region內剩餘空間開始位址
  • 有使用MEMORY命令定義硬體記憶區塊的話,從定義的區塊中挑第一個符合SECTION的區塊。再將address設成該區塊內剩餘空間開始位址
  • 以上皆非的情況下,位址設成locale counter

address欄位因為可以使用exression所以可能有下面的陷阱

  • .text . : { *(.text) }
  • .text : { *(.text) } 這兩個差一個.,意義就差很多。沒有.那個,表示沒有設定address,所以就是設成locale counter,並且linker會保證alignment。而有.的就表示hardcode成locale counter,所以有可能會有alignment的問題。

另外一點要注意的設定後locale counter也會跟著改變。

輸入object檔案的section描述

這部份可以說是整個output-section-command的重點,目的是告訴linker讀取輸入object檔案後,怎麼把這些檔案裏面的section複製到輸出object檔案裏面適當地section。

輸入object檔案的section描述可以被分為下面幾個部份

輸入object檔案的section 基礎概念

格式為檔案(section1 section2 ...),檔案支援萬用字元

所以常看到的*(.text)的意思是:所有輸入object檔案裏面的.text section。

指定多個section的方式有兩種

  • *(.sec1 .sec2):如果輸入object有兩個檔案的話,輸出object檔案裏面section會變成

  • *(.sec1) *(.sec2): 如果輸入object有兩個檔案的話,輸出object檔案裏面section會變成 test

你也可以根據flag區分object檔案的section,範例如下

1
2
3
4
SECTIONS {
  .text : { INPUT_SECTION_FLAGS (SHF_MERGE & SHF_STRINGS) *(.text) }
  .text2 :  { INPUT_SECTION_FLAGS (!SHF_WRITE) *(.text) }
}

望文生義可以看到上面的規範就是

  • 所有輸入object檔案的.textsection flag有SHF_MERGE 和 SHF_STRINGS的,請放在輸出object檔案的.text section
  • 所有輸入object檔案的.textsection flag沒有SHF_WRITE的,請放在輸出object檔案的.text2 section

你如果對於範例中的flag有興趣,可以看這邊, 這邊,還有這邊。我目前還不想看就是了。

另外指定輸入object檔案部份,除了指定單獨的輸入object檔案,還可以指定archieve (如libwen.a, libc.a)裏面的object檔案,用法如下 archive:file,隨便猜一個範例libc.a:fprintf.o

輸入object檔案的section 語法的萬用字元

支援 *:任何長度的任何字元 ?:單一任何字元 []:單一字元有效的範圍如[a-z]指小寫英文字母 \:接下來的字元不是萬用字元,如\*

由於linker複製section的方式是多個條件滿足的話,選第一個條件滿足就處理,所以配合萬用字元可能會產生意想不到的錯誤,範例如下

1
2
.data : { *(.data) }
.data1 : { data.o(.data) }

由於複製section的方式是第一個條件滿足就處理,所以會造成data.o的.data section放字輸出object檔案的.data section而不是.data1 section。手冊提供了建議處理方式,有興趣的可以參考

輸入object檔案的COMMOM section

  • 這邊有提到common symbol存在的原因。手冊中更進一步的提到在輸出object檔案時的命令大概是這樣:
1
.bss { *(.bss) *(COMMON) }

也就是說,最後沒特別狀況,就把輸入object 檔案的COMMON section放在.bss section。

KEEP指令

  • KEEP(要保留的section):因為linker有garbage collection,如果要保證section不會被回收,可以用該指令。

輸入object檔案放到輸出object檔案範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SECTIONS {
  outputa 0x10000 :
  {
      all.o
      foo.o (.input1)
  }
  outputb :
  {
      foo.o (.input2)
      foo1.o (.input1)
  }
  outputc :
  {
      *(.input1)
      *(.input2)
  }
}

以圖示就是

輸出object檔案內指定固定資料長度

  • 長度單位命令(expression)
    • 長度單位命令
      • BYTE:1 byte
    • SHORT:2 bytes
    • LONG:4 bytes
    • QUAD:8 byte

以下的命令將會佔 5 bytes,第一個byte後面4個bytes將用來存放addr (如果我英文沒看錯的話,原文是store the byte 1 followed by the four byte value of the symbol addr':)。 BYTE(1) LONG(addr)`

關於64-bit的目前沒心情看,跳過。

至於endian的部份,如果輸出的object檔案有規範,則依該規範存放,否則則遵守第一個讀入的輸入object檔案。

  • FILL(expression):section內沒使用的空間將被填入expression計算後的數字。同樣效果的命令是[=fillexp],忘記這是啥嗎?我也忘了,所以回去找了一下

3.6.6看不懂,跳過。

輸出object檔案捨棄的section

為什麼要丟掉?原因是在設定輸出section的script有提到特定的section,但是link完畢後發現所有輸入object檔案都沒有該section的symbol。最後就是把這些section丟掉。

另外一個情況是輸入object檔案有/DISCARD/既然就說要丟了就恭敬不如從命了。

輸出object檔案section其他屬性

還記得前面的格式嘛?再複習一下:

輸出object檔案的section描述格式
1
2
3
4
5
6
7
8
9
10
section [address] [(type)] :
  [AT(lma)]
  [ALIGN(section_align) | ALIGN_WITH_INPUT]
  [SUBALIGN(subsection_align)]
  [constraint]
  {
      output-section-command
      output-section-command
      ...
  } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

前面篇幅已經說明了sectionaddress,以及output-section-command等語法和命令,我們接著要介紹其他部份如下

輸出object檔案 Section Type

有支援

  • NOLOAD
    • 當程式執行的時候不載入到記憶體(想像ROM或是NOR FLASH)
  • DSECT
  • COPY
  • INFO
  • OVERLAY
    • 上面四個是為了往前相容保留的type,基本上很少用了。用途都相同,指定該區段不可以分配記憶體。不過我本身不懂什麼情況下不要分配記憶體就是了。

基本上type繼承輸入object中的type,不過你要硬上就是在輸出object檔案描述,範例如下。該範例顯示ROM 區段起始位址為0,並且在該section執行程式不要載入到記憶體。

1
2
3
4
SECTIONS {
  ROM 0 (NOLOAD) : { ... }
  ...
}

輸出object檔案 Section LMA

前情回顧

設定輸出object檔案的VMA是在address欄位中指定。請比對section描述格式address

LMA就是section描述格式AT(lma)AT>lma_region這兩個部份了。這兩個指令是optional的。他們的差別是:

  • AT(lma)中間的lma是透過expression算出來的lma位址
  • AT>lma_region是指定MEMORY裏面描述的region

如果你的section沒有指定LMA的話,linker會使用下面的規則決定LMA

  • address欄位中指定VMA,則LMA = VMA
  • section為allocatable,則LMA = VMA
  • 有設定region的情況在滿足下面的條件下,把VMA和LMA的差距會被設成該region裏面最後一個section中VMA和LMA的差距。
    • section滿足region條件(三小條件?)
    • 該region已經有最少一個section
  • 沒有設定region的情況在找不到相容的region,linker會指令預設包含所有memory space的region,從裏面挑一個section,把VMA和LMA的差距會被設成該region裏面最後一個section中VMA和LMA的差距。 (三小?為什麼要這樣做?)
  • 找不到合適的region放section的話,就閉著眼睛把LMA = VMA吧。

來點範例,這是一個嵌入式系統,假設所有的資料都放在唯讀記憶體中。那麼會發生什麼事呢?那就是你的i++就GG了,所以要把變數部份還有其他需要寫入的部份放在RAM中,所以這個script顯示了

  • VMA的0x1000的位址放程式碼
  • VMA的0x2000放有初始化的全域變數,而這些初始值從那邊搬到記憶體呢來呢?就是LMA描述的東西,望文生義可以知道是接在.text之後的資料。
  • VMA的0x3000就是放未初始化的全域變數
1
2
3
4
5
6
7
8
9
SECTIONS
{
      .text 0x1000 : { *(.text) _etext = . ; }
      .mdata 0x2000 :
          AT ( ADDR (.text) + SIZEOF (.text) )
          { _data = . ; *(.data); _edata = . ;  }
      .bss 0x3000 :
          { _bstart = . ;  *(.bss) *(COMMON) ; _bend = . ;}
}

當然事情沒那麼簡單,這邊只有講layout。在沒有OS幫你搞定的時候什麼事都要自己來,所以你還要自己把有初始化的全域變數一個一個搬到RAM裏面如下。請仔細比對變數和script的symbol。另外如果有興趣看CMSIS(Cortex Microcontroller Software Interface Standard)的source code也可以看到類似的行為。

1
2
3
4
5
6
7
8
9
10
11
extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;

/* ROM has data at end of text; copy it.  */
while (dst < &_edata)
  *dst++ = *src++;

/* Zero bss.  */
for (dst = &_bstart; dst< &_bend; dst++)
  *dst = 0;

強制輸出object檔案的 Alignment

請使用ALIGN,或是使用ALIGN_WITH_INPUT將讀入的object檔案中的section設定成你要的alignment。

強制輸入object檔案的 Alignment

請使用SUBALIGN 去指定輸入object檔案單一個section的alignment。

輸出object檔案 Section 限制

  • ONLY_IF_RO
    • 當輸入object檔案符合條件的section為唯讀才產生你要的輸出object檔案section
  • ONLY_IF_RW
    • 當輸入object檔案符合條件的section為可讀寫才產生你要的輸出object檔案section

輸出object檔案 Section Region

使用>MEMORY_指令_宣告的region

範例,把.text放ROM section,該section位址是在硬體rom記憶體區塊。

1
2
MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }

輸出object檔案 Section Phdr

PHDR 是ELF的program header縮寫,又稱為segment。當ELF loader載入ELF執行檔的時候,會看這些segment決定要如何把讀入的檔案放在記憶體中,這部份和ABI有關係,按下不表,等我那天心情好再來看ELF和ABI。

section描述格式phdr的用法是

  • 宣告一個phdr
  • 指令特定的section屬於該phdr

範例如下

1
2
PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }

指定輸出object檔案 Section 填空的資料

前面FILL講到指令填空的資料。而section描述格式=fillexp也有相同效果,範例如下

1
SECTIONS { .text : { *(.text) } =0x90909090 }

OVERLAY命令

Overlay是一種在記憶體小於執行檔案時的技巧。其基本概念就是

  • 把程式切成不同模組
  • 載入單個模組到記憶體並執行,當程式行為Z要另外一個模組的話,就釋放目前模組,再載入新的模組到記憶體並執行。

對應到linker script就會格式這樣

OVERLAY命令格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OVERLAY [start] : [NOCROSSREFS] [AT ( ldaddr )]
{
  secname1
  {
      output-section-command
      output-section-command
  ...
  } [:phdr...] [=fill]
  secname2
  {
      output-section-command
      output-section-command
      ...
  } [:phdr...] [=fill]
...
} [>region] [:phdr...] [=fill]

OVERLAY命令中除了OVERLAYsection 名稱以外其他都是optional。另外要注意的是OVERLAY命令不允許region和address的描述。而在OVERLAY最後面的資料固定為OVERLAY起始位址 + 最大section的size

由於OVERLAY就是動態切換並執行不同section,所以在VMA的位址會固定。這表示所有的section的VMA會相同。為了方便,linker會把所有OVERLAY中的section串接成連續的空間。

OVERLAY用法如下

  • linker script設定OVERLAY
  • 程式語言視情況需要切換時「人肉」搬移OVERLAY裏面的section到記憶體

「人肉」搬移表示我們需要

  • section 起始位址
  • section 結束位址

這部份linker會自動幫我們加入symbol,規則如下,很容易望文生義所以就不解釋了。

  • __load_start_section_名稱
  • __load_stop_section_名稱

那麼現在看一下手冊上面的範例

1
2
3
4
5
OVERLAY 0x1000 : AT (0x4000)
{
  .text0 { o1/*.o(.text) }
  .text1 { o2/*.o(.text) }
}

還記得addressAT命令嗎?一個是指定VMA另外一個是指定LMA。所以上面的設定白話文就是

  • 我要一個overlay,從0x4000載入到0x1000的記憶體內
  • 這個overlay有.text0.text1兩個section
  • .text0裏面放的是o1目錄下所有object檔案中的.text
  • .text1裏面放的是o2目錄下所有object檔案中的.text

那麼人肉搬移要怎麼處理呢?手冊列出如下

1
2
3
extern char __load_start_text1, __load_stop_text1;
memcpy ((char *) 0x1000, &__load_start_text1,
          &__load_stop_text1 - &__load_start_text1);

可以看到,我們說要從LMA搬到VMA,LMA的位址就由symbol內容提供。

另外手冊這個section我跳過一些東西,有興趣的朋友可以去超級比一比。

上一篇 下一篇 回總目錄

Comments