Linux kernel 启动流程分析1 ---vmlinux.lds分析
一、基础部分
1.1、段说明
- text段
代码段,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定。
- data段
数据段,通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
- bss段
通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态内存分配。
- init段
linux定义的一种初始化过程中才会用到的段,一旦初始化完成,那么这些段所占用的内存会被释放掉,后续会继续说明
1. .text 段
- 用途:存储程序的 执行代码。
- 内容:包含程序中所有的可执行指令,例如函数体、逻辑运算等。对于编译后的程序来说,
.text
段通常包含从源代码中编译而来的所有机器指令。 - 特性:
- 通常是 只读(Read-Only),以避免程序在运行时修改自身的代码。
.text
段是 可执行 的(Executable),以便 CPU 可以正确执行其中的指令。
示例:
int add(int a, int b) {
return a + b;
}
这个 add
函数的代码将被存放在 .text
段中。
2. .data 段
- 用途:存储程序中 已初始化的全局变量和静态变量。
- 内容:
.data
段存放的是程序中所有全局变量和静态变量,并且这些变量在编译时就已经有了初始值。 - 特性:
- 已初始化:该段包含程序中已赋值的全局或静态变量。
- 可以是 读写 的(Read-Write),程序在运行过程中可以修改这些变量的值。
示例:
int global_var = 10; // 这会存放在 .data 段
在这个例子中,global_var
变量将存放在 .data
段,因为它有初始值 10。
3. .bss 段
- 用途:存储程序中 未初始化的全局变量和静态变量。
- 内容:
.bss
段用于存放未初始化的全局变量、静态变量以及有默认初始值(如零)的变量。实际上,这些变量在磁盘上并不占用空间,而是在程序加载到内存时由操作系统自动填充为默认值(如零)。 - 特性:
- 未初始化:存放程序中没有显式初始化的全局变量和静态变量。
- 占用的空间仅在内存中分配,不占用磁盘空间(因为它们的初始值是零或者不明确的)。
- 程序在加载时会将这些变量的值初始化为零或者其他默认值。
示例:
static int uninitialized_var; // 这会存放在 .bss 段
uninitialized_var
变量虽然是静态变量,但没有显式初始化,因此它会存放在 .bss
段。
4. .init 段
- 用途:存储 初始化代码,通常包含内核或应用程序的初始化代码。
- 内容:
.init
段是专门为执行初始化操作而分配的内存区域,通常用于存放 初始化函数,这些函数在程序加载时执行。例如,在内核中,.init
段可能包含内核启动时需要执行的初始化代码。 - 特性:
.init
段中的代码在初始化过程中执行,并且在程序执行的其他阶段 不再使用。- 在一些系统中,
.init
段的内容可能会在初始化结束后被释放,以节省内存。
示例:
void __init init_func() {
// 初始化操作
}
在 Linux 内核中,__init
宏将把 init_func
函数放入 .init
段。在内核启动过程中,.init
段会被执行,但在初始化完成后,.init
段的内存可以被释放。
1.2、各种地址说明
地址解释
1. 链接地址(Link Address)
- 定义:链接地址是指在程序 编译阶段,链接器为程序中各个部分分配的虚拟内存地址。这个地址通常是程序在执行前的理想地址,即程序中的各个段(如
.text
、.data
、.bss
等)在链接时的预定地址。 - 作用:链接地址用于在 静态链接 时,决定各个模块(或文件)之间的符号地址。链接器会根据源代码中对变量和函数的引用,计算并确定这些符号的地址。
- 特点:
- 链接地址是在 链接时计算的,它代表了程序模块内部地址的分配,通常不依赖于程序是否被加载到内存。
- 链接地址是 虚拟地址,用于描述程序在内存中的布局,但在程序实际运行时,这些地址可能会发生变化(例如,地址空间布局随机化)。
int global_var = 10; // 链接器将为 global_var 分配一个链接地址
链接地址是程序中的符号(如 global_var
)在链接时的地址。
2. 加载地址(Load Address)
- 定义:加载地址指的是程序在 加载到内存时 被分配的实际地址。操作系统的加载器根据 内存管理机制(如虚拟内存、地址空间布局随机化)决定程序实际被加载到内存的哪个位置。
- 作用:加载器将编译后的程序文件(包括可执行文件或共享库)从磁盘加载到内存中。加载地址与程序的运行环境相关,因此它是程序在实际执行时的内存起始位置。
- 特点:
- 加载地址由操作系统的加载器决定,通常是 动态的,具体的加载地址可能会因每次运行而不同。
- ASLR(Address Space Layout Randomization):为了提高安全性,操作系统可能会随机化加载地址,使得每次加载的程序地址位置不同,从而防止攻击者利用已知地址执行恶意代码。
当程序被加载到内存时,操作系统可能将 .text
段的起始地址放置在 0x400000 地址处。
3. 运行地址(Run Address)
- 定义:运行地址是指程序 实际运行时 的内存地址,通常也就是加载地址。在程序运行时,操作系统会将程序加载到内存的一个位置,并根据虚拟内存的管理机制对程序进行地址映射。
- 作用:运行地址是程序在执行过程中实际使用的内存地址。这是程序通过系统调用访问的地址,它们可能会经过内存映射(Memory Mapping)等机制转换为物理地址。
- 特点:
- 运行地址通常是 虚拟地址,每个进程都会有自己独立的虚拟地址空间。
- 通过内存管理单元(MMU),虚拟地址会被映射到物理内存中。
- 动态链接库:当程序使用共享库时,运行地址也可能因加载库时的地址变化而有所不同。
如果程序在加载时 .text
段的实际加载地址为 0x400000
,在程序运行时,0x400000
就是 .text
段的运行地址。
4. 地址之间的关系:
-
链接地址与加载地址:
- 在编译和链接阶段,程序的所有段(如
.text
、.data
、.bss
等)会根据链接器的设定分配链接地址。这些地址是相对的,链接器不会考虑程序被加载到物理内存中的实际情况。 - 当程序被加载到内存时,加载器会根据系统的内存布局(如地址空间布局随机化)为程序选择一个 加载地址。因此,链接地址通常与加载地址不同。
- 在编译和链接阶段,程序的所有段(如
-
加载地址与运行地址:
- 加载地址是程序被加载到内存时的实际地址,通常也就是程序的 运行地址。不过在某些复杂的情况下,程序可能会使用一些特定的技术(如内存映射、动态库加载等),使得加载地址与运行地址有所区别。
-
虚拟地址与物理地址:
- 程序使用的地址通常是虚拟地址(加载地址和运行地址),操作系统通过虚拟内存管理(MMU)将虚拟地址映射到物理内存地址。这样,程序不直接操作物理内存,而是通过操作系统提供的虚拟内存空间。
注意,运行地址并不一定完全和链接地址相同,也不一定完全和加载地址相同。
如果没有打开MMU,并且使用的是位置相关设计,那么加载地址、运行地址、链接地址三者需要一致。
需要保证链接地址和加载地址是一致的,否则会导致程序跑飞,从uboot上可以理解。
当打开MMU之前,如果使用的是位置无关设计,那么运行地址和加载地址应该是一致的。
例如kernel在打开mmu之前,使用的是位置无关设计,其运行地址和加载地址一致。
如果打开了MMU,那么运行地址和链接地址相同。
硬件会根据运行地址进行计算并自动寻址到对应的加载地址上。
1.3、举例说明
1. 链接地址(Link Address)
定义:链接地址是在程序 编译和链接 阶段,链接器为各个代码段(如 .text
、.data
、.bss
等)分配的 预定虚拟地址。这些地址通常是相对于程序源代码的符号来分配的,目的是为了在 静态链接 时进行符号解析和地址计算。
特点:
- 链接地址在编译时确定,通常表示程序各个部分(如代码段、数据段等)的 相对位置,但不一定等于程序加载到内存中的实际地址。
- 链接地址是 虚拟地址,用于生成最终的可执行文件,和实际的加载地址或运行地址不一定一致。
RK3568 示例: 在开发嵌入式系统时,比如在 RK3568(基于 ARM 的处理器)平台上,程序在链接时可能会指定一些特定的地址。例如:
.text
段(代码段)的链接地址可能是0x10000
。.data
段(数据段)的链接地址可能是0x20000
。
这些链接地址是根据链接器的规则生成的,是相对于源代码的地址,而不是实际的加载地址。
2. 加载地址(Load Address)
定义:加载地址是程序在 加载到内存时,操作系统的加载器为程序分配的实际内存地址。加载器会将程序从磁盘上的可执行文件加载到内存的特定位置。由于内存布局的不同,这个加载地址与链接地址通常不同。
特点:
- 加载地址是由操作系统的加载器(如
ld-linux
或loader
)决定的,通常是根据操作系统的内存管理策略以及 地址空间布局随机化(ASLR) 来确定的。 - 加载地址也可以由固件或引导加载程序(如 U-Boot)在嵌入式系统中决定。
RK3568 示例: 在 RK3568 这种嵌入式平台上,程序加载通常通过 U-Boot 或引导加载程序完成。假设我们有一个程序 app.bin
,它会被加载到内存的特定位置。例如:
- 程序的
.text
段在加载时可能被放置到内存地址0x40000000
(该地址与链接地址不同)。 .data
段的加载地址可能是0x40010000
。
这种加载地址是实际的内存位置,它取决于系统的内存配置。
3. 运行地址(Run Address)
定义:运行地址是程序在实际运行时使用的地址。它通常是 虚拟地址,通过内存管理单元(MMU)映射到物理内存中的某个物理地址。在现代操作系统中,运行地址和加载地址通常是相同的,但在某些情况下它们可以不同。
特点:
- 运行地址是程序实际 执行 时的地址,通常是 虚拟地址,由操作系统的内存管理来映射。
- 在嵌入式系统中,如果程序采用固定地址加载,运行地址通常和加载地址相同。
RK3568 示例: 在 RK3568 上,假设操作系统启用了虚拟内存管理(如 Linux 的 MMU),程序加载到 0x40000000
地址后,它的运行地址也是 0x40000000
,因为程序是直接加载到内存中并执行的。在这种情况下,加载地址和运行地址是相同的。
4. 示例流程:RK3568平台
假设在 RK3568 上开发一个嵌入式 Linux 应用,编译和加载过程可能如下:
-
链接阶段:
- 链接器根据程序源代码和链接脚本为
.text
、.data
、.bss
等段分配链接地址。例如:.text
段链接地址:0x10000
.data
段链接地址:0x20000
- 链接器根据程序源代码和链接脚本为
-
加载阶段:
- 程序在启动时由引导加载程序(如 U-Boot)加载到内存。假设加载地址是:
.text
段加载地址:0x40000000
.data
段加载地址:0x40010000
- 程序在启动时由引导加载程序(如 U-Boot)加载到内存。假设加载地址是:
-
运行阶段:
- 程序在内存中的实际运行时,操作系统通过虚拟内存管理将加载地址映射到虚拟地址空间中,程序的运行地址通常是:
.text
段的运行地址:0x40000000
.data
段的运行地址:0x40010000
运行地址与加载地址相同,但如果启用了虚拟内存或地址空间布局随机化(ASLR),程序可能会被映射到不同的虚拟地址空间,而 MMU 会负责将这些虚拟地址映射到物理内存。
- 程序在内存中的实际运行时,操作系统通过虚拟内存管理将加载地址映射到虚拟地址空间中,程序的运行地址通常是:
二、链接脚本语言
vmlinux.lds.S的例
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的分析:
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查看各个段的布局
$ 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)
{
int ret;
/*
* Wait until kthreadd is all set-up.
*/
wait_for_completion(&kthreadd_done);
kernel_init_freeable();
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
system_state = SYSTEM_FREEING_INITMEM;
kprobe_free_init_mem();
ftrace_free_init_mem();
kgdb_free_init_mem();
exit_boot_config();
free_initmem();
mark_readonly();
...
}
void __weak free_initmem(void)
{
free_initmem_default(POISON_FREE_INITMEM);
}
/*
* Default method to free all the __init memory into the buddy system.
* The freed pages will be poisoned with pattern "poison" if it's within
* range [0, UCHAR_MAX].
* Return pages freed into the buddy system.
*/
static inline unsigned long free_initmem_default(int poison)
{
extern char __init_begin[], __init_end[];
return free_reserved_area(&__init_begin, &__init_end,
poison, "unused kernel image (initmem)");
}
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 lowmem, memory above
* the virtual address range for userspace.
* KERNEL_OFFSET: the virtual address of the start of the kernel image.
* we may further offset this with TEXT_OFFSET in practice.
*/
#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)
#define KERNEL_OFFSET (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
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#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) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
__initcall_start = .; \
KEEP(*(.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) \
__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_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
__initcall#_start存放了每个initcall段的起始地址。
通过上述结构体,就将__initcall1.init段中的数据结构放在__initcall1_start结构体里面了。
并且将所有initcall段里的数据结构initcall_t统一放到了initcall_levels里。
* 调度流程如下:
init/main.c
static void __init do_initcall_level(int level, char *command_line)
{
initcall_entry_t *fn;
parse_args(initcall_level_names[level],
command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, ignore_unknown_bootoption);
trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
int level;
size_t len = strlen(saved_command_line) + 1;
char *command_line;
command_line = kzalloc(len, GFP_KERNEL);
if (!command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len);
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
/* Parser modifies command_line, restore it each time */
strcpy(command_line, saved_command_line);
do_initcall_level(level, command_line);
}
kfree(command_line);
}
从initcall_levels获取各个initcall数据段的起始地址__initcall_start,然后调用do_one_initcall进行执行。
通过如上,就完成了initcall的段中的函数调用。
七、以__earlycon_table为例
从 链接过程 的角度来分析 __earlycon_table
是如何被设置和存储的,重点是链接脚本、数据段的管理,以及如何在内核中找到这个符号。
1. __earlycon_table
的定义和用途
__earlycon_table
通常是由多个 earlycon_id
实例组成的,它们被添加到一个表中。这些 earlycon_id
实际上是描述早期控制台设备的结构体。早期控制台的配置项(例如串口的地址、波特率等)会存储在这些结构体中。
例如,在 Linux 内核的 drivers/tty/serial/earlycon.c
文件中,定义了一个 earlycon_table
数组,它存储了所有早期控制台设备的信息。
static const struct earlycon_id __earlycon_table[] = {
{ .id = "uart8250", .base = 0x10000000, .flags = EARLYCON_FLAG_UARTEarly },
{ .id = "uart16550", .base = 0x10001000, .flags = EARLYCON_FLAG_UARTEarly },
{ .id = "pl011", .base = 0x20000000, .flags = EARLYCON_FLAG_UARTEarly },
// 其他早期控制台设备
};
2. __earlycon_table
的链接过程
链接脚本的作用
__earlycon_table
通常是通过一个 链接脚本 来定义的。链接脚本的作用是将这个符号放置在特定的段中,通常是 .earlycon
段或类似的段,以便在内核启动时能够快速访问。
Linux 内核的链接脚本(arch/arm/kernel/vmlinux.lds.S
或类似的文件)会指定如何处理 __earlycon_table
。例如,在 ARM 架构下,链接脚本可能会将 __earlycon_table
放入 .data
或 .earlycon
段中。
__earlycon_table = .;
.earlycon : {
*(.earlycon)
__earlycon_table_end = .;
} >RAM
这段链接脚本的作用是:
- 将
__earlycon_table
放到内存中的某个特定位置(比如.earlycon
段)。 - 确保
__earlycon_table_end
也被定义,用于表示__earlycon_table
的结束位置。
earlycon_id
的存储方式
earlycon_id
结构体的数组 __earlycon_table
会在链接过程中被填充到指定的内存区域中。每个 earlycon_id
实例的内容(例如串口的基地址、控制台类型等)会被直接拷贝到内存中的该段。
具体来说,__earlycon_table
会被赋予一个 链接地址,这个地址在编译时确定,但它还会受到链接脚本和最终内存布局的影响。假设 __earlycon_table
被放置到内存地址 0x10000000
,那么在内核初始化过程中,内核会通过访问这个内存地址来获得早期控制台的信息。
__earlycon_table
的地址映射
在程序启动时,链接器为 __earlycon_table
分配了一个 链接地址,然后通过加载过程将其加载到内存中的某个实际位置(即 加载地址)。假设程序的内核被加载到地址 0x10008000
,那么 __earlycon_table
的实际位置会根据内核的加载位置来确定。
在启动时,早期控制台驱动会通过 __earlycon_table
来初始化适当的控制台设备。这个过程可能发生在内核初始化的 early_init
阶段。
3. __earlycon_table
在内核启动中的作用
一旦 __earlycon_table
被加载到内存并且内核开始执行,内核会遍历这个表来初始化早期控制台设备。这些设备可能是串口、USB 控制台或其他硬件设备。
控制台初始化
在内核启动时,控制台初始化通常会通过 earlycon
驱动来实现,具体流程如下:
- 访问
__earlycon_table
:内核会通过__earlycon_table
查找是否有可用的早期控制台设备。 - 初始化早期控制台设备:根据
earlycon_id
中的配置信息,内核会初始化指定的控制台设备。例如,内核可能会使用uart8250
驱动程序来初始化位于0x10000000
地址的串口设备。 - 输出日志信息:一旦早期控制台被初始化,内核就会通过该控制台输出调试信息,帮助开发者进行故障排除。