天天看點

linux核心連接配接腳本,linux核心連結腳本詳解

3.3 構造Section

在建構了基本的segment後,就可以從輸入.o檔案中擷取感興趣的section以生成新的section并放入相應的segment。在這裡,輸入的section稱為input

section,生成的新section稱為output section。除此之外,有一個重要的連結腳本符号“.”需要了解。”.”是個位置計數器,記錄着目前位置在目标檔案中的虛拟位址(VMA)。”.”是個自動增加的計數器,當一個output

section生成後,”.”的值自動加上該output section的長度。我們也可以顯式的給”.”指派以改變目前位置的位址,這在核心連結腳本中被大量使用。一個例子可以很好的描述”.”的作用:

. = 0x100000;

_start_addr = .;

.text : { *(.text) }

_end_addr = . ;

這裡我們首先給”.”賦了一個初值,将位址指定到0x100000處,并将該值賦給變量_start_addr,它是.text section的起始位址;接着我們生成了一個.text

section,此時”.”自動加上該section的長度,可描述為. = . + SIZEOF(.text);最後将”.”指派給_end_addr,記錄下.text的結束位址。此時”.”的值變成了0x100000 + SIZEOF(.text)。有了”.”的幫助,我們可以靈活的控制目标檔案中各個section所在的虛拟位址(VMA)。

3.3.1 Text Segment的構造

核心首先構造的是text segment,該segment又由若幹個.text.*節構成,除此之外,它還包含了note segment的内容以及隻讀資料section。下面的代碼完成了這些工作:

SECTIONS

{

. = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;

phys_startup_32 = startup_32 - LOAD_OFFSET;

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;              

*(.text.head)

} :text = 0x9090

.text : AT(ADDR(.text) - LOAD_OFFSET) {

. = ALIGN(PAGE_SIZE);

*(.text.page_aligned)

TEXT_TEXT

SCHED_TEXT

LOCK_TEXT

KPROBES_TEXT

*(.fixup)

*(.gnu.warning)

_etext = .;                    

} :text = 0x9090

NOTES :text :note

. = ALIGN(16);        

__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {

__start___ex_table = .;

*(__ex_table)

__stop___ex_table = .;

} :text = 0x9090

RODATA

SECTIONS

{

. = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;

phys_startup_32 = startup_32 - LOAD_OFFSET;

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;              

*(.text.head)

} :text = 0x9090

.text : AT(ADDR(.text) - LOAD_OFFSET) {

. = ALIGN(PAGE_SIZE);

*(.text.page_aligned)

TEXT_TEXT

SCHED_TEXT

LOCK_TEXT

KPROBES_TEXT

*(.fixup)

*(.gnu.warning)

_etext = .;                    

} :text = 0x9090

NOTES :text :note

. = ALIGN(16);        

__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {

__start___ex_table = .;

*(__ex_table)

__stop___ex_table = .;

} :text = 0x9090

RODATA

首先是SECTIONS關鍵字,官方的解釋是“The SECTIONS command tells the linker how to map input sections into output sections, and how to place the output sections in memory.”,實際上可以把它看成一個描述符,所有的工作要在它的内部完成。就像你在C中定義一個結構體要以struct關鍵字開頭一樣。

構造的第一步是為”.”指定初值,之後所有section虛拟位址(VMA)都由該值計算得來(前面我們講過,生成一個section後”.”的值會自動加上改section的長度)。這裡初始值為

LOAD_OFFSET + LOAD_PHYSICAL_ADDR,前者是我們熟知的核心虛拟位址空間起始位址0xC0000000,LOAD_PHYSICAL_ADDR是核心image加載的實體位址,由CONFIG_PHYSICAL_START計算得到。該實體位址是可以指定的,你可以在.config檔案中找到它,也可以由make

menuconfig得到,具體解釋參考arch/x86/Kconfig檔案的PHYSICAL_START條目。對于一般的x86架構,核心被加載到實體位址0x100000處,故”.”的初值為0xC0100000。接着

phys_startup_32 = startup_32 - LOAD_OFFSET;

