[kernel 啟動流程] 前篇——vmlinux.lds分析
以下例子都以project X專案tiny210(s5pv210平臺,armv7架構)為例
[kernel 啟動流程]系列:
建議參考文件:
================================================
一、基礎部分
1、段說明
- text段
程式碼段,通常是指用來存放程式執行程式碼的一塊記憶體區域。這部分割槽域的大小在程式執行前就已經確定。 - data段
資料段,通常是指用來存放程式中已初始化的全域性變數的一塊記憶體區域。資料段屬於靜態記憶體分配。 - bss段
通常是指用來存放程式中未初始化的全域性變數和靜態變數的一塊記憶體區域。BSS段屬於靜態記憶體分配。 - init段
linux定義的一種初始化過程中才會用到的段,一旦初始化完成,那麼這些段所佔用的記憶體會被釋放掉,後續會繼續說明
2、各種地址說明
- 地址解釋
- 載入地址:程式中指令和變數等載入到RAM上的地址。
- 執行地址:CPU執行一條程式中指令時的執行地址,也就是PC暫存器中的值。更簡單的講,就是要定址到一個指令或者變數所使用的地址。
- 連結地址:連結過程中連結器為指令和變數分配的地址。
- 地址之間聯絡
注意,執行地址並不一定完全和連結地址相同,也不一定完全和載入地址相同。
- 如果沒有開啟MMU,並且使用的是位置相關設計,那麼載入地址、執行地址、連結地址三者需要一致。
需要保證連結地址和載入地址是一致的,否則會導致程式跑飛,從uboot上可以理解。 - 當開啟MMU之前,如果使用的是位置無關設計,那麼執行地址和載入地址應該是一致的
例如kernel在開啟mmu之前,使用的是位置無關設計,其執行地址和載入地址一致。關於位置無關設計請自行度娘。 - 如果打開了MMU,那麼執行地址和連結地址相同。
硬體會根據執行地址進行計算並自動定址到對應的載入地址上。
- 如果沒有開啟MMU,並且使用的是位置相關設計,那麼載入地址、執行地址、連結地址三者需要一致。
- 舉例說明
以s5pv210為例
- uboot(BL2)階段並沒有開啟MMU,並且其使用的是位置相關設計,所以其載入地址和連結地址都需要設定成相同,
也就是載入地址是0x23E00000,連結地址也是0x23E00000,執行地址也就和這兩者一致,也就是 - kernel啟動過程中,在MMU開啟之前,使用的是位置無關設計,
核心映象載入地址是0x20008000,連結地址是0xc0008000,執行地址是0x20008000. - 開啟MMU之後,
核心映象載入地址是0x20008000,連結地址是0xc0008000,執行地址是0xc0008000.
- uboot(BL2)階段並沒有開啟MMU,並且其使用的是位置相關設計,所以其載入地址和連結地址都需要設定成相同,
二、連結指令碼語言
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
第3行指示,連結地址為0x100000;即指定了後面的text段的連結地址
__第4行指示:輸出檔案的text段內容由所有目標檔案(,理解為所有的.o檔案,.o)的text段組成;
注意理解.text : { (.text) }的用法,冒號前面.text表示這個段的名稱,{.text}則表示所有目標檔案的text段.__
第5行指示:連結地址變了,變為0x8000000;即重新指定了後面的data段的連結地址;
第6行指示:輸出檔案的data端由所有目標檔案的data段組成;
第7行指示:輸出檔案的bss端由所有目標檔案的bss段組成;
三、vmlinux.lds.S分析
__關於vmlinux.lds.S的分析我不建議直接去從頭看到尾。
在本文裡面也是先分析一個大的框架,然後在第四節和第五節中分析一些細節和例子。__
0、一些有助於我們分析vmlinux.lds.S的東西
- kernel在啟動過程中會列印一些和memory資訊相關的log
Memory: 514112K/524288K available (2128K kernel code, 82K rwdata, 696K rodata, 1024K init, 204K bss, 10176K reserved, 0K cma-reserved)
Virtual kernel memory layout:
vector : 0xffff0000 - 0xffff1000 ( 4 kB)
fixmap : 0xffc00000 - 0xfff00000 (3072 kB)
vmalloc : 0xe0800000 - 0xff800000 ( 496 MB)
lowmem : 0xc0000000 - 0xe0000000 ( 512 MB)
modules : 0xbf000000 - 0xc0000000 ( 16 MB)
.text : 0xc0008000 - 0xc03c228c (3817 kB)
.init : 0xc0400000 - 0xc0500000 (1024 kB)
.data : 0xc0500000 - 0xc0514ba0 ( 83 kB)
.bss : 0xc0514ba0 - 0xc0547d74 ( 205 kB)
這部分log在mm/page_alloc.c中的mem_init_print_info函式中列印。
這裡我們著重關注連線過程中的一些段的位置:
.text : 0xc0008000 - 0xc03c228c (3817 kB)
.init : 0xc0400000 - 0xc0500000 (1024 kB)
.data : 0xc0500000 - 0xc0514ba0 ( 83 kB)
.bss : 0xc0514ba0 - 0xc0547d74 ( 205 kB)
- 編譯之後生成的System.map檔案
System.map是核心的核心符號表,在這裡可以找到函式地址,變數地址,包括一些連結過程中的地址定義等等,
build/out/linux/System.map(這裡列出一些關鍵部分)
c0008000 T _text
c0008000 T stext
c0100000 T _stext
c03c228c T _etext
c0400000 T __init_begin
c0500000 D __init_end
c0500000 D _data
c0500000 D _sdata
c0514ba0 D _edata
c0514ba0 B __bss_start
c0547d74 B __bss_stop
c0547d74 B _end
可以看出和上述(1)中是匹配的。
- 通過反彙編命令對vmlinux進行反彙編,可以解析出詳細的彙編程式碼,包括了一些地址
指令如下:
./arm-none-linux-gnueabi-4.8/bin/arm-none-linux-gnueabi-objdump -D out/linux/vmlinux > vmlinux_objdump.txt
- 通過arm-readelf -s vmlinux檢視各個段的佈局
hlos@node4:linux$ readelf -S vmlinux
There are 39 section headers, starting at offset 0x1ed6388:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .head.text PROGBITS 80008000 008000 000220 00 AX 0 0 32
[ 2] .text PROGBITS 80100000 010000 1f8ba0 00 AX 0 0 64
[ 3] .fixup PROGBITS 802f8ba0 208ba0 000028 00 AX 0 0 4
[ 4] .rodata PROGBITS 80300000 210000 0952e8 00 A 0 0 64
[ 5] __bug_table PROGBITS 803952e8 2a52e8 002418 00 A 0 0 4
[ 6] __ksymtab PROGBITS 80397700 2a7700 0042d0 00 A 0 0 4
1、整體的段結構
vmlinux.lds.S的段基本上會按照如下格式進行組織。
參考include/asm-generic/vmlinux.lds.h註釋部分
* OUTPUT_FORMAT(...)
* OUTPUT_ARCH(...)
* ENTRY(...)
* SECTIONS
* {
* . = START;
* __init_begin = .;
* HEAD_TEXT_SECTION
* INIT_TEXT_SECTION(PAGE_SIZE)
* INIT_DATA_SECTION(...)
* PERCPU_SECTION(CACHELINE_SIZE)
* __init_end = .;
*
* _stext = .;
* TEXT_SECTION = 0
* _etext = .;
*
* _sdata = .;
* RO_DATA_SECTION(PAGE_SIZE)
* RW_DATA_SECTION(...)
* _edata = .;
*
* EXCEPTION_TABLE(...)
* NOTES
*
* BSS_SECTION(0, 0, 0)
* _end = .;
*
* STABS_DEBUG
* DWARF_DEBUG
*
* DISCARDS // must be the last
* }
*
* [__init_begin, __init_end] is the init section that may be freed after init
* // __init_begin and __init_end should be page aligned, so that we can
* // free the whole .init memory
* [_stext, _etext] is the text section
* [_sdata, _edata] is the data section
*
* Some of the included output section have their own set of constants.
* Examples are: [__initramfs_start, __initramfs_end] for initramfs and
* [__nosave_begin, __nosave_end] for the nosave data
*/
如上述描述,主要分成了幾個區間
* __init_begin - __init_end區間:
核心把一些初始化才會使用到的段(並不侷限於資料段或者程式碼段,也可以是自己定義的段),簡稱初始化相關段,放在這個區間裡,一旦初始化完成,那麼這個區間裡的資料或程式碼在後面就不會被使用,核心會把這部分記憶體釋放出來。
* _stext - _etext區間:
存放核心的程式碼段,正文
* _sdata - _edata區間:
存放data段,包括只讀data段和可讀可寫資料段。
* bss段
2、__init_begin - __init_end區間定義段
arch/arm/kernel/vmlinux.lds.S
__init_begin = .;
...
INIT_TEXT_SECTION(8)
.exit.text : {
ARM_EXIT_KEEP(EXIT_TEXT)
}
.init.proc.info : {
ARM_CPU_DISCARD(PROC_INFO)
}
.init.arch.info : {
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
}
...
.exit.data : {
ARM_EXIT_KEEP(EXIT_DATA)
}
__init_end = .;
在__init_begin和__init_end之間定義了很多初始化過程中會使用到的段。具體例子在後面會說明。
從System.map可以看到對應地址如下:
c0400000 T __init_begin
c0500000 D __init_end
- 疑問:為什麼exit也放在這裡?
- 補充知識:
這個區間的記憶體會在初始化完成後被free,具體程式碼在init/main.c
static int __ref kernel_init(void *unused)
{
free_initmem();
}
void free_initmem(void)
{
poison_init_mem(__init_begin, __init_end - __init_begin);
}
3、_stext - _etext區間定義段
.head.text : {
_text = .;
HEAD_TEXT
}
.text : { /* Real text segment */
_stext = .; /* Text and read-only data */
IRQENTRY_TEXT
SOFTIRQENTRY_TEXT
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
HYPERVISOR_TEXT
KPROBES_TEXT
_etext = .; /* End of text and rodata section */
注意_stext和_etext的定義位置。但是真正的文字段是從_text開始的。
各部分的程式碼段都被放到了這個區間,注意,只讀資料段也放到這裡來了。
從System.map可以看到對應地址如下:
c0008000 T _text
c0008000 T stext
c0100000 T _stext
c03c228c T _etext
4、_sdata - _edata區間
__data_loc = .;
.data : AT(__data_loc) {
_data = .; /* address in memory */
_sdata = .;
INIT_TASK_DATA(THREAD_SIZE)
NOSAVE_DATA
CACHELINE_ALIGNED_DATA(L1_CACHE_BYTES)
READ_MOSTLY_DATA(L1_CACHE_BYTES)
DATA_DATA
CONSTRUCTORS
_edata = .;
}
_edata_loc = __data_loc + SIZEOF(.data);
注意_sdata和_edata的定義位置。
各部分的資料段都被放到了這個區間。
從System.map可以看到對應地址如下:
c0500000 D _data
c0500000 D _sdata
c0514ba0 D _edata
5、bss段定義
BSS_SECTION(0, 0, 0)
_end = .;
include/asm-generic/vmlinux.lds.h
#define BSS_SECTION(sbss_align, bss_align, stop_align) \
. = ALIGN(sbss_align); \
VMLINUX_SYMBOL(__bss_start) = .; \
SBSS(sbss_align) \
BSS(bss_align) \
. = ALIGN(stop_align); \
VMLINUX_SYMBOL(__bss_stop) = .;
從System.map看出對應地址如下:
c0514ba0 B __bss_start
c0547d74 B __bss_stop
四、vmlinux.lds.S更多說明
1、入口
有很多不同的方法來設定入口點.連結器會通過按順序嘗試一下方法來設定入口點,如果成功了,就會停止.
<1> ’-e’ 入口命令列選項
<2> 連結指令碼中的ENTRY(SYMBOL)命令
<3> 如果定義了start,就使用start的值
<4> 如果存在就使用’.text’段的首地址
<5> 地址’0’
arm/arch/kernel/vmlinux.lds.S指定入口地址如下:
ENTRY(stext)
說明其入口地址是stext,在arch/arm/kernel/head.S中。
注意:也就是說kernel啟動的入口在這裡,後續分析kernel啟動流程就是從這裡開始分析的。
2、連線地址
為什麼stext的地址是0xc0008000呢?(通過System.map檢視的)。
連結器是通過vmlinux.lds.S連結指令碼來進行地址定義的。但是如果起始地址不為0的話,我們需要在連結指令碼中為其指定一個起始地址。
arm/arch/kernel/vmlinux.lds.S指定起始連線地址如下(所謂的起始連線地址就是在入口的時候對’.’進行賦值):
. = PAGE_OFFSET + TEXT_OFFSET;
- PAGE_OFFSET表示核心空間的起始地址。
定義位置如下:
./arch/arm/include/asm/memory.h
/* PAGE_OFFSET - the virtual address of the start of the kernel image */
#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)
CONFIG_PAGE_OFFSET在配置Kconfig的時候會被設定
arch/arm/Kconfig
config PAGE_OFFSET
hex
default PHYS_OFFSET if !MMU
default 0x40000000 if VMSPLIT_1G
default 0x80000000 if VMSPLIT_2G
default 0xB0000000 if VMSPLIT_3G_OPT
default 0xC0000000
預設情況下是0xC0000000。可以通過配置VMSPLIT來進行修改。
* TEXT_OFFSET表示核心在RAM中的起始位置相對於RAM起始地址偏移。
定義位置如下:
./arch/arm/Makefile
# The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)
# Text offset. This list is sorted numerically by address in order to
# provide a means to avoid/resolve conflicts in multi-arch kernels.
textofs-y := 0x00008000
也就是說預設情況下是0x00008000。
拓展:為什麼要有0x8000的偏移?
因為kernel映象的前16K需要預留出來給初始化頁表項使用。這裡先暫時瞭解一下,後續研究kernel啟動流程會遇到,再學習。
對應程式碼arch/arm/kernel/head.S,這裡的註釋也提到了。
/*
* swapper_pg_dir is the virtual address of the initial page table.
* We place the page tables 16K below KERNEL_RAM_VADDR. Therefore, we must
* make sure that KERNEL_RAM_VADDR is correctly set. Currently, we expect
* the least significant 16 bits to be 0x8000, but we could probably
* relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
*/
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif
五、例子:initcall
1、說明
initcall的功能和使用不詳細說明了,簡單一個例子如下:
core_initcall(pm_init);
initcall又分成很多等級,各個等級主要是排程時機不一樣,core_initcall也屬於其中一個等級
include/linux/init.h
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
2、段連結位置
include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level) \
VMLINUX_SYMBOL(__initcall##level##_start) = .; \
*(.initcall##level##.init) \
*(.initcall##level##s.init) \
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
*(.initcallearly.init) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
VMLINUX_SYMBOL(__initcall_end) = .;
*(.initcall##level##.init),例如level為1,則表示由所有目標檔案中的.initcall1.init段組成。
所以程式碼中所要實現的,就是往.initcall1.init這個段裡新增資料結構。
檢視System.map檔案,__initcall1_start符號如下
c0427e98 T __initcall1_start
...
c0427eac t __initcall_pm_init1
c0427eb0 t __initcall_init_jiffies_clocksource1
c0427eb4 t __initcall_cpu_pm_init1
c0427ed8 t __initcall_s5pv210_audss_clk_init1
c0427edc T __initcall2_start
3、initcall實現(往initcall段裡新增資料結構)
include/linux/init.h
以arch_initcall為例:
#define core_initcall(fn) __define_initcall(fn, 1)
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
core_initcall(pm_init);定義了一個名稱為__initcall_pm_init1的initcall_t的資料結構,並且放在.initcall1.init段中。
也就實現了上述2說的,往.initcall1.init這個段裡新增資料結構。
4、initcall段的使用
- kernel把幾個initcall的段的起始地址都放到__initdata中:
init/main.c
extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
...
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
...
__initcall7_start,
__initcall_end,
};
__initcall#_start存放了每個initcall段的起始地址。
通過上述結構體,就將__initcall1.init段中的資料結構放在__initcall1_start結構體裡面了。
並且將所有initcall段裡的資料結構initcall_t統一放到了initcall_levels裡。
* 排程流程如下
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
static void __init do_initcall_level(int level)
{
initcall_t *fn;
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
從initcall_levels獲取各個initcall資料段的起始地址__initcall_start,然後呼叫do_one_initcall進行執行。
通過如上,就完成了initcall的段中的函式呼叫。
七、例子:以__earlycon_table為例
在《earlycon實現流程》中我們知道earlycon_id都是被存放到__earlycon_table段中。
以下我們看__earlycon_table段是怎麼被連結的。
1、新增一個數據結構到一個段中。
OF_EARLYCON_DECLARE(s5pv210, "samsung,s5pv210-uart",
s5pv210_early_console_setup);
定義如下:
#define OF_EARLYCON_DECLARE(_name, compat, fn) \
static const struct earlycon_id __UNIQUE_ID(__earlycon_##_name) \
__used __section(__earlycon_table) \
= { .name = __stringify(_name), \
.compatible = compat, \
.setup = fn }
__section(S) __attribute__ ((__section__(#S)))
拓展為:attribute ((section(__earlycon_table))),使用__earlycon_table段來存放資料結構。
2、這個段的資料的使用
static int __init early_init_dt_scan_chosen_serial(void)
{
const struct earlycon_id *match;
for (match = __earlycon_table; match < __earlycon_table_end; match++) {
}
直接獲取__earlycon_table和__earlycon_table_end,符號表中會找到這兩個地址,earlycon_id的資料結構就放在這裡面。
3、__earlycon_table段的連線過程
include/asm-generic/vmlinux.lds.h
#ifdef CONFIG_SERIAL_EARLYCON
#define EARLYCON_TABLE() STRUCT_ALIGN(); \
VMLINUX_SYMBOL(__earlycon_table) = .; \
*(__earlycon_table) \
VMLINUX_SYMBOL(__earlycon_table_end) = .;
#else
#define EARLYCON_TABLE()
#endif
(__earlycon_table) 由所有輸入檔案(,理解為所有的.o檔案,*.o)的__earlycon_table段組成;
/* init and exit section handling */
#define INIT_DATA \
KERNEL_DTB() \
...
EARLYCON_TABLE()
arch/arm/kernel/vmlinux.lds.S
__init_begin = .;
.init.data : {
...
INIT_DATA
...
}
__init_end = .;
可以觀察到是放在init區間中的。
4、通過System.map檢視這個段的定義位置
build/out/linux/System.map
c0400000 T __init_begin
c04276e0 T __earlycon_table
c0427780 t __UNIQUE_ID___earlycon_s5pv2104
c0427820 t __UNIQUE_ID___earlycon_s3c64003
c04278c0 t __UNIQUE_ID___earlycon_s3c24402
c0427960 t __UNIQUE_ID___earlycon_s3c24121
c0427a00 t __UNIQUE_ID___earlycon_s3c24100
c0427aa0 T __earlycon_table_end
c0500000 D __init_end