1. 程式人生 > 其它 >淺談RISC-V GCC之:連結指令碼學習筆記(一)

淺談RISC-V GCC之:連結指令碼學習筆記(一)

我們在用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段的位元組數。如果段還沒被分配就是用函式求值,將會產生錯誤。