計算了核心image的入口位址,這在前面已經提到。

開始構造section了。由于使用的文法是固定的,我們隻需要了解一個例子,其餘的就可舉一反三。以第一個section為例:

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;              

*(.text.head)

} :text = 0x9090

.text.head : AT(ADDR(.text.head) - LOAD_OFFSET) {

_text = .;              

*(.text.head)

} :text = 0x9090

.text.head指定了生成的section的名字,後面的冒号是固定文法。AT關鍵字前面介紹過,指定該section的加載位址(LMA),它的完整表達是

AT(expression)

括号中expression表達式指定LMA的值。在此例中該表達式由

ADDR(.text.head) - LOAD_OFFSET

計算得到。這裡

ADDR(section)

計算section的虛拟位址,故.text.head的加載位址(LMA)是它的實體位址。在大括号内部,_text = .;

定義了一個全局變量,它的值為”.”的目前值,記錄了整個text segment的起始位址.。在這裡,由于_text變量前還沒有任何section被建立,故_text有如下等價關系:

_text = ADDR(.text.head) = . = LOAD_OFFSET + LOAD_PHYSICAL_ADDR;

*(.text.head)完成了具體的section建立工作,”*”代表所有輸入的.o檔案,括号中的.text.head指定了連結器感興趣的section名。

*(text.head)

表示從所有輸入檔案中抽取名為.text.head的section并填充到目标檔案的.text.head section中。

: text

指定了新生成section所在的segment,這裡冒号後的text是segment名,可見核心的第一個section被放到了text

segment。

= 0x9090

指定section的填充内容。從輸入檔案中抽取來的section由于代碼對齊的緣故,其二進制的存放可能是不連續的,這裡指定對section中的空隙用0x9090進行填充。0x90是彙編指令NOP的機器碼,故相當于在不連續代碼間填充空操作。至此,核心的第一個section就建立好了,它名為.text.head,由輸入檔案的.text.head

section構成(并非所有檔案都有.text.head section,連結器隻從具有該section的檔案中抽取内容),該section的虛拟位址(VMA)由”.”的值确定,加載位址(LMA)為其實體位址,section中不連續區域産生的間隙由0x9090填充,最後該section被放入了核心的text

segment中。

通過objdump核心,我們可以看到關于該section的最終内容:

Sections:

Idx Name          Size      VMA       LMA       File off  Algn

0 .text.head    00000375  c1000000  01000000  00001000  2**2

CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

…………………………………………………………………………………………..

Disassembly of section .text.head:

c1000000 <_text>:

c1000000:>--f6 86 11 02 00 00 40 >--testb  $0x40,0x211(%esi)

c1000007:>--75 14                >--jne    c100001d <_text>

c1000009:>--0f 01 15 1a e1 4d 01 >--lgdtl  0x14de11a

>--->--->---c100000c: R_386_32>-boot_gdt_descr

c1000010:>--b8 18 00 00 00       >--mov    $0x18,%eax

c1000015:>--8e d8                >--mov    %eax,%ds

…………………………………………………………………………………………………

c10013d5:>--5b                   >--pop    %ebx

c10013d6:>--5e                   >--pop    %esi

c10013d7:>--c9                   >--leave--

c10013d8:>--c3                   >--ret----

c10013d9:>--90                   >--nop----

c10013da:>--90                   >--nop----

c10013db:>--90                   >--nop----

c10013dc:>--90                   >--nop----

Sections:

Idx Name          Size      VMA       LMA       File off  Algn

0 .text.head    00000375  c1000000  01000000  00001000  2**2

CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

…………………………………………………………………………………………..

Disassembly of section .text.head:

c1000000 <_text>:

c1000000:>--f6 86 11 02 00 00 40 >--testb  $0x40,0x211(%esi)

c1000007:>--75 14                >--jne    c100001d <_text>

c1000009:>--0f 01 15 1a e1 4d 01 >--lgdtl  0x14de11a

>--->--->---c100000c: R_386_32>-boot_gdt_descr

