|
我們在用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’內存區域。
MEMORY
{
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. 段描述
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)
5. 一些內建函數
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段的字節數。如果段還沒被分配就是用函數求值,將會產生錯誤。
|
|