c语言如何输出字母锥子塔,GCC连接脚本学习笔记 zz
連接腳本將我整整蒙了1天零一個上午,做了很多實驗,看了人家不少例子代碼
勉強能駕馭了,讓linker按照我想要的來處理,做個筆記。
1,什么叫輸入段,什么叫輸出段
不知道怎么回事,我對GCC系列的輸入和輸出兩個單詞總是進入思維死角,很簡單
就是 input section 和 output
section,這里不是說翻譯的問題,我覺得是一種
思考的方式的問題。
我的問題就是:既然叫輸入端,那輸入什么?同理,輸出的是什么?不知道其他人
不會不理解這個問題,我自己的話是理解了不少時間了 -v-
所謂的輸出段,是指生成的文件,例如 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 的英文手冊.
總結
以上是生活随笔為你收集整理的c语言如何输出字母锥子塔,GCC连接脚本学习笔记 zz的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: java点赞功能的实现,类似微信点赞,用
 - 下一篇: Windows程序开机自启动