一、導(dǎo)讀
本文描述linux內(nèi)核的“頭”究竟是什么,感覺她非常的神秘。
為了解釋“頭”是什么,首先從linux內(nèi)核的鏈接腳本文件vmlinux.lds.S聊起。在linux內(nèi)核中,arch目錄下放置的是關(guān)于linux內(nèi)核所支持架構(gòu)相關(guān)的代碼描述文件。其中在具體架構(gòu)目錄下的kernel目錄中都會(huì)有一個(gè)鏈接腳本文件。
二、鏈接器是什么
現(xiàn)代軟件工程中,一個(gè)大的程序通常都由多個(gè)源文件組成,其中包含以高級(jí)計(jì)算機(jī)語言編寫的源文件以及匯編語言編寫的匯編文件。在編譯構(gòu)建過程中會(huì)分別對(duì)這些源文件進(jìn)行匯編或者編譯并生成目標(biāo)文件,這些目標(biāo)文件包含代碼段、數(shù)據(jù)段、符號(hào)表等內(nèi)容。而鏈接則是把這些目標(biāo)文件的代碼段、數(shù)據(jù)段以及符號(hào)表等內(nèi)容收集起來并按照某種格式(例如ELF)組合成一個(gè)可執(zhí)行二進(jìn)制文件的過程,而這個(gè)過程是使用鏈接器來完成的。
鏈接器在鏈接過程中會(huì)使用到一個(gè)鏈接腳本文件,該文件用于描述鏈接的過程,當(dāng)沒有通過“-T”參數(shù)指定鏈接腳本時(shí),鏈接器會(huì)使用內(nèi)置的鏈接腳本。
三、鏈接腳本
鏈接腳本控制著如何把輸入文件中的段合并到輸出文件的段中,以及這些段的地址空間布局等。本質(zhì)上則是把在編譯構(gòu)建過程中大量的二進(jìn)制文件(例如.o文件)合并成一個(gè)可執(zhí)行的二進(jìn)制文件。
四、linux內(nèi)核的鏈接腳本
本文以ARM64架構(gòu)為例,首先貼上鏈接腳本的完整內(nèi)容,后面會(huì)詳細(xì)描述。linux內(nèi)核針對(duì)ARM64架構(gòu)的鏈接腳本放置于/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文件的開始處,會(huì)使用#include包含頭文件,這一點(diǎn)與C語言類似:
?
#include?#include? #include? #include? #include? #include?"image.h"
?
在以上列出的頭文件中,大多會(huì)使用宏定義方式編寫特定段的描述內(nèi)容,用于在vmlinux.lds.S文件中引用。
4-2 參數(shù)設(shè)置和宏定義描述
OUTPUT_ARCH(aarch64)語句用于輸出處理器體系架構(gòu)格式。
ENYRY(_text)語言用于設(shè)置程序的入口為_text,程序執(zhí)行的第一條指令稱為入口點(diǎn)(entry point)。除了這種方式,還有其他的方式設(shè)置入口點(diǎn):
(1)在GCC工具鏈的LD命令通過“-e”參數(shù)指定入口點(diǎn)。
(2)在鏈接腳本中通過ENTRY命令設(shè)置入口點(diǎn)。
(3)通過特定符號(hào)(例如start符號(hào))設(shè)置入口點(diǎn)。
(4)使用代碼段的起始地址。
(5)使用地址0。
在上述五種方式中,鏈接器會(huì)依次嘗試設(shè)置入口點(diǎn),直到成功為止。
接下來設(shè)置jiffies參數(shù)值: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內(nèi)容分析
SECTIONS{}是鏈接腳本語法中的關(guān)鍵命令,用于描述輸出文件的內(nèi)存布局。SECTIONS命令告訴鏈接文件如何把輸入文件的段映射到輸出文件的各個(gè)段中,如何將輸入端整合為輸出段,如何把輸出段放入程序地址空間和進(jìn)程地址空間中。
在開始之前,先描述兩個(gè)linux內(nèi)核中重要的知識(shí)點(diǎn):
(1)在鏈接腳本中,有一個(gè)特殊的符號(hào):".",用于表示當(dāng)前位置計(jì)數(shù)器。在vmlinux.lds.S文件中很多地方都會(huì)使用到。
(2)在鏈接腳本中有一個(gè)常用的編程技巧:為每個(gè)段(或者多個(gè)段)設(shè)置一些符號(hào),用于標(biāo)識(shí)內(nèi)存位置的開始和結(jié)束,這樣便可以在C語言代碼中訪問每個(gè)段(或者多個(gè)段)的起始地址和結(jié)束地址。該技巧在linux內(nèi)核以及u-boot源碼中較為常見。
在SECTIONS{}中最先開始的是:
?
?/DISCARD/?:?{ ??ARM_EXIT_DISCARD(EXIT_TEXT) ??ARM_EXIT_DISCARD(EXIT_DATA) ??EXIT_CALL ??*(.discard) ??*(.discard.*) ?}
?
/DISCARD/ 是一個(gè)特殊的輸出段,被該段引用的任何輸入段將不會(huì)出現(xiàn)在輸出文件中。
接著是_text段:
?
?.?=?PAGE_OFFSET?+?TEXT_OFFSET; ?.head.text?:?{ ??_text?=?.; ??HEAD_TEXT ?}
?
上述. = PAGE_OFFSET + TEXT_OFFSET;意思是把代碼段的鏈接地址設(shè)置為PAGE_OFFSET + TEXT_OFFSET的計(jì)算值。PAGE_OFFSET表示內(nèi)核空間和用戶空間對(duì)虛擬地址空間的劃分,TEXT_OFFSET表示代碼段的偏移地址。
開始的是.head.text輸出段,對(duì)應(yīng)的輸入段為HEAD_TEXT,本質(zhì)為*(.head.text)。意思是將所有目標(biāo)文件中的.head.text段放入.head.text輸出段中。其中_text = .;用于標(biāo)識(shí)_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??*/ ?}
?
上述代碼會(huì)匯集目標(biāo)文件中的多個(gè)輸入段到.text中。例如:.exception.text、.irqentry.text、.sched.text、.spinlock.text、.hyp.idmap.text等。
接下來則是RO_DATA(PAGE_SIZE)宏代表的只讀數(shù)據(jù)段,該宏定義非常長(此處不展開)。緊隨其后的是異常表段:
?
#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)?=?.;??? ?}
?
上述內(nèi)容就是text和rodata段的定義了,最后以_etext = .位置計(jì)數(shù)器結(jié)束。
接下來是與初始化相關(guān)的段,由[__init_begin , __init_end]符號(hào)標(biāo)識(shí):
?
?__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兩個(gè)輸出段。
后面是[_data , _edata]符號(hào)代表的數(shù)據(jù)相關(guān)段:
?
?_data?=?.; ?_sdata?=?.; ?RW_DATA_SECTION(64,?PAGE_SIZE,?THREAD_SIZE) ?PECOFF_EDATA_PADDING ?_edata?=?.;
?
然后是BSS相關(guān)段:BSS_SECTION(0, 0, 0)。
最后以_end = .;標(biāo)識(shí)linux內(nèi)核的結(jié)束。然后還放置了與stab相關(guān)的調(diào)試段:
?
#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)?}
?
在內(nèi)存布局的最后會(huì)放置HEAD_SYMBOLS代表的三個(gè)符號(hào)標(biāo)志:
?
#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是信息標(biāo)志(小端序)。
_kernel_offset_le表示linux內(nèi)核鏡像的有效大?。ㄐ《诵颍?/p>
在內(nèi)核鏡像生成過程中,上述三個(gè)符號(hào)標(biāo)志代表的值會(huì)作為鏡像頭的一部分輸出。
五、linux內(nèi)核的“頭”
上述內(nèi)容對(duì)linux內(nèi)核的vmlinux.lds.S進(jìn)行了描述,已經(jīng)知道在內(nèi)存布局的開始處放置的是.head.text輸出段,這正是linux內(nèi)核的“頭”,對(duì)應(yīng)的輸入段為*(.head.text)。在linux內(nèi)核源碼中,在arch/arm64/kernel/head.S文件中則描述了.head.text段:
六、總結(jié)
本文主要描述了linux內(nèi)核針對(duì)ARM64的鏈接腳本文件vmlinux.lds.S,尋找linux內(nèi)核鏡像的入口點(diǎn)。不同架構(gòu)下的vmlinux.lds.S文件內(nèi)容大多不同,需要具體查看。
總而言之,linux內(nèi)核鏡像中的組成內(nèi)容由鏈接腳本控制,從鏈接腳本和head.S啟動(dòng)匯編代碼中可以尋找到linux內(nèi)核鏡像的入口點(diǎn)。
審核編輯:湯梓紅
評(píng)論