一、導讀
本文描述linux內核的“頭”究竟是什么,感覺她非常的神秘。
為了解釋“頭”是什么,首先從linux內核的鏈接腳本文件vmlinux.lds.S聊起。在linux內核中,arch目錄下放置的是關于linux內核所支持架構相關的代碼描述文件。其中在具體架構目錄下的kernel目錄中都會有一個鏈接腳本文件。
二、鏈接器是什么
現代軟件工程中,一個大的程序通常都由多個源文件組成,其中包含以高級計算機語言編寫的源文件以及匯編語言編寫的匯編文件。在編譯構建過程中會分別對這些源文件進行匯編或者編譯并生成目標文件,這些目標文件包含代碼段、數據段、符號表等內容。而鏈接則是把這些目標文件的代碼段、數據段以及符號表等內容收集起來并按照某種格式(例如ELF)組合成一個可執行二進制文件的過程,而這個過程是使用鏈接器來完成的。
鏈接器在鏈接過程中會使用到一個鏈接腳本文件,該文件用于描述鏈接的過程,當沒有通過“-T”參數指定鏈接腳本時,鏈接器會使用內置的鏈接腳本。
三、鏈接腳本
鏈接腳本控制著如何把輸入文件中的段合并到輸出文件的段中,以及這些段的地址空間布局等。本質上則是把在編譯構建過程中大量的二進制文件(例如.o文件)合并成一個可執行的二進制文件。
四、linux內核的鏈接腳本
本文以ARM64架構為例,首先貼上鏈接腳本的完整內容,后面會詳細描述。linux內核針對ARM64架構的鏈接腳本放置于/arch/arm64/kernel/vmlinux.lds.S文件中:
?
/* * ld script to make ARM Linux kernel * taken from the i386 version by Russell King * Written by Martin Mares*/ #include #include #include #include #include #include "image.h" /* .exit.text needed in case of alternative patching */ #define ARM_EXIT_KEEP(x)x #define ARM_EXIT_DISCARD(x) OUTPUT_ARCH(aarch64) ENTRY(_text) jiffies = jiffies_64; #define HYPERVISOR_TEXT /* * Align to 4 KB so that * a) the HYP vector table is at its minimum * alignment of 2048 bytes * b) the HYP init code will not cross a page * boundary if its size does not exceed * 4 KB (see related ASSERT() below) */ . = ALIGN(SZ_4K); VMLINUX_SYMBOL(__hyp_idmap_text_start) = .; *(.hyp.idmap.text) VMLINUX_SYMBOL(__hyp_idmap_text_end) = .; VMLINUX_SYMBOL(__hyp_text_start) = .; *(.hyp.text) VMLINUX_SYMBOL(__hyp_text_end) = .; /* * The size of the PE/COFF section that covers the kernel image, which * runs from stext to _edata, must be a round multiple of the PE/COFF * FileAlignment, which we set to its minimum value of 0x200. 'stext' * itself is 4 KB aligned, so padding out _edata to a 0x200 aligned * boundary should be sufficient. */ PECOFF_FILE_ALIGNMENT = 0x200; #ifdef CONFIG_EFI #define PECOFF_EDATA_PADDING .pecoff_edata_padding : { BYTE(0); . = ALIGN(PECOFF_FILE_ALIGNMENT); } #else #define PECOFF_EDATA_PADDING #endif #if defined(CONFIG_DEBUG_ALIGN_RODATA) #define ALIGN_DEBUG_RO. = ALIGN(1< phys conversions will fail. */ ASSERT(_text == (PAGE_OFFSET + TEXT_OFFSET), "HEAD is misaligned")
?
4-1 頭文件包含描述
在vmlinux.lds.S文件的開始處,會使用#include包含頭文件,這一點與C語言類似:
?
#include?#include? #include? #include? #include? #include?"image.h"
?
在以上列出的頭文件中,大多會使用宏定義方式編寫特定段的描述內容,用于在vmlinux.lds.S文件中引用。
4-2 參數設置和宏定義描述
OUTPUT_ARCH(aarch64)語句用于輸出處理器體系架構格式。
ENYRY(_text)語言用于設置程序的入口為_text,程序執行的第一條指令稱為入口點(entry point)。除了這種方式,還有其他的方式設置入口點:
(1)在GCC工具鏈的LD命令通過“-e”參數指定入口點。
(2)在鏈接腳本中通過ENTRY命令設置入口點。
(3)通過特定符號(例如start符號)設置入口點。
(4)使用代碼段的起始地址。
(5)使用地址0。
在上述五種方式中,鏈接器會依次嘗試設置入口點,直到成功為止。
接下來設置jiffies參數值:jiffies = jiffies_64;,jiffies_64定義在/kernel/time/timer.c文件中:
接著定義HYPERVISOR_TEXT代碼段:
?
#define?HYPERVISOR_TEXT????? ?.?=?ALIGN(SZ_4K);???? ?VMLINUX_SYMBOL(__hyp_idmap_text_start)?=?.;? ?*(.hyp.idmap.text)???? ?VMLINUX_SYMBOL(__hyp_idmap_text_end)?=?.;? ?VMLINUX_SYMBOL(__hyp_text_start)?=?.;?? ?*(.hyp.text)????? ?VMLINUX_SYMBOL(__hyp_text_end)?=?.;
?
4-3 SECTIONS內容分析
SECTIONS{}是鏈接腳本語法中的關鍵命令,用于描述輸出文件的內存布局。SECTIONS命令告訴鏈接文件如何把輸入文件的段映射到輸出文件的各個段中,如何將輸入端整合為輸出段,如何把輸出段放入程序地址空間和進程地址空間中。
在開始之前,先描述兩個linux內核中重要的知識點:
(1)在鏈接腳本中,有一個特殊的符號:".",用于表示當前位置計數器。在vmlinux.lds.S文件中很多地方都會使用到。
(2)在鏈接腳本中有一個常用的編程技巧:為每個段(或者多個段)設置一些符號,用于標識內存位置的開始和結束,這樣便可以在C語言代碼中訪問每個段(或者多個段)的起始地址和結束地址。該技巧在linux內核以及u-boot源碼中較為常見。
在SECTIONS{}中最先開始的是:
?
?/DISCARD/?:?{ ??ARM_EXIT_DISCARD(EXIT_TEXT) ??ARM_EXIT_DISCARD(EXIT_DATA) ??EXIT_CALL ??*(.discard) ??*(.discard.*) ?}
?
/DISCARD/ 是一個特殊的輸出段,被該段引用的任何輸入段將不會出現在輸出文件中。
接著是_text段:
?
?.?=?PAGE_OFFSET?+?TEXT_OFFSET; ?.head.text?:?{ ??_text?=?.; ??HEAD_TEXT ?}
?
上述. = PAGE_OFFSET + TEXT_OFFSET;意思是把代碼段的鏈接地址設置為PAGE_OFFSET + TEXT_OFFSET的計算值。PAGE_OFFSET表示內核空間和用戶空間對虛擬地址空間的劃分,TEXT_OFFSET表示代碼段的偏移地址。
開始的是.head.text輸出段,對應的輸入段為HEAD_TEXT,本質為*(.head.text)。意思是將所有目標文件中的.head.text段放入.head.text輸出段中。其中_text = .;用于標識_text段的開始。
接下來是.text輸出段:
?
?.text?:?{???/*?Real?text?segment??*/ ??_stext?=?.;??/*?Text?and?read-only?data?*/ ???__exception_text_start?=?.; ???*(.exception.text) ???__exception_text_end?=?.; ???IRQENTRY_TEXT ???TEXT_TEXT ???SCHED_TEXT ???LOCK_TEXT ???HYPERVISOR_TEXT ???*(.fixup) ???*(.gnu.warning) ??.?=?ALIGN(16); ??*(.got)???/*?Global?offset?table??*/ ?}
?
上述代碼會匯集目標文件中的多個輸入段到.text中。例如:.exception.text、.irqentry.text、.sched.text、.spinlock.text、.hyp.idmap.text等。
接下來則是RO_DATA(PAGE_SIZE)宏代表的只讀數據段,該宏定義非常長(此處不展開)。緊隨其后的是異常表段:
?
#define?EXCEPTION_TABLE(align)?????? ?.?=?ALIGN(align);?????? ?__ex_table?:?AT(ADDR(__ex_table)?-?LOAD_OFFSET)?{?? ??VMLINUX_SYMBOL(__start___ex_table)?=?.;??? ??*(__ex_table)?????? ??VMLINUX_SYMBOL(__stop___ex_table)?=?.;??? ?}
?
接下來放置.notes段:
?
#define?NOTES???????? ?.notes?:?AT(ADDR(.notes)?-?LOAD_OFFSET)?{??? ??VMLINUX_SYMBOL(__start_notes)?=?.;??? ??*(.note.*)?????? ??VMLINUX_SYMBOL(__stop_notes)?=?.;??? ?}
?
上述內容就是text和rodata段的定義了,最后以_etext = .位置計數器結束。
接下來是與初始化相關的段,由[__init_begin , __init_end]符號標識:
?
?__init_begin?=?.; ?INIT_TEXT_SECTION(8) ?.exit.text?:?{ ??ARM_EXIT_KEEP(EXIT_TEXT) ?} ?ALIGN_DEBUG_RO_MIN(16) ?.init.data?:?{ ??INIT_DATA ??INIT_SETUP(16) ??INIT_CALLS ??CON_INITCALL ??SECURITY_INITCALL ??INIT_RAM_FS ?} ?.exit.data?:?{ ??ARM_EXIT_KEEP(EXIT_DATA) ?} ?PERCPU_SECTION(64) ?.?=?ALIGN(PAGE_SIZE); ?__init_end?=?.;
?
接著是.altinstructions和.altinstr_replacement兩個輸出段。
后面是[_data , _edata]符號代表的數據相關段:
?
?_data?=?.; ?_sdata?=?.; ?RW_DATA_SECTION(64,?PAGE_SIZE,?THREAD_SIZE) ?PECOFF_EDATA_PADDING ?_edata?=?.;
?
然后是BSS相關段:BSS_SECTION(0, 0, 0)。
最后以_end = .;標識linux內核的結束。然后還放置了與stab相關的調試段:
?
#define?STABS_DEBUG??????? ??.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)?}
?
在內存布局的最后會放置HEAD_SYMBOLS代表的三個符號標志:
?
#define?HEAD_SYMBOLS?????? ?_kernel_size_le??=?DATA_LE64(_end?-?_text);? ?_kernel_offset_le?=?DATA_LE64(TEXT_OFFSET);? ?_kernel_flags_le?=?DATA_LE64(__HEAD_FLAGS);
?
_kernel_offset_le是鏡像從RAM開始加載的偏移量(小端序)。
_kernel_flags_le是信息標志(小端序)。
_kernel_offset_le表示linux內核鏡像的有效大小(小端序)。
在內核鏡像生成過程中,上述三個符號標志代表的值會作為鏡像頭的一部分輸出。
五、linux內核的“頭”
上述內容對linux內核的vmlinux.lds.S進行了描述,已經知道在內存布局的開始處放置的是.head.text輸出段,這正是linux內核的“頭”,對應的輸入段為*(.head.text)。在linux內核源碼中,在arch/arm64/kernel/head.S文件中則描述了.head.text段:
六、總結
本文主要描述了linux內核針對ARM64的鏈接腳本文件vmlinux.lds.S,尋找linux內核鏡像的入口點。不同架構下的vmlinux.lds.S文件內容大多不同,需要具體查看。
總而言之,linux內核鏡像中的組成內容由鏈接腳本控制,從鏈接腳本和head.S啟動匯編代碼中可以尋找到linux內核鏡像的入口點。
審核編輯:湯梓紅
評論