c1000010:>--b8 18 00 00 00       >--mov    $0x18,%eax

c1000015:>--8e d8                >--mov    %eax,%ds

…………………………………………………………………………………………………

c10013d5:>--5b                   >--pop    %ebx

c10013d6:>--5e                   >--pop    %esi

c10013d7:>--c9                   >--leave--

c10013d8:>--c3                   >--ret----

c10013d9:>--90                   >--nop----

c10013da:>--90                   >--nop----

c10013db:>--90                   >--nop----

c10013dc:>--90                   >--nop----

c10013dd:>--90                   >--nop----

c10013de:>--90                   >--nop----

c10013df:>--90                   >--nop----

其中最後一部分顯示了填充0x9090産生的nop指令。

連結腳本知識:

建立一個section的完整格式是:

section [address] [(type)] : [AT(lma)]

{

output-section-command

output-section-command

...

} [>region] [:phdr :phdr ...] [=fillexp]

其中[address]參數在上例中沒有提到,它指定了section的虛拟位址(VMA),如果沒有指定該參數及region參數,section的虛拟位址由目前”.”的值确定,正如上例我們看到的一樣。Region用于将section配置設定給通過MEMORY關鍵字建立的記憶體描述塊,核心連結腳本沒使用它,本文也不關注,具體内容詳見參考文獻1的MEMORY

command一節。

通過這個例子,我們很容易就可以了解text segment中其它section的建立。例如接下來的第二個.text section,它的建立方法和.text.head類似,唯一不同的是這裡多了一句:

. = ALIGN(PAGE_SIZE);

ALIGN(exp)關鍵字計算目前”.”值對齊到exp邊界後的位址,即:

ALIGN(exp) = ( . + exp – 1) & ~(exp – 1);

此處在建立.text section前,将”.”對齊到了頁邊界,從第一個輸入section的名字.text.page_aligned就可以看出,輸入section的内容是有對齊要求的。核心使用了TEXT_TEXT等宏将不同類型的輸入section進行了封裝,展開後可以看到它們都是:

*(section_name)

的形式,和我們前面講的一樣,不再多做介紹。

從上面内容可以看出,輸入檔案中的section有各種各樣的名字,如.text.head、.text.page_aligned、.text.hot等,并不是所有的section名都是标準的,絕大部分是核心使用GCC擴充生成的自定義名。舉個例子,我們常見的__init宏,展開後如下:

#define __init __attribute__ ((__section__(“.init.text”)))

這裡.init.text是個自定義的section,用__init修飾的函數編譯後會被放到名為.init.text section中。

自定義的section極大的發揮了連結腳本的作用,讓我們可以對代碼中的函數、資料進行歸類操作,同時還可以完成一些在程式中不易完成的功能。這很容易了解,如果我們都用GCC内置的section,何必要自定義連結腳本,用預設的不就好了。

連結腳本向我們展示了大量的自定義section,本人水準有限,無法一一弄清每個section的用途,但通過幾個常見的典型例子,我們可以了解它們的用法。首先就以text segment中的exception

table舉例。

3.1.1 Exception Table

此exception table不是用于處理硬體異常的(那是IDT表的工作),但它确實和硬體異常有一點關系,具體來說是和Page Fault有關系。Exception

Table的具體機制在核心文檔”Exception”中有詳細介紹,你可以在/path_to_your_kernel_src/ Documentation/exception.txt中找到它。這裡為了說明問題做一點簡要介紹。

我們尊敬的Linus大神為了避免核心在通路使用者态位址時進行有效性檢查帶來的開銷(我們總是需要這樣的檢查,雖然大部分情況下結果是成功的),利用了page fault的處理函數來完成這項任務,這樣隻有在真正通路了一個壞的使用者态位址時檢查才會發生。或許你會問:此時檢查有什麼用?一個例子就很容易說明問題,假設我們有一個函數叫is_user_addr_ok(),用于檢查傳入的使用者态位址是否合法。那麼,當位址非法時它能幹什麼?什麼都不能幹,僅僅是告訴核心:“這是個非法位址,你不要通路”。這樣便帶來了個問題,讓它在90%的時間裡告訴核心:“這是個合法位址,去吧!”是件很無聊的事情。既然該函數對非法位址無能為力,我們幹脆就什麼都不要幹,直到核心真通路到一個非法位址時再告訴調用者:“噢,抱歉,您通路到一個非法位址。”不管用哪種方法,調用者遇到非法位址最終結果都是獲得一個錯誤碼,但後者明顯省下了對合法位址進行檢查的開銷。讓我們來看看如何用自定義section完成這個任務。

