GCC的連接腳本--LD 學習筆記

GCC的連接腳本學習筆記

連接腳本將我整整蒙了1天零一個上午,做了很多實驗,看了人家不少例子代碼
勉強能駕馭了,讓linker按照我想要的來處理,做個筆記。

1,什麼叫輸入段,什麼叫輸出段

不知道怎麼回事,我對GCC系列的輸入和輸出兩個單詞總是進入思維死角,很簡單
就是 input section 和 output section,這裏不是說翻譯的問題,我覺得是一種
思考的方式的問題。
我的問題就是:既然叫輸入端,那輸入什麼?同理,輸出的是什麼?不知道其他人
不會不理解這個問題,我自己的話是理解了不少時間了 。
所謂的輸出段,是指生成的文件,例如 elf 中的每個段
所謂的輸入段,是指連接的時候提供LD的所有目標文件(OBJ)中的段。

2,lma 和 vma

lma = load memory address
vma = vitual memory address

如果有研究過ADS的估計有印象,那裏有個 RO BASE 和 RW BASE 和 ZI BASE,也
就是說,lma 是裝載地址,vma 是運行地址,想搞清楚這兩個問題,可以閱讀一下
《ARM學習報告(杜雲海)》作者寫的很好,將這個問題分析的很透澈。lma 和vma
只是GCC的叫法而已,其實原理是一樣的。

3,兩個基本架構
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")

OUTPUT_ARCH(arm)

一句話,照抄…因爲我們沒有修改的餘地,都是系統默認的關鍵字。第一句
指示系統可以有生成兩種格式,默認是 elf32-arm,端格式是 little endian

4,ENTRY(__ENTRY)

指定入口點,LD的手冊說,ENTRY POINT 就是程序第一條執行的指令,但是,說老
實話,我並不理解,因爲這裏跟我的理解矛盾了,首先,通常情況,系統需要一個
初始化的 STARTUP.S文件來初始化硬件,也就是 bootloader的第一階段了。那麼
很自然,入口點需要設置在這段代碼的第一條指令中,那麼正常運行的時候從第一
條指令開始運行。所以這裏設置了__ENTRY爲入口點,這個在彙編代碼中必須得先
聲明一下爲全局,才能用,否則系統找不到。例如:
.global __ENTRY
但是問題是,如果我用同樣的辦法,設置另外一個不是第一條指令的入口點,LD並
沒有報錯,但是問題來了,生成的文件和剛纔設置入口點爲 __ENTRY 的時候一模一
樣,這就蒙了,到底這個入口點是怎麼回事?
記得以前ADS的時候也碰到過 entry point的問題,下載仿真的時候確實是自動跳轉
到 entry point中運行。
我想到的可能的原因,第一,生成 elf 文件並不是能直接用在嵌入式平臺上面裸跑
的,因爲我們並沒有操作系統,我們不需要elf文件頭的那些指示信息提供給操作
系統,指示系統怎麼去加載文件,在嵌入式上面的完全沒有那個必要,只需要將實
際的代碼提取出來,直接運行就OK,也就是 objcopy的操作,所以我覺得,在裸奔
的嵌入式系統上面,entry point是沒有意義的,只需要指向整個代碼最開始的指
令就OK了。
暫時我還是不能清晰的理解這個東西。先放下。以後碰到問題再分析。

5,一個輸出段的標準格式

section [address] [(type)] : [AT(lma)]
  {
    output-section-command
    output-section-command
    ...
  } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

前面也說了,所謂的輸出段是指最終生成的文件裏面的段,所以一個輸出段就可以
理解爲最終文件裏面的一個塊,那麼多個塊合起來就是一個完成文件了。
而每個小塊又分別有什麼文件來組成呢?那就是輸入段了。
我自己實際用到有下面的一些,其他暫時不會用。

section_name  vma : AT(lma)
  {
    output-section-command
    output-section-command
    ...
  } [AT>lma_region]

section_name 根據ld手冊說是有個確定的名字,其他沒啥,自己添加一些新段也是
可以的。
默認的4個段是必須有的

.text 代碼
.rodata 常量,例如字符串什麼的
.data 初始化的全局變量
.bss  沒有初始化的全局變量

其實沒什麼,可以說,都是固定的,所以一句話,照抄。
段名字後面緊跟的是 vma ,也就是這個段在程序運行的時候的地址,例如

.text 0x30000000 :
{
    *(.text)
}

