Linux源码分析(二):系统启动
x86的入口在arch/x86/boot/header.S,接下来我们将从BIOS开始介绍,直到Linux开始初始化。
BIOS
电源接通时,CPU的寄存器CS selector = 0xf000,CS base = 0xffff0000(此时CS = 0xfffff000),EIP = 0xfff0,即此时代码执行的地址是0xfffffff0,也就是4GB内存的倒数第16个比特位置。这个地址叫做reset vector,存的是一个jmp指令,跳转到BOIS执行入口。注意虽然CPU此时为实模式,但这里的CS地址也确实超过了1MB,显然超过了实模式的寻址限制。
原因是虽然0xfffffff0不是实模式的有效地址,但在电源刚启动时,CS:EIP的值是可以被设定指向0xfffffff0,所以CPU的第一条寻址地址就超过了1MB的限制,而bus传递地址时并不在乎CPU的模式,地址自然也就可以被传递给指定的内存。在CPU执行了reset vector内容后,即一次jmp,就会自动地把CS的高位置为0,这样以后执行的地址就在实模式的寻址范围内了。附上物理内存图:
|
接下来,BIOS开始初始化和自检,找到可启动硬盘(通过判断硬盘第一个扇区的最后两个字节),把可启动硬盘的第一个扇区内容加载到内存0x7c00处,即bootloader,BIOS将控制权交给bootloader。
Bootloader
Linux的bootloader有多种,我们以GRUB2为例。
BIOS将控制权交给bootloader后,bootloader先执行boot.img部分,boot.img只负责跳转到GRUB2的diskboot.img部分,这部分加载GRUB2的其它未加载部分和用于文件处理的驱动到内存中。加载完后,GRUB2开始执行grub_main函数。grub_main函数完成初始化console、加载grub配置文件等任务后,调用grub_normal_execute函数,用于展示选择操作系统的界面(电脑可能是多系统)。当我们选择一个操作系统后,grub_menu_execute_entry加载操作系统部分内核到内存中。
Kernel
终于到了kernel的步骤,目前在内存中的是Linux的setup部分,这一部分的主要工作是把Linux真正内核加载并解压到内存中。
Setup
这部分的程序入口是arch/x86/header.S里的_start标记处,_start处的第一条指令就是跳转到start_of_setup处。
.globl _start _start: # Explicitly enter this as bytes, or the assembler # tries to generate a 3-byte jump here, which causes # everything else to push off to the wrong offset. .byte 0xeb # short (2-byte) jump .byte start_of_setup-1f 1:
start_of_setup处的主要内容有:
- 保证所有段寄存器值相等
- 建立栈
- 建立bss段
- 跳转到arch/x86/boot/main.c
段寄存器
这一步保证段寄存器全部相等(不明白为啥)。ss、ds等寄存器都是存0x1000,但是由于之前_start跳转到start_of_setup处,cs内容变成了0x1020,所cs也要设为0x1000,这一过程是在建立栈后完成的,先压栈,再出栈,使得EIP=符合6所在的地址,cs=ds。
# We will have entered with %cs = %ds+0x20, normalize %cs so # it is on par with the other segments. pushw %ds pushw $6f lretw
建立栈
把ss:sp置为栈底。
建立bss
将bss段的所有内容置零。
# Zero the bss movw $__bss_start, %di movw $_end+3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl
启开32位
main函数在arch/x86/boot/main.c下,主要完成的功能为
- 初始化console、heap
- 检测内存、CPU验证、键盘初始化
- 进入保护模式
copy_boot_params
copy_boot_params是main函数调用的第一个函数,它完成了两个工作:
- 把header.S中的hdr拷贝到boot_params中。
- 如果kernel用的是旧式命令行协议,就更新为kernel的命令行。
console_init
初始化console。
init_heap
初始化堆。还是在实模式中建立,堆的上限不能超过栈的下限。
heap_end = (char *) ((size_t)boot_params.hdr.heap_end_ptr + 0x200); if (heap_end > stack_end) heap_end = stack_end;
validate_cpu
此函数确保CPU能给我们提供需要的功能。
detect_memory
detect_memory从BIOS处收集内存信息,包括内存段起始、内存段大小、内存段类型等。
keyboard_init
初始化键盘。
query_*
获取BIOS中的其它信息包括机器型号、BIOS版本、高级电池管理等。
set_video
设置显示模式。
go_to_protected_mode
这是初始化的最后一步,之后kernel就进入保护模式。 这里有两个问题我以前没深入了解过:
- 在启动保护模式前,kernel没有page table可以使用。
- kernel的主要部分在保护模式启动前就被加载到了1M地址处。
第一个问题源于cr0的机制,即开启保护模式(PE)和开启分页(PG)是可以分开来的,即PE被置位后,CPU即可访问32位空间地址,此时若PG没有被置位,那么就用不到页表。
第二个问题的答案来自GRUB,GRUB把kernel加载到1M地址处(GRUB在real mode和protected mode下都有运行),详细的步骤未找到描述。
realmode_switch_hook
这个函数暂且不知道有什么用,但会关闭NMI中断。
enable_a20 & reset_coprocessor & mask_all_interrputs
开启保护模式,屏蔽所有中断,注意此时还没进入保护模式。
setup_idt
建立中断解释子表,此时表为空。
/* * Set up the IDT */ static void setup_idt(void) { static const struct gdt_ptr null_idt = {0, 0}; asm volatile("lidtl %0" : : "m" (null_idt)); }
setup_gdt
建立boot阶段的gdt表,包含代码段、数据段和TSS,其中TSS暂时不会被使用,在这里填充是为了迎合Intel要求。
protected_mode_jump
跳入保护模式,此时正式进入保护模式。
切换到64位
这一步CPU会先检查是否支持64位和SSE,然后初始化boot page table,最后切换到64位。内核在上一步跳转到0x10000处。64位的引导代码在arch/x86/boot/compressed/head_64.S下。
内核启动
这一步完成kernel解压和kernel重定位,这一步由函数startup_64开始执行。
内核解压
内核被压缩过,要执行的话必须先解压,同时还会随机选择一块地址把解压后的kernel复制过去,随机选择的物理地址和虚拟地址都是随机产生的。
参考资料
Linux内核源码
https://gohalo.me/post/kernel-bootstrap.html
https://developer.ibm.com/technologies/linux/articles/l-linuxboot/
https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-1.html
https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-2.html
https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-3.html
https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-4.html
https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-5.html
https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-6.html

浙公网安备 33010602011771号