我們在用RISC-V GCC做嵌入式開發的時候,免不了要和啟動檔案和連結檔案等打交道,本篇文章記錄了一些連結腳本相關的學習筆記。
1.基礎概念
連結腳本的主要作用是描述輸入檔案中的段應當如何映射到輸出檔案中,并控制輸出檔案的記憶體布局。多數連結腳本都執行類似功能。但是,如果需要,連結腳本也可以使用下面所描述的指令指揮連結器進行很多其他操作。
連結器通常使用一個連結腳本。如果沒有為其提供一個,連結器将會使用預設的編譯在連結器執行檔案内部的腳本。可以使用指令’–verbose’顯示預設的連結腳本。
為了描述連結腳本語言,我們需要定義一些基本概念和詞彙。
連結器将許多輸入檔案組合成一個輸出檔案。輸出檔案和每個輸入檔案都有一個特定的已知格式成為目标檔案格式。每個檔案都被稱為目标檔案。輸出檔案通常叫做可執行檔案,但我們仍将其稱為目标檔案。每個目标檔案在其他東西之間,都有一個段清單。有時把輸入檔案的段稱作輸入段,類似的,輸出檔案的段稱作輸出段。
每個目标檔案中的段都有名字和大小。多數段還有一個相關的資料塊,稱為 段内容。一個段可能被标記為可加載,表示當輸出檔案運作時,段内容需要先加載到記憶體中。一個沒有内容的段可能是可配置設定段,即在記憶體中留出一段空間(有時還需要清零)。一個即不是加載又不是可配置設定的段,通常含有一些調試資訊。
每個加載或可配置設定輸出段有兩個位址。第一個位址為VMA,或者叫做虛位址。這是當輸出檔案運作時段所擁有的位址。第二個位址是LMA,或者叫加載記憶體位址。這是段将會被加載的位址。一個它們會産生差別的例子是,當一個資料段加載到ROM, 此後在程式啟動時被複制到RAM中(這個技術通常被用來初始化全局變量)。此種情況下,ROM使用LMA位址,RAM使用VMA位址。
如果想檢視目标檔案中的段,可以用objdump程式的’-h’選項。
每個目标檔案還有一個符号清單,稱為符号清單。一個符号可能是被定義的或者未定義的。每個符号都有一個名字,且所有已定義的符号在其他資訊中間都有一個位址。如果将一個c或者c++程式編譯成目标檔案,會将所有定義過的函數和全局變量以及靜态變量作為已定義符号。所有輸入檔案引用的未定義的函數或者全局變量會成為未定義符号。
2.常用關鍵詞與用法
ENTRY(symbol) 用來指定程式執行的入口點
MEMORY 記憶體配置設定指令
SECTIONS 段指令 描述輸出檔案的記憶體和布局
.text 程式代碼段
.rodata 隻讀資料
.data 可讀寫且需要初始化的資料
.bss 可讀寫的清零初始化資料
ASSERT 斷言
PROVIDE(symbol=expression) 定義一個符号
AT 後跟MEMORY定義的記憶體區域或者位址
ALIGN 位元組對齊
3 . MEMORY
連結器預設的設定允許配置設定所有可用的記憶體。你通過MEMORY指令可以重載這些。
MEMORY指令描述了一個記憶體塊在目标中的位置和大小。你可以使用它描述一個可能會在連結器中使用的記憶體區域,以及那些必須避免使用的記憶體區域。此後你可以把段放到特定的記憶體區域裡。連結器将會基于記憶體區域設定段位址,如果區域趨于飽和将會産生警告資訊。連結器不會為了把段更好的放入記憶體區域而打亂段的順序。
一個連結腳本可能含有許多MEMORY指令,但是,所有定義的記憶體塊都被當作他們是在一個MEMORY指令中定義的一樣。MEMORY的文法是:
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
...
}
name是連結腳本用來引用記憶體區域的名字。區域名在連結腳本外部沒有任何意義。區域名被存儲在一個獨立的名字空間,且不會與符号名,檔案名,或者段名起沖突。每個記憶體區域必須在MEMORY指令中有一個不同的名字。但是你此後可以使用REGION_ALIAS指令為已存在的記憶體區域添加别名。
attr字元是一個可選的屬性清單,用來決定是否讓一個腳本中沒有顯式指定映射的輸入段使用一個特定的記憶體區域。就像SECTIONS中進行過的說明,如果你不為一個輸入段指定一個輸出段,連結器将會建立一個與輸入段名字相同的輸出段。如果你定義了區域屬性,連結器會使用他們來決定建立的輸出段存放的記憶體區域。
attr字元串隻能使用下面的字元組成:
‘R’隻讀段
‘W’讀寫段
‘X’可執行段
‘A’可配置設定段
‘I’已初始化段
‘L’類似于’I’
‘!’反轉其後面的所有屬性
如果一個未映射段比對了上面除’!’之外的一個屬性,它就會被放入該記憶體區域。’!’屬性對該測試取反,是以隻有當它不比對上面列出的行何屬性時,一個未映射段才會被放入到記憶體區域。
origin是一個數字表達式,代表了記憶體區域的起始位址。表達式必須等價于一個常數并且不能含有任何符号。關鍵字ORIGIN縮短為org或者o(但不能寫成ORG)。
len是一個表達式用來給出記憶體區域中的位元組數大小。類似于origin表達式,表達式必須隻能為數字的切必須求值為常數。關鍵字LENGTH可以被縮寫為len或者l。
下面的例子裡,我們制定了有兩個可配置設定的記憶體區域:一個從’0’開始有256k位元組,另一個從’0x40000000’開始,由4兆位元組。連結器把所有沒有顯式映射到一個記憶體區域的段放到’rom’記憶體區域内,段可以是隻讀的或者可執行的。連結器将把其它沒顯式指定記憶體區域映射的段放到’ram’記憶體區域。
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
一旦你定義了一個記憶體區域,你可以使用’>region’輸出段屬性指引連結器把特殊輸出段放到該記憶體區域。例如,如果你擁有一個記憶體區域名為’mem’,你可以在輸出段定義中使用’>mem’。參考Output Section Region。如果沒有給輸出段指出位址,連結器将會把位址放到最先符合要求的記憶體區域中的可用位址。如果指引給一個記憶體區域的組合輸出段比區域還大,連結器将會送出錯誤。
可以通過ORIGIN(memory)和LENGTH(memory)函數獲得記憶體區域的起始位址以及長度:
_fstack = ORIGIN(ram) + LENGTH(ram) - 4;
- 段描述
4.1輸出段
完整的輸出段描述如下
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] [,]
位址(address)是一個輸出段VMA(虛位址)的表達式。此位址為可選參數,但如果給出了位址,則輸出位址就會被精确的設定到給定值。
如果輸出的位址沒有給定,則依照下面的嘗試選擇一個位址。此位址将會被調整到符合輸出端要求的對齊位址。輸出段的對齊要求是所有輸入節中含有的對齊要求中最嚴格的一個。
輸出段位址探索如下:
如果為段設定了記憶體區域,則段被放如該區域,并且段位址為區域中的下一個空閑位置。
如果使用MEMORY指令建立了一個記憶體區域清單,此時第一個屬性比對段的區域被選擇來加載段,段位址為區域中的下一個空閑位置。參見MEMORY。
如果沒有指定的記憶體區域,或者沒有比對段的,則輸出位址将會基于目前位置計數器的值
4.2輸入段
輸入段存在于輸出段的内容中,用來指定不同輸入段在輸出段中的位置,常見的有.text .data .rodat .bss COMMOM等,一個輸入段描述由跟随在段名稱後面括号包含的一個可選的檔案名稱清單構成。也可以使用通配符,例如
main.o(.text)或者直接(.text)
前一個代表main.o 檔案中所有.text段,後一個代表所有參與連結檔案中的.text段,當然也可以排除一些檔案
EXCLUDE_FILE (檔案名.o) (.text)
- 一些内建函數
ABSOLUTE(exp)
傳回表達式exp的絕對(非可重配置設定的,而不是非負)值。主要用來在段定義内為符号配置設定一個絕對值,通常段定義内的符号值都是相對段位址的。
ADDR(section)
傳回名為’section’的段的位址(VMA)。你的腳本必須事先未該段定義了位置。在下面的例子裡,start_of_output_1, symbol_1, symbol_2配置設定了同樣的值,除了symbol_1為與段.output1相關的值而其他兩個為絕對值:
SECTIONS { ...
.output1 :
{
start_of_output_1 = ABSOLUTE(.);
...
}
.output :
{
symbol_1 = ADDR(.output1);
symbol_2 = start_of_output_1;
}
... }
LENGTH(memory)
傳回名為memory的記憶體的長度。
MAX(exp1, exp2)
傳回exp1和exp2最大的
MIN(exp1, exp2)
傳回exp1和exp2最小的。
ORIGIN(memory)
傳回名為memory的記憶體區域的起始位址。
SIZEOF(section)
傳回名為section段的位元組數。如果段還沒被配置設定就是用函數求值,将會産生錯誤。