如果你順着copy_from_user()向下找幾層,會看到__get_user_asm宏,該宏展開後可讀性太差,我們用下面的僞代碼來描述它:

1:      movb (%from),(%to) 

2:

.section .fixup,"ax"

3:      movl $ERROR_CODE,%eax

xorb %dl,%dl

jmp 2b

.section __ex_table,"a"

.align 4

.long 1b,3b

上面的僞代碼描述了__get_user_asm宏的用途,它将使用者态位址from中的内容拷貝到核心位址to。當from是個非法位址時,會産生page

fault進而執行核心的do_page_fault(),在進行一系列檢查處理後fixup_exception()被調用,該函數會調用search_exception_tables()查找exception

table,将EIP設定成對應handler的位址并傳回。至此該非法位址造成的錯誤就交由exception table中的handler處理了。

所有問題的歸結到了exception table的建立和錯誤處理handler的設定。其實上面的僞代碼已經告訴我們答案了。首先,标号”1”代表了可能産生page

fault的EIP,當page fault産生時這個位址會被記錄在struct pt_regs的ip字段中(不知道的看看do_page_fault()的參數);其次,标号”3”是錯誤處理handler的位址,很明顯,它隻是傳回了一個錯誤碼(EAX是x86的傳回值寄存器)。jmp

2b跳到了産生page fault的指令的下一條指令繼續執行。這裡

.section .fixup,"ax"

建立了名為.fixup的自定義section,并将整個handler放入其中。标号”1”後的代碼是位于.text

section的,故你看到它們在源代碼裡寫在了一起,但在目标檔案中去是分開的,它們在不同的section。

好了,我們已經有了會産生錯誤的代碼位址,也有了錯誤處理handler的位址,

.section __ex_table,"a"

将它們放到了自定義的__ex_table section中(.long 1b,3b),以如下格式存放:

出錯位址,處理函數位址

核心用結構體struct exception_table_entry表示該格式,定義如下:

struct exception_table_entry {

unsigned long insn, fixup;

};

很明顯,exception table的格式簡單,表項的前4個位元組是出錯位址,後4個位元組是處理函數的位址。下圖展示了通過exception

table解決一次通路使用者态非法位址産生的錯誤。

linux核心連接配接腳本,linux核心連結腳本詳解

圖3.通過execption table處理非法使用者态位址通路的過程

怎樣,所有的事實都清楚了。當在核心在不同位置調用copy_from_user()時,展開的__get_user_asm宏都會将可能出錯的位址和處理函數的位址存入該源檔案對應.o檔案的__ex_table

section中。連結腳本的如下代碼:

__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {

__start___ex_table = .;

*(__ex_table)

__stop___ex_table = .;

} :text = 0x9090

将分散的__ex_table收集起來産生一張完整的exception表,并将表的起始位址和結束位址記錄在__start___ex_table和__stop___ex_table兩個全局變量中,進而search_exception_tables()函數可以順利的索引該表。

這種通過自定義section和連結腳本構造表的技巧被大量使用,後面我們還會看到兩個例子。在此先告一段落。

3.3.2 Note Segment

前面提到核心image分為三個segment,其中就有note segment,它是包含在text segment中的。NOTE

segment被用于不同的vendor在ELF檔案中添加一些辨別,讓運作這些二進制代碼的系統确定能否為該ELF提供其所需要的系統調用接口。它對我們了解核心用處不大,詳細内容參見參考文獻2。

NOTES :text :note

