Linux启动分析(2)— bootsect.S、setup.S、head.S分析
阅读原文时间:2021年04月20日阅读:1

bootsect.S ,系统引导程序,一般不超过 512 字节。

在 PC 系统结构中,线性地址 0xA0000 以上,即 640K 以上用于图形接口卡和 BIOS 自身, 640K 以下为系统的基本内存。如果配置更多的内存,则 0x100000 ,即 1MB 处开始称为高内存。当 BIOS 引导一个系统时,总是把引导扇区读入到基本内存地址为 0x7c00 的地方,然后跳转到此执行引导扇区的代码。这段代码将自身搬运到 0x90000 处,并跳转到那继续执行,然后通过 BIOS 提供的读磁盘调用“ int 0x13”从磁盘上读入setup 和内核映像。其中 setup 的映像读入到 0x90200 处,然后跳转到 setup 的代码中。

从 0x90000 到 0xA0000 一共 64K , bootsect 仅占 512 字节,所以 setup 大小理论上可到 63.5KB 。

在 Linux2.4 版本以前,在最前面的 512 字节里保护了一个 mini “boot loader” ,只要拷贝启动代码运行就可从软盘启动;但在 2.6 版本中不再保护这样的 ”boot loader” ,所以必须在第一个磁盘分区上存储一个合适的 boot loader 才能从软盘启动,软盘、硬盘和光驱启动都是一样的过程。

setup 进行映像的解压缩,从 BIOS 收集一些数据,在控制台显示一些信息。

基本内存中开头一部分空间是保留给 BIOS 自己用的,另一方面对于 Linux 内核的引导也需要保留一些运行空间,一共保存了 64K 。基本内存中用于内核映像的就是 8*64K=512K ,其中顶端留 4K 用于引导命令行及从 BIOS 获取需要传递给内核的数据。内核映像一般都经过压缩,压缩后的映像和引导扇区及辅助引导程序的映像拼接在一起,成为内核的引导映像。大小不超过 508K 的映像称为小映像 zImage ,早期版本放在 0x10000 位置处,否则称为大内核 bzImage ,放在 0x100000 位置处。

CPU 在 bootsect 时处于 16 位实地址模式,然后在 setup 的执行过程中转入 32 位保护模式。

Setup 从 BIOS 中读取系统数据(内存大小、显卡模式、磁盘等参数),将数据保存在 0x90000-0x901FF ,覆盖了 bootsect 的内容。设置 32 位运行方式:加载中断描述表寄存器 IDTR 、全局描述表寄存器 GDTR ;临时设置 IDT 表和 GDT 表,并在 GDT 表中设置内核代码段和数据段的描述符,在 Head.S 中会根据内核的需要重新设置这些描述符表;开启 A20 地址线;重新设置两个中断控制器 8259A ,将硬件中断号重新设置为 0x20 和 0x2f ;最后设置 CPU 的控制寄存器 CR0 (机器状态字)的保护模式比特( PE )位,从而进入 32 位保护模式运行;然后跳转到 head.S 中的 startup_32 执行。

对于小内核映像放在 0x10000 处, Setup 会把 system 从 0x10000 移到 0x0000 开始处。对于大内核映像, vmlinux 中普通内核代码被编译成以 PAGE_OFFSET+1MB 为起始地址,在 Head.S 中初始化代码把虚拟地址减去 PAGE_OFFSET 就能得到以 1MB 为起始位置的物理地址,这也正是内核映像在物理内存中的存放位置。

Head.S 中的 startup_32 主要用于开启页面单元。初始化工作在编译过程中开始进行,它先定义一个称为 swapper_pg_dir 的数组,使用链接器指示在地址 0x00101000 。然后分别为两个页面 pg0 和 pg1 创建页表项。第一组指向 pg0 和 pg1 的指针放在能覆盖 1 ~ 9MB 内存的位置,第二组指针放在 PAGE_OFFSET+1MB 的位置。一旦开始页机制,在上述页表和页表项指针建立后可以保证,在内核映像中不论是采用物理地址还是虚拟地址,都可以进行正确的页面映射。内核其他部分的页表初始化在 paging_init() 中完成。映射建立后,通过设置 cr0 寄存器中的某位开启页面映射,然后通过一个跳转指令保证指令指针的正确性。

1.Bootsect启动过程:

假设用 LILO 启动,启动时用户可以选择启动哪个操作系统。 LILO 将 boot loader 分为两部分,一部分放到启动分区的第一个扇区;

1) BIOS 将 MBR 或启动分区的第一个扇区的启动部分加载到地址 0x00007c00 处;

2) 该程序将自身移到 0x00096a00 ,建立实模式栈 ( 从 0x00098000 到 0x000969ff) ,将 LILO 的第二部分加载到 0x00096c00 处,然后跳转到此执行;

3) 然后第二部分程序从磁盘读取一个可启动的操作系统列表让用户选择,最后用户选择每个 OS 后, boot loader 可以拷贝不启动分区或者之间拷贝内核映像到 RAM 中去;

4) 加载 Linux 内核映像时, LILO boot loader 首先调用 BIOS 例程显示 ”Loading …” 信息;