表示的是代碼的運行時地址爲 0x30000000 假如你的ROM在 0x0 地址,程序放在ROM
中,那個時候程序是不能正常運行的(位置無關代碼除外),必須將代碼COPY到VMA
也就是 0x30000000 中才能正常運行。
至於那個 AT(lma) 的關鍵字,只指示代碼連接的時候應該放在什麼地方,注意好
這個英文是 load memory address,是指程序應該裝載在什麼地方,而不是指這個段
應該在最後生成的bin文件的位置!!!這個東西蒙騙了我,讓我鬱悶了1天。elf格
式的文件裏面不但包含了代碼,還包含了各種各樣的信息,例如上面說的每個段的
lma 和vma,還有其他信息都包含在裏面了。
默認狀態下,lma 是等於當前的vma的,例如

.text 0x30000000 : 
{ 
  *(.text) 
  *(.rodata)
}
.data 0x33ffff00 :  
{ 
  *(.data) 
}

例如我們基本的兩個段,我們指定了.text 和.data段的vma,但是沒有指定lma,那麼
lma到底應該是多少?很簡單,ld認爲當前的lma和vma是相同的,所以lma應該分別是
0x30000000 0x33ffff00,編譯生成的elf文件很小,但是objcopy出來的文件卻非常
巨大,達到了60多MB,這是什麼問題?
elf文件很聰明,他只是保存了信息,.text段的 lma 是0x30000000,那麼elf就保存了
知道本程序的代碼應該加載到 0x30000000,然後又保存了.data 的 lma,我們留意到
中間有很多的地址空間是空的,並沒有實際的代碼,elf怎麼處理?elf只保存了兩個
地址和實際的代碼,而對於其他空間裏面的代碼他並不處理,所以可以看出,最後生成
的elf文件並不大,也就100多KB而已,但是後來的OBJCOPY操作中,從elf文件中copy
出程序代碼,這下就糟了,objcopy是從最開始的lma開始,這裏是 0x30000000一直
複製到最尾段的lma,這裏是 0x33ffff00,中間沒有代碼地方全部補零,那麼60多MB
的大bin文件就是這樣來的。
可以驗證一下,如果手動指定開始的lma爲0的話

    .text 0x30000000 : AT(0)
{ 
  *(.text) 
  *(.rodata)
}
    .data 0x33ffff00 :  
{ 
  *(.data) 
}

其中.text段的lma被AT強制指定爲0,那麼objcop出來的bin文件相當的華麗,達到了
700多MB,爲什麼?都說了,從0開始到 0x33ffff00,中間補零,字節數相當的可觀呢。
一般我們常用的做法是:

1,.data段的 lma 和 vma 都是緊跟着 .text 的,或者用ARM的說法就是 RW段緊跟着
RO段,這樣的做法非常簡單

.text 0x30000000 : 
{ 
   *(.text) 
   *(.rodata)
}
.data :  
{ 
   *(.data) 
}
.bss  :
{
   *(.bss) *(.COMMON)
}

只指定RO BASE,然後所有代碼都是跟着RO BASE分配,這樣非常簡單。

2,.data段分離出來,連接到不同的vma運行時地址。

 .text 0x30000000 : 
{ 
  *(.text) 
  *(.rodata)
}
.data 0x31000000 : AT(LOADADDR(.text) + SIZEOF(.text))  
{ 
  *(.data) 
}
.bss  :
{
   *(.bss) *(.COMMON)
}

其實也不難解決,像上面的代碼那樣做就OK了,上面也分析了,如果vma不同的話,objcopy
會一直複製,這樣生成的bin文件會很大,怎麼解決?很簡單,手工指定 .data段的lma地址
讓 .data段的 lma 緊緊跟着 .text段的末尾,這樣生成的 bin 文件就會很漂亮,跟第一種
辦法生成的bin文件結構一模一樣!!
AT(LOADADDR(.text) + SIZEOF(.text))
這個指令大概解釋一下,AT 是指定lma 的,然後裏面用了兩個指令 LOADADDR ,和名字一樣
這個指令是用來求 lma 地址的! SIZEOF 也就是名字那樣,求大小的
LOADADDR(.text) 求出 .text 段的 lma,注意是開始地址
SIZEOF(.text) 求出 .text 段的大小
AT(LOADADDR(.text) + SIZEOF(.text)) 的效果就是,指定 .data段的lma在 .text段lma
的結尾處!
這裏補充一下,還有一個指令 ADDR(.text) 這個是求vma的,不是求lma。
另外,注意一下 .bss段的lma和 .data段的 lma是一樣的,這也反映了一個實質問題 .bss 段
只分配運行地址 vma,並不實際佔空間的。
3,如果我想自己添加一些段,應該怎麼去實現?
例如我要添加一個 .vector 的段,裏面放的是一些數據,怎麼實現?
(1)如果在彙編代碼裏面添加,那麼可以新啓動一個段
例如在 2440init.S 中添加 .vector 段

.section .text
....
....     
(其他代碼)
.section   .vector     @ 在這裏聲明一個段,並且放連個數據
.word  0x55
.word  0xaa 