上面代碼中,NOTE是一個宏,展開的格式和建構其它section的格式一樣,這裡”:text:note”表示把生成的section即加入text

segment又加入note segment。從objdump的内容可以看到後者包含在前者之中,如下:

LOAD off    0x00001000 vaddr 0xc1000000 paddr 0x01000000 align 2**12

filesz 0x004de000 memsz 0x004de000 flags r-x

NOTE off    0x0037b844 vaddr 0xc137a844 paddr 0x0137a844 align 2**2

filesz 0x00000024 memsz 0x00000024 flags ---

LOAD off    0x00001000 vaddr 0xc1000000 paddr 0x01000000 align 2**12

filesz 0x004de000 memsz 0x004de000 flags r-x

NOTE off    0x0037b844 vaddr 0xc137a844 paddr 0x0137a844 align 2**2

filesz 0x00000024 memsz 0x00000024 flags ---

3.3.3 .rodata section的構造

前面提到,隻讀資料被放到了text segment,連結腳本中的RODATA宏完成了這項工作。RODATA建立了大量不同名稱的section,它們有些是内置的,有些則是自定義的。建立方式并無特别之處,有了前面的知識,你可以輕易的看懂它們。這裡要說的是關于自定義section的第二個例子——核心符号表。

讀過LDD的朋友都知道,在module中導出符号給核心其它部分應該使用__ksymtab,我們也經常在核心中看到類似的代碼,如:

EXPORT_SYMBOL(boot_cpu_data);

但,核心是怎麼做的?符号表如何被建立?如果你看了/path_to_your_kernel_src/include/linux/module.h中EXPORT_SYMBOL的定義,再配合自定義section的知識,很快就能明白核心隻是建立了一個名為__ksymtab的自定義section,當調用EXPROT_SYMBOL宏時會生成一個struct

kernel_symbol變量記錄下函數/資料的名稱和位址,最後将這個變量存入__ksymtab section中。RODATA宏的如下代碼:

                \

__ksymtab         : AT(ADDR(__ksymtab) - LOAD_OFFSET) {        \

VMLINUX_SYMBOL(__start___ksymtab) = .;                 \

*(__ksymtab)                                     \

VMLINUX_SYMBOL(__stop___ksymtab) = .;                 \

}                                                      \

\

将輸入檔案中的__kysmtab section合并生成新的__ksymtab section,這就是核心最終的導出符号表,同樣,__start___ksymtab和__stop___ksymtab記錄下了表的起始位址和結束位址。如此一來,動态加載module時核心如何将module中調用的函數替換成相應的位址就不難了解了吧。

連結腳本知識:

或許你已經注意到,上述建立__ksymtab section的代碼中,并沒有在最後加上:text标明将該section放到text

segment。實際上這是連結腳本的一個簡化,當沒有為section指定segment時,以上一個明确指定的segment為準。例如之前最後一次明确指定segment的__ex_table

section指定了text segment,則其後沒有指定segment的section也被放到了text

segment,直到下一次明确指定segment的section出現為止。

3.3.4 Data Segment的建構

從現在開始,所有的section都歸屬于data segment。與text segment不同的是,data segment的所有section都是可寫的。實際上我已經不需要繼續寫下去,因為data segment的建立中并沒有新奇的連結腳本文法出現。有趣的是,我們發現大量的代碼編譯後産生的二進制也被放到了data segment(從常理看,它們應該被放到text segment)。這些代碼在核心裡非常常見,都被__init宏或__initcall宏修飾。核心把它們放到data segment的原因很簡單,它們隻在初始化階段有用,一旦進入正常運作階段,這些代碼所在的頁面将被回收以作它用。同樣會被回收的section還有被__initdata、__setup_param等宏修飾的變量。這些宏會生産名為.init.*或*.init的自定義section,核心在初始化完成後回收它們占用的頁面。如何回收這些頁面的内容不在本文讨論範圍之内,感興趣的朋友可以grep

free_initmem()函數,看看核心在何時調用它。這裡我們舉自定義section的第三個例子,從技術上來說它和前兩個例子并沒有什麼差别,它較常見于核心代碼但多數人不一定了解它的原理,故這裡特别提出來說一下。