5) 调用 BIOS 例程加载内核映像的初始化部分到 RAM 上,内核映像的前 512 字节放在 0x00090000 位置, setup() 函数代码放在 0x00090200 位置;

6) 接着调用 BIOS 例程装载内核映像的其余部分,映像可能放在低地址 0x00010000( 使用 make zImage 编译的小内核映像 ) 或者高地址 0x00100000 (使用 make bzImage 编译的大内核映像)。

7) 然后跳至刚刚 setup 部分。

2.Setup.S分析

setup() 汇编函数被连接器放在内核映像文件中的 0x200 偏移处。 Setup 函数必须初始化计算机中的硬件设备并为内核程序的执行建立环境。

1) 在 ACPI 兼容的系统中,调用 BIOS 例程建立描述系统物理内存布局的表。在早期系统中,它调用 BIOS 例程返回系统可以的 RAM 大小;

2) 设置键盘的重复延迟和速率;

3) 初始化显卡;

4) 检测 IBM MCA 总线、 PS/2 鼠标设备、 APM BIOS 支持等;

5) 如果 BIOS 支持 Enhanced Disk Drive Services (EDD) ,将调用正确的 BIOS 例程建立描述系统可用硬盘的表;

6) 如果内核加载在低 RAM 地址 0x00010000 ,则把它移动到 0x00001000 处;如果映像加载在高内存 1M 位置,则不动;

7) 启动位于 8042 键盘控制器的 A20 pin 。

8) 建立一个中断描述表 IDT 和全局描述表 GDT 表;

9) 如果有的话,重启 FPU 单元;

10) 对可编程中断控制器进行重新编程,屏蔽所以中断,级连 PIC 的 IRQ2 不需要;

11) 设置 CR0 状态寄存器的 PE 位使 CPU 从实模式切换到保护模式, PG 位清 0 ,禁止分页功能;

12) 跳转到 startup_32() 汇编函数 , jmpi 0x100000, __BOOT_CS ,终于进入内核 Head.S ;

3.Head.S分析

有两个不同的 startup_32() 函数,一个在 arch/i386/boot/compressed/head.S 文件中, setup 结束后,该函数被放在 0x00001000 或者 0x00100000 位置,该函数主要操作:

1) 首先初始化段寄存器和临时堆栈;

2) 清除 eflags 寄存器的所有位;

3) 将 _edata 和 _end 区间的所有内核未初始化区填充 0 ;

4) 调用 decompress_kernel( ) 函数解压内核映像。首先显示 "Uncompressing Linux…" 信息,解压完成后显示 "OK, booting the kernel." 。内核解压后,如果时低地址载入,则放在 0x00100000 位置;否则解压后的映像先放在压缩映像后的临时缓存里,最后解压后的映像被放置到物理位置 0x00100000 处;

5) 跳转到 0x00100000 物理内存处执行;

解压后的映像开始于 arch/i386/kernel/head.S 文件中的 startup_32() 函数,因为通过物理地址的跳转执行该函数的,所以相同的函数名并没有什么问题。该函数未 Linux 第一个进程建立执行环境,操作如下:

1) 初始化 ds,es,fs,gs 段寄存器的最终值;

2) 用 0 填充内核 bss 段;

3) 初始化 swapper_pg_dir 数组和 pg0 包含的临时内核页表:

l 将 swapper_pg_dir ( 0x1000) 和 pg0(0x2000) 清空, swapper_pg_dir 作为整个系统的页目录;

l 将 pg0 作为第一个页表,将其地址赋到 swapper_pg_dir 的第一个 32 位字中。

l 同时将该页表项也赋给 swapper_pg_dir 的第 3072 个入口,表示虚拟地址 0xc0000000 也指向 pg0 。

l 将 pg0 这个页表填满指向内存前 4M 。

l 在 cr3 寄存器中存放 PGD 的地址,并设置 cr0 寄存器中的 PG 位,启用分页支持。

4) 建立进程 0idle 进程的内核模式的堆栈;

5) 再次清除 eflags 寄存器的所有位;

6) 调用 setup_idt() 用非空的中断处理函数填充 IDT 表;

7) 将从 BIOS 获取的系统参数传递到操作系统的第一个页面帧;

8) 识别处理器的模式;

9) 将 GDT 和 IDT 表的地址加载到 gdtr 和 idtr 寄存器中;

10)  跳转到 start_kernel 函数,这个函数是第一个 C 编制的函数,内核又有了一个新的开始。

4.start_kernel()分析:

1) 调度器初始化,调用 sched_init();

2) 调用 build_all_zonelists 函数初始化内存区;

3) 调用 page_alloc_init() 和 mem_init() 初始化伙伴系统分配器;

4) 调用 trap_init() 和 init_IRQ() 对中断控制表 IDT 进行最后的初始化;

5) 调用 softirq_init() 初始化 TASKLET_SOFTIRQ 和 HI_SOFTIRQ ;

6) Time_init() 对系统日期和时间进行初始化;

7) 调用 kmem_cache_init() 初始化 slab 分配器;

8) 调用 calibrate_delay() 计算 CPU 时钟频率;

通过调用 kernel_thread() 启动进程 1init 进程的内核线程,然后该线程再创建其他的内核线程执行 /sbin/init 程序。