先要講講這個問題是怎麼來的。(咱們在分析一個技術的時候,先要考慮它是想解決什麼問題,或者學習新知識的時候,要清楚這個知識的目的是什麼)。
- 我在編譯核心的時候,發現arch/arm/kernel目錄下有一個這樣的檔案:vmlinux.lds.S。第一眼看上去,想想是不是彙編檔案呢?開啟一看,好像不是。那它是幹嘛的?而且前面已經說過,make V=1的時候,發現這個檔案的用處在ld命令中,即ld -T vmlinux.lds.S,好像是連結命令用的,如下所示
如arm-linux-ld -EL -p --no-undefined -X --build-id -o vmlinux -T arch/arm/kernel/vmlinux.lds。man ld,得到-T的意思是:為ld指定一個Linker script,意思是ld根據這個檔案的內容來產生最終的二進位。
- 也許上面這個問題,你從沒關注過,但是在研究核心代碼的時候,常常有地方說“ __init宏會在最後的模組中產生一個特定的section,然後kernel載入的時候,尋找這個section中的函數”,說白了,上面這句話就是說最後產生的模組中,有一個特定的section,這又是什麼東西?
好吧,希望上面的問題勾起你的好奇心。下面我們來掃盲,最後會給一個連結地址,各看官可以去那深造。
一 section是什嗎?好吧,我們需要解釋一下平時編譯連結產生的二進位可執行程式(比如說ELF,EXE也行),so或者dll,核心(非壓縮的,參加本系列第一節內容、vmlinux),或者ko是怎麼組織的。其實,大家或多或少都知道這些二進位中包括有什麼text/bss/data節(也叫section)。text節儲存的是代碼、data儲存的是已經初始化的靜態變數、bss節儲存的是未初始化的什麼東西...上面的東西我就不細究了。反正一點,一個二進位,最終會包含很多section。那麼,為什麼section叫text/bss/data,能叫別的名字嗎?OK,可以。但是你得告訴ld,那麼這些內容就通過-T選項指定一個linker script就行了。這些內容我們放到後面的執行個體中來介紹。(再三強調,咱們在理論上只是拋磚引玉,希望有興趣的看官自己研究,注意和我們分享你的成果就行了。)
二 link script基礎知識介紹linker script中的文法是linker command language(很簡單的language,大家不用害怕...)。那麼LS的目的是什麼呢?
- LS描述輸入檔案(也就是gcc -c命令產生的.o檔案即object檔案)中的section最終如何對應到一個輸出檔案。這個其實好理解,例如一個elf由三個.o檔案構成,每個.o檔案都有text/data/bss段,但最終的那一個elf就會將三個輸入的.o檔案的段合并到一起。
好了,下面我們介紹一些基本知識:
- ld的功能是將input檔案組裝成一個output檔案。這些檔案內部的都有特殊的組織圖,這種結構被叫做object file format。每一個檔案叫做object file(這可能就是.o檔案的來曆吧。哈哈),輸出檔案也叫可執行檔(an executable),但是對於ld來說,它也是一種object檔案。那麼Object檔案有什麼特殊的地方呢?恩,它內部組織是按照section(段、或者節,以後不再區分二者)來組織的。一句話,object檔案內部包含段......
- 每個段都有名字和size。另外,段內部還包含一些資料,這些資料叫做section contents,以後稱段內容。每個段有不同的屬性。例如text段標誌為可載入(loadable),表示該段內的contents在運行時候(當然指輸出檔案執行的時候)需要載入到記憶體中。另外一些段中沒有contents,那麼這些段標示為allocatable,即需要分配一些記憶體(有時候這些記憶體會被初始化成0,這裡說的應該是BSS段。BSS段在二進位檔案中沒有佔據空間,即磁碟上二進位檔案的大小比較小,但是載入到記憶體後,需要為BSS段分配記憶體空間。),還有一些段屬於debug的,這裡包含一些debug資訊。
- 既然需要載入到記憶體中,那麼載入到記憶體的地址是什麼呢?loadable和allocable的段都有兩個地址,VMA:虛擬位址,即程式運行時候的地址,例如把text段的VMA首地址設定為0x800000000,那麼運行時候的首地址就是這個了。另外還有一個LMA,即Load memory address。這個地址是section載入時的地址。暈了吧?二者有啥區別?一般情況下,VMA=LMA。但也有例外。例如設定某資料區段的LMA在ROM中(即載入的時候拷貝到ROM中),啟動並執行時候拷貝到RAM中,這樣LMA和VMA就不同了。---------》很難搞懂不是?這種方法用於初始化一些全域變數,基於那種ROM
based system。(問一個問題,run的時候,怎麼根據section中的VMA進行相應設定啊??以後可能需要研究下核心中關於execve實現方面的內容了)。關於VMA和LMA,大家通過objdump -h選項可以查看。
三 簡單例子下面來一個簡單例子, SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
- SECTIONS是LS文法中的關鍵command,它用來描述輸出檔案的記憶體布局。例如上例中就含text/data/bss三個部分(實際上text/data/bss才是段,但是SECTIONS這個詞在LS中是一個command,希望各位看官要明白)。
- .=0x10000; 其中的.非常關鍵,它代表location counter(LC)。意思是.text段的開始設定在0x10000處。這個LC應該指的是LMA,但大多數情況下VMA=LMA。
- .text:{*(.text)},這個表示輸出檔案的.text段內容由所有輸入檔案(*)的.text段組成。組成順序就是ld命令中輸入檔案的順序,例如1.obj,2.obj......
- 此後,由來了一個.=0x800000000;。如果沒有這個賦值的,那麼LC應該等於0x10000+sizeof(text段),即LC如果不強制指定的話,它預設就是上一次的LC+中間section的長度。還好,這裡強制指定LC=0X800000000.表明後面的.data段的開始位於這個地址。
- .data和後面的.bss表示分別有輸入檔案的.data和.bss段構成。
你看,我們從這個LC檔案中學到了什嗎?恩,我們可以任意設定各個段的LMA值。當然,絕大部分情況,我們不需要有自己的LS來控制輸出檔案的記憶體布局。不過LK(linux kernel)可不一樣了......
四 霸王硬上弓---vmlinux.lds.S分析 OK,有了上面的基礎知識,下面我們霸王硬上弓,直接分析arch/arm/kernel/vmlinux.lds.S.雖然最終連結用的是vmlinux.lds,但是那個檔案由vmlinux.lds.S(這是一個彙編檔案)得到,arm-linux-gcc -E -Wp,-MD,arch/arm/kernel/.vmlinux.lds.d -nostdinc ...... -D__KERNEL__ -mlittle-endian ......-DTEXT_OFFSET=0x00008000 -P -C -Uarm -D__ASSEMBLY__ -o
arch/arm/kernel/vmlinux.lds
arch/arm/kernel/vmlinux.lds.S所以,我們直接分析vmlinux.lds好了。/*
一堆注釋,這裡就不再貼上了,另外,增加//號做為注釋標識
* Convert a physical address to a Page Frame Number and back
*///OUTPUT_ARCH是LS文法中的COMMAND,用來指定輸出檔案的machine arch。objdump -f可查詢所有支援的machine。另外//這些東西涉及到一種叫BFD的。各位看官可以自己搜尋下BFD的內容。//下面這 表示輸出檔案基於ARM架構
OUTPUT_ARCH(arm) //ENTRY也是一個command,用來設定進入點。這裡表示進入點是stext 。根據LD的描述,進入點的意思就是程式啟動並執行第一條指令。核心是一個模組,大家把他想象//成一個運行在硬體上的大程式就可以了。而我們的程式又是運行在核心至上的。比較下Java虛擬機器以及運行在其上的Java程式吧......
ENTRY(stext)//設定jiffies為jiffies_64
jiffies = jiffies_64;//定義輸出檔案的段
SECTIONS
{//設定location count為0xc0008000,這個好理解吧?核心啟動並執行地址全在C0000000以上
. = 0xC0000000 + 0x00008000;//定義一個.text.head段,由輸入檔案中所有.text.head段組成/*LS文法中,關於seciton的定義如下:section [
address] [(type)] :
[
AT(lma)] [ALIGN(section_align)]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
...
} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]其中,
address為VMA,而
AT命令中的為LMA。一般情況,address不會設定,所以它預設等於當前的location counter*/
.text.head : {/*這個非常關鍵,咱們在核心代碼中經常能看到一些變數聲明,例如extern int __stext,但是卻找不到在哪定義的其實這些都是在lds檔案中定義的。這裡得說一下編譯連結相關的小知識。咱們這知道大概即可,具體內容可以自己深入研究假設C代碼中定義一個變數 int x = 0;那麼1 編譯器首先會分配一塊記憶體,用來儲存該變數的值2 編譯器在程式的symbol表中,建立一項,用來儲存這個變數的地址例如,上面的 int x = 0,就在symbol表中建立一x項,這個x項指向一塊記憶體,sizeof(int)大小,儲存的值為0。當有地方使用這個x的時候,編譯器會產生相應的代碼,首先指向這個x的記憶體,然後讀取記憶體中的值。上面的內容是C中一個變數的定義。但是Linker script中也可以定義變數,這時候只會產生一個symbol項,但是沒有分配記憶體。。例如_stext=0x100,那麼會建立一個symbol項,指向0x100的記憶體,但該記憶體中沒有儲存value。所以,我們在C中使用LS中定義的變數的話,只能取它的地址。下面是一個例子:
start_of_ROM = .ROM;end_of_ROM = .ROM + sizeof (.ROM) - 1;start_of_FLASH = .FLASH;
上面三個變數是在LS中定義的,分別指向.ROM段的開始和結尾,以及FLASH段的開始。現在在C代碼中想把ROM段的內容拷貝到FLASH段中,下面是C代碼:
extern char start_of_ROM, end_of_ROM, start_of_FLASH;memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);
注意其中的取地址符號&。C代碼中只能通過這種方式來使用LS中定義的變數. start_of_ROM這個值本身是沒有意義的,只有它的地址才有意義。因為它的值沒有初始化。地址就指向.ROM段的開頭。說白了,LS中定義的變數其實就是地址,即_stext=0x100就是C代碼中的一個地址 int *_stext=0x100。明白了?最終的ld中會分配一個slot,然後儲存x的地址。也就是說,ld知道這些勾當。那麼當然我們在LS中也可以定義一個變數,然後在C中使用了。所以下面這句話實際上定義了一個_stext變數。在C中通過extern就可以引用了。但是這裡有一個比較關鍵的問題。C中定義的x=0,其值被初始化為0了。也就是slot...待補充*/
_stext = .;.
_sinittext = .;
*(.text.head)
}//定義.init段,由所有的.init.text/.cpuinit.text/.meminit.text組成//這時的LC的值為.init的開始
.init : { /* Init code and data */
*(.init.text) *(.cpuinit.text) *(.meminit.text)//定義一個變數 _einitext,它的值為當前的LC,即.init的初值+*(.init.text) *(.cpuinit.text) *(.meminit.text)的大小。也就是說變數//_einitext標示一個結尾。
_einittext = .;//下面這個變數
__proc_info_begin標示一個開頭
__proc_info_begin = .;
*(.proc.info.init) //所有.proc.info.init段內容在這
__proc_info_end = .;//下面這個變數
__proc_info_end標示結尾,它和__proc_info_begin變數牢牢得把輸出檔案.proc.info.init的內容卡住了。//有了上面begin和end的介紹,後面就簡單了,大部分都是一個begin+end來卡住一段內容。根據前面的介紹,begin和end又可以在C程式中引用//也就是我們通過Begin+end,就可以獲得卡住的內容了。例如我們把一些初始化的函數指標放到一個begin和end中。然後通過一個迴圈,不就是//可以調用這些函數了麼。最後我們就來個例子介紹下。
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
__tagtable_begin = .;
*(.taglist.init)
__tagtable_end = .;
. = ALIGN(16);
__setup_start = .;
*(.init.setup)
__setup_end = .;
__early_begin = .;
*(.early_param.init)
__early_end = .;
__initcall_start = .;
*(.initcallearly.init) __early_initcall_end = .;
*(.initcall0.init) *(.initcall0s.init) *(.initcall1.init) *(.initcall1s.init) *(.initcall2.init) *(.initcall2s.init) *(.initcall3.init) *(.initcall3s.init) *(.initcall4.init) *(.initcall4s.init) *(.initcall5.init) *(.initcall5s.init) *(.initcallrootfs.init)
*(.initcall6.init) *(.initcall6s.init) *(.initcall7.init) *(.initcall7s.init)
__initcall_end = .;
__con_initcall_start = .;
*(.con_initcall.init)
__con_initcall_end = .;
__security_initcall_start = .;
*(.security_initcall.init)
__security_initcall_end = .;
. = ALIGN(32);//ALIGN,表示對齊,即這裡的Location Counter的位置必須按32對齊
__initramfs_start = .; //ramfs的位置
usr/built-in.o(.init.ramfs)
__initramfs_end = .;
. = ALIGN(4096); //4K對齊
__per_cpu_load = .;
__per_cpu_start = .;
*(.data.percpu.page_aligned)
*(.data.percpu)
*(.data.percpu.shared_aligned)
__per_cpu_end = .;
__init_begin = _stext;
*(.init.data) *(.cpuinit.data) *(.cpuinit.rodata) *(.meminit.data) *(.meminit.rodata)
. = ALIGN(4096);
__init_end = .;
}//DISACARD是一個特殊的section,表示符合這個條件的輸入段都不會寫到輸出段中,也就是輸出檔案中不包含下列段
/DISCARD/ : { /* Exit code and data */
*(.exit.text) *(.cpuexit.text) *(.memexit.text)
*(.exit.data) *(.cpuexit.data) *(.cpuexit.rodata) *(.memexit.data) *(.memexit.rodata)
*(.exitcall.exit)
*(.ARM.exidx.exit.text)
*(.ARM.extab.exit.text)
}
//省略部分內容//ADDR為內建函數,用來返回VMA的/*這裡舉個小例子,大家看看VMA和LMA到底有什麼作用SECTIONS
{
.text 0x1000 : { *(.text) _etext = . ; } /.text段的VMA為0x1000,而且LMA=VMA
.mdata 0x2000 : //.mdata段的VMA為0x2000,但是它的LMA卻在.text段的結尾
AT ( ADDR (.text) + SIZEOF (.text) )
{ _data = . ; *(.data); _edata = . ; }
.bss 0x3000 :
{ _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}看到了嗎?.mdata段啟動並執行時候在0x2000,但是資料load地址卻在.text段後,所以啟動並執行時候需要把.mdata段內容拷貝過去。 extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext; //_etext為.text端的末尾 VMA地址,但同時也是.mdata段LMA的開始,有LS種的AT指定
char *dst = &_data; //_data為mdata段的VMA,現在需要把LMA地址開始的內容拷貝到VMA開始的地方 /* ROM has data at end of text; copy it. */
while (dst < &_edata)
*dst++ = *src++; //拷貝....明白了?不明白的好好琢磨
/* Zero bss. */
for (dst = &_bstart; dst< &_bend; dst++)
*dst = 0; //初始化資料區域 */
.rodata : AT(ADDR(.rodata) - 0) {
__start_rodata = .;
*(.rodata) *(.rodata.*) *(__vermagic) *(__markers_strings) *(__tracepoints_strings)
}
.rodata1 : AT(ADDR(.rodata1) - 0) {
*(.rodata1)
}
......//省略部分內容 _edata_loc = __data_loc + SIZEOF(.data);
.bss : {
__bss_start = .; /* BSS */
*(.bss)
*(COMMON)
_end = .;
}
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
}
//ASSERT是命令,如果第一個參數為0,則列印第二個參數的資訊(也就是錯誤資訊),然後ld命令退出。
ASSERT((__proc_info_end - __proc_info_begin), "missing CPU support")
ASSERT((__arch_info_end - __arch_info_begin), "no machine record defined") 五 核心代碼中使用LS中定義的變數咱們看一個小例子[-->init/main.c]extern initcall_t __initcall_start[], __initcall_end[], __early_initcall_end[];
//這幾個值在LS中定義。大家可以在上面搜尋下static void __init do_initcalls(void)
{
initcall_t *call;
//上面已經定義成數組了,所以下面這些變數直接取的就是指標,和上面例子中使用&一個意思,反正不能用value
for (call = __early_initcall_end; call < __initcall_end; call++)
do_one_initcall(*call);
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_work();
} 六 總結關於LS的詳細文檔,見下面的網址:http://sourceware.org/binutils/docs/ld/index.html上面文檔寫得比較粗,但大家知道兩點即可:
- LK源碼中那些找不到來源的變數是怎麼來的---》在LS定義。
- VMA和LMA是怎麼回事。