彙編代碼段的開始由 .section 聲明,接着後面的都屬於這個段,直到第二個 .section 聲明
爲止。
我這個 .vector段是需要連接到 0x33ffff00 的,非常的特殊,那麼按照前面的辦法

    .text 0x30000000 : 
{ 
  *(.text) 
  *(.rodata)
}
    .data 0x31000000 : AT(LOADADDR(.text) + SIZEOF(.text))  
{ 
  *(.data) 
}
    .bss  :
        {
                *(.bss) *(.COMMON)
        }
    .vector 0x33ffff00 : AT(LOADADDR(.data) + SIZEOF(.data))
        {
                *(.vector)
        }

可以看出,形式其實是一樣,不過看一下,添加的段的lma放在 .data 段的lma的後面,前面也說
看 .bss 和 .data的lma是一樣的,所以其實無視掉 .bss段就OK了。
(2)在C語言中怎麼添加一個變量指定放到 .vector段
很簡單,用GNU擴展語法(注意了,是GNU系列工具通用而已,例如gcc,這個並不是C的標準)
格式如下

unsigned int __attribute__((section(".vector"))) vec=0x9988;

定義一個 vec 變量,值爲 0x9988,分配在 .vector 段,編譯後用 objdump 一下查看彙編代碼
可以發現到

Disassembly of section .vector:
33ffff00 :
33ffff00: 00009988  .word 0x00009988
33ffff04: 00000055  .word 0x00000055
33ffff08: 000000aa  .word 0x000000aa

看到沒有?剛纔說的在彙編代碼和C代碼裏面定義的數值都被連接進去了 .vector段了,vma也正確
最後還可以看看生成的 bin 文件,看看最後的幾個數據是不是就是 0x9988 0x55 0xaa ?這樣應該
就理解了整個連接的過程了吧?
4,MEMORY 命令在指定lma中的使用
每個段都要用 AT 來指定具體的位置,其實挺煩的,我們有更加簡單的辦法,我們定義一個內存區域
讓,然後將所有的段都扔進去。

MEMORY
{
rom (rx)     : ORIGIN = 0x30000000, LENGTH = 1M 
}

注意,我們現在要實現的是lma,並不是vma,也就是說在最後生成的 bin文件中怎麼將所有段合在一
起。定義一個開始地址爲 0x30000000 ,也就是lma,對應上面的 .text段的lma,長度自己設,我設置
爲 1M ,其實溢出會提示的,隨便設就OK了。

    .text 0x30000000 : 
{ 
  *(.text) 
  *(.rodata)
} AT>rom
    .data 0x31000000 : 
{ 
  *(.data) 
} AT>rom
    .bss  :
        {
                *(.bss) *(.COMMON)
        } 
    .vector 0x33ffff00 :
        {
                *(.vector)
        } AT>rom

看到每個輸出段的末尾都有個 AT>rom 的操作吧?應該大概猜到,通俗一點說就是:“將這個輸出段
扔到rom 指定的那個內存區域!!”

rom是上面已經定義了,那麼這些操作之後,.text .data .vector 都乖乖的扔進 rom 指向的那個區域,注意了,我們說的是lma,所以不要在意那個開始地址,剛纔不是說了嗎?那個objcopy是從最開始的lma開始copy而已,這樣出來的效果和第三點中生成的bin文件其實是一模一樣的!!
不信的話用UE查看一下 bin 文件的16進制代碼,或者查看連接生成的 map文件。這樣做方便很多。

既然 lma 是包含在 elf文件當中,那這個地址到底有什麼用?這個我也不知道了,我猜測,首先,elf文件是linux下面的可執行文件格式,跟windows上面的 .exe文件其實一樣的,看過window的可執行文件的PE結構的應該知道,真正的代碼前面是有一堆標誌啊,地址啊,什麼的,操作系統就是通過讀取這部分信息,就知道應該怎麼將這個可執行文件加載進去。同理,elf文件頭也有一堆有用的信息。不過對於我們的嵌入式系統我估計應該是用不上了(我說的是裸奔),基本上都是通過 objcopy 將真正的代碼弄出來燒些到 flash裏面跑的,所以在嵌入式系統上面,這個 lma我覺得應該是沒有用處的。

另外,如果用工具調試的時候,例如我用的是 openocd,如果加載 elf文件,並不需要指定地址,openocd會自動的加載,爲什麼這個神奇?我覺得應該是elf文件裏面包含了 lma 的作用吧,呵呵,其實挺方便的。

結束語:

被這個小東西虐待了整整一天半,瘋狂找資料,啃ld as等的英文資料手冊,算是實驗了一點成果出來,上面
說的技術對於我暫時的應用來說已經足夠了,也足夠看懂很多例子裏面的 ld script了,網上的資料基本都是
在翻譯 ld 的英文手冊 … 唉 …

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章