我們經常在驅動或核心子系統的代碼中看到由__initcall、fs_initcall、arch_initcall等類似的宏修飾的函數名,例如:

fs_initcall(acpi_event_init);

很多資料告訴我們這些宏定義了函數的初始化級别,核心會在初始化的不同階段調用它們,級别從0~7不等。實際上這也是自定義section的應用,原理跟核心符号表的建立一樣。尋根究底,這些宏都是由宏__define_initcall生成的,其定義如下:

#define __define_initcall(level,fn,id) \

static initcall_t __initcall_##fn##id __used \

__attribute__((__section__(".initcall" level ".init"))) = fn

其中level參數即0~7(實際是0~7s)共14個級别,這樣核心在編譯時會生産14個名為”.initcall.(0~7s).init”的section,例如.initcall0.init、.initcall2s.init等。被不同宏修飾的函數被放到對應的section中,例如上例的fs_initcall(acpi_event_init)最終會被放到.initcall5.init。

連結腳本的如下代碼:

.initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {

__initcall_start = .;

INITCALLS

__initcall_end = .;

}

生成了initcall表,起始位址和結束位址存在__initcall_start和__initcall_end中。很多資料說根據level參數的值不同,核心在不同階段調用這些函數。但從代碼來看,我認為并非如此,核心隻分兩個階段調用,即early initcall階段和剩餘階段(level = 0~7s)。感興趣的朋友可以看看上面INITCALLS宏的展開以及do_initcalls()、do_pre_smp_initcalls()兩個函數,很容易就能明白。

在建構data segment的最後部分,我們看到如下代碼:

.bss : AT(ADDR(.bss) - LOAD_OFFSET) {

__init_end = .;

__bss_start = .;             

*(.bss.page_aligned)

*(.bss)

. = ALIGN(4);

__bss_stop = .;

_end = . ;

. = ALIGN(PAGE_SIZE);

pg0 = . ;

}

它告訴我們.bss section位于data segment的最後,變量pg0存放的是“Provisional kernel Page Tables”的位址,不熟悉的朋友可以閱讀ULK3的2.5.5.1節。

最後,我們列出幾個著名的由連結腳本提供的全局變量:

名稱

描述

_text

text segment的起始位址,也是核心image的起始位址

_etext

核心代碼段的結束位址,僅僅是代碼段,因為text segment還包含.rodata、exception table、note segment

_edata

不好描述具體含義,見下圖

_end

核心image的結束位址

上述描述不一定準确,實際上你隻要在連結腳本中一看它們出現的位置就能很快知道其含義,也可以在合适的位置列印它們的值驗證一下,例如setup_arch()函數中。讀過ULK的朋友一定想起一副熟悉的圖,把它粘貼如下:

圖4. 著名的全局變量布局(摘自《Understanding Linux Kernel》)

前面我們提過并非所有輸入檔案中的section都會出現在目标檔案中,對于不感興趣的section,連結腳本用下列代碼抛棄它們。

/DISCARD/ : {

*(.exitcall.exit)

}

4. 做一些嘗試

通過學習核心連結腳本,筆者最大的收獲不是了解了核心image的布局,而是通過自定義的section并配合連結腳本來建構動态表的方式(之是以說是動态表,是因為它的長度由加入該section的元素個數決定,并非事先定義好的)。也許你說一個全局的大數組也可以做到,但這樣壞處是數組要預定義到最夠大,其次它占用的記憶體在核心運作時釋放不掉,最糟糕的是這個數組必須在整個核心空間共享,這樣你才能在需要往裡添加元素的時候通路到它。這種污染整個名字空間的設計無疑是糟糕的,把工作交給連結器是最好的選擇。

最後我建議感興趣的朋友嘗試試試這種方式,把你自定義的section放到data segment,你會發現資料在section中的排列順序和連結器從輸入檔案中抽取section的順序有關。嗯,exception文檔提到.text section沒有這個問題,還需要研究研究 ……