Linux源代码阅读——内核引导
转自:http://home.ustc.edu.cn/~boj/courses/linux_kernel/1_boot.html
目录
- Linux 引导过程综述
- BIOS
- POST
- 自举过程
- Boot loader
- 主引导扇区结构
- GRUB stage1
- GRUB stage2
- 内核初始化:体系结构相关部分
- 内核映像结构
- header.S
- 初始化与保护模式
- 自解压内核
- startup_32
- 内核初始化:体系结构无关部分
- 核心数据结构初始化
- 设备初始化
1 Linux引导过程综述
- BIOS
在 i386 平台中,由 BIOS 作最初的引导工作,执行加电自检、初始化,读取引导设备的主引导扇区并执行。 - Boot loader(以 GRUB 为例)
MBR 中的、紧随 MBR 后的 phase 1/1.5 boot loader 载入文件系统中的 phase 2 及其配置,显示操作系统选择菜单,执行用户命令,载入选定的操作系统内核与 initrd。 - 内核初始化:体系结构相关部分
从 header.S 开始,到 main.c 初始化参数,再到 pm.c 进入保护模式,然后载入 vmlinuz 并自解压,在 startup_32.S 中开启分页机制、初始化中断向量表、检测 CPU 类型等,完成 x86 体系结构的保护模式初始化。这是本文重点。 - 内核初始化:体系结构无关部分
分为核心数据结构初始化(start_kernel)和设备初始化两个阶段。 - 用户态初始化
以下内容超出了本文范围。用户态的 init 程序:- 获取运行信息
- 执行 /etc/rc[runlevel].d 中的启动脚本
- 加载内核模块(/etc/modprobe.conf)
- 执行 /etc/init.d 中的脚本
- 执行 /bin/login,等待用户登录
- 接受 shell 中的用户控制
2 BIOS
BIOS的主要功能概括来说包括如下几部分:
- POST
加电自检,检测 CPU 各寄存器、计时芯片、中断芯片、DMA 控制器等 - Initial
枚举设备,初始化寄存器,分配中断、IO 端口、DMA 资源等 - Setup
进行系统设置,存于 CMOS 中。 - 常驻程序
INT 10h、INT 13h、INT 15h 等,提供给操作系统或应用程序调用。 - 启动自举程序
在POST过程结束后,将调用 INT 19h,启动自举程序,自举程序将读取引导记录,装载操作系统。
BIOS 的启动主要由 POST 过程与自举过程构成。
2.1 POST
当 PC 加电后,CPU 的寄存器被设为某些特定值。其中,指令指针寄存器(program counter)被设为 0xfffffff0。
CR1,一个32位控制寄存器,在刚启动时值被设为0。CR1 的 PE (Protected Enabled,保护模式使能) 位指示处理器是处于保护模式还是实模式。由于启动时该位为0,处理器在实模式中引导。在实模式中,线性地址与物理地址是等同的。
在实模式下,0xfffffff0 不是一个有效的内存地址,计算机硬件将这个地址指向 BIOS 存储块。这个位置包含一条跳转指令,指向 BIOS 的 POST 例程。
POST(Power On Self Test,加电自检)过程包括内存检查、系统总线检查等。如果发现问题,主板会蜂鸣报警。在 POST 过程中,允许用户选择引导设备。
POST 的最后一步是执行 INT 0x19 指令,开始自举过程。
POST 过程在 AWARD BIOS 的源码中在 BOOTROM.ASM 文件中 BootBlock_POST 函数过程中实现,主要步骤如下:
- 初始化各种主板芯片组
- 初始化键盘控制器
- 初始化中断向量、中断服务例程
- 初始化 VGA BIOS 控制器
- 显示 BIOS 的版本和公司名称
- 扫描各种介质容量并显示
- 读取 CMOS 的启动顺序配置
- 调用 INT 0x19 启动自举程序
2.2 自举过程
自举过程即为执行中断 INT 0x19 的中断服务例程 INT19_VECT 的过程 (Bootrom.asm)
主要功能为读取引导设备第一个扇区的前 512 字节(MBR),将其读入到内存 0x0000:7C00,并跳转至此处执行。
3 Boot loader
3.1 主引导扇区结构
硬盘第一个扇区的前 512 个字节是主引导扇区,由 446 字节的 MBR、64 字节的分区表和 2 字节的结束标志组成。
- MBR(Master Boot Record)是 446 字节的引导代码,被 BIOS 加载到 0x00007C00 并执行。
- 硬盘分区表占据主引导扇区的 64 个字节(0x01BE -- 0x01FD),可以对四个分区的信息进行描述,其中每个分区的信息占据 16 个字节。
一个分区记录有如下域:
- 1字节 文件系统类型
- 1字节 可引导标志
- 6字节 CHS格式描述符
- 8字节 LBA格式描述符
LBA和CHS两种描述符指示相同的信息,但是指示方式有所不同:LBA (逻辑块寻址,Logical Block Addressing)指示分区的起始扇区和分区长度, 而CHS(柱面 磁头 扇区)指示首扇区和末扇区。
- 结束标志字 55,AA(0x1FEH -- 0x1FFH)是主引导扇区的最后两个字节,是检验主引导记录是否有效的标志。
3.2 GRUB stage1
Linux 的启动方式包括 LILO、GRUB 等。这里结合 GRUB 源代码分析其引导过程。
GRUB 的引导过程分为 stage1、stage 1.5 和 stage 2。其中 stage1 和可能存在的 stage1.5 是为 stage2 做准备,stage2 像一个微型操作系统。
- BIOS 加载 GRUB stage1(如果安装到 MBR)到 0x00007C00.
- stage1 位于 stage1/stage1.S,汇编后形成 512 字节的二进制文件,写入硬盘的0面0道第1扇区。
stage1 将0面0道第2扇区上的 512 字节读到内存中的0x00007000处,然后调用 COPY_BUFFER 将其拷贝到 0x00008000 的位置上,然后跳至 0x00008000 执行。这 512 字节代码来自 stage2/start.S,作用是 stage1_5 或者 stage2(编译时决定加载哪个)的加载器。
/* start.S */ blocklist_default_start: .long 2 /* 从第3扇区开始*/ blocklist_default_len: /* 需要读取多少个扇区 */ #ifdef STAGE1_5 .word 0 /* 如果是 STAGE1_5,则不读入 */ #else .word (STAGE2_SIZE + 511) >> 9 /* 读入 Stage2 所占的所有扇区 */ #endif blocklist_default_seg: #ifdef STAGE1_5 .word 0x220 /* 将 stage1.5 加载到 0x2200 */ #else .word 0x820 /* 将 stage2 加载到 0x8200 */ #endif
- 由于 stage1 和 start 不具备文件系统识别功能,stage 1.5 只能被存放在固定的扇区中。例如 e2fs_stage1_5 就被存放在0面0道第3扇区开始的一段连续空间里。(第一个主分区是从1面0道第1扇区开始的,stage 1.5 不会覆盖主分区内容)
stage 1.5 能够读取文件系统,负责从文件系统中载入并执行 stage 2,即 GRUB 的核心映像。由于系统引导过程中不需要修改文件系统,因此只实现了文件系统的读取。可以说,stage 1.5 是 stage 1 与 stage 2 之间的桥梁,解决了文件系统这个“先有鸡还是先有蛋”的问题。
3.3 GRUB stage2
stage2 将系统切换到保护模式,设置 C 运行环境,寻找 config 文件,执行 shell 接受用户命令,载入选定的操作系统内核。
- stage2 的入口点是 asm.s
#ifdef STAGE1_5 # define ABS(x) ((x) - EXT_C(main) + 0x2200) #else # define ABS(x) ((x) - EXT_C(main) + 0x8200) #endif
- 初始化一些变量
- 跳转到 code_start
- 关中断,设置段寄存器和堆栈起始地址
- 从实模式切换到保护模式
- 清空 bss 段
- init_bios_info()
- 随后进入 stage2.c,执行 GRUB 的主要功能。
- cmain(): 主函数,载入配置文件 menu.lst(GRUB 1)或 grub.cfg(GRUB 2),如果成功载入就进入 run_menu(),显示菜单,进入循环倒计时,如果超时就进入第一个,如果用户按了键就停止倒计时。用户作出选择后,跳转到 boot_entry(),清空屏幕、获取入口,通过 find_command 找到的函数指针调用相应的命令。
- 如果没有成功载入配置文件,就 enter_cmdline(),也是通过 find_command 调用相应的命令。
- 每个 GRUB 命令都要在 stage2/builtin.c 的 builtin_table 数组中登记:
struct builtin { char *name; /* 命令名称 */ int (*func) (char *, int); /* 命令执行时调用的函数指针 */ int flags; /* 标志,似乎未用到 */ char *short_doc; /* 短帮助 */ char *long_doc; /* 详细帮助 */ }; struct builtin *builtin_table[];
- 常用 GRUB 命令:
- root:挂载分区并设为根分区。
root_func (char *arg, int flags)
- kernel:对传进来的参数逐个解析,获得 linux 内核映像路径,通过 load_image() 载入内核。
kernel_func (char *arg, int flags)
- boot:根据操作系统类型调用不同的启动函数,将控制权转交给操作系统。支持 BSD、linux、chain loader、multi boot 等方式。
boot_func (char *arg, int flags)
- root:挂载分区并设为根分区。
- stage2 中的文件系统驱动:
每种文件系统都要按照 stage2/filesys.h 的定义在 stage2/disk_io.c 的 fsys_table 数组中登记:/* stage2/filesys.h */ struct fsys_entry { char *name; //文件系统名称 int (*mount_func) (void); //挂载 int (*read_func) (char *buf, int len); //读文件 int (*dir_func) (char *dirname); //打开文件 void (*close_func) (void); //关闭文件 int (*embed_func) (int *start_sector, int needed_sectors); //不清楚 };
GRUB 调用 grub_open() 打开文件。grub_open 在 fsys_table 数组中逐个调用 fsys_entry::mount_func(),找到当前已挂载的文件系统,再用 fsys_entry::dir_func() 方法打开文件。
4 内核初始化:体系结构相关部分
4.1 内核映像结构
根据 Linux/I386 启动协议(Documentation/i386/boot.txt),x86 体系结构大内核内存使用如下:
For a modern bzImage kernel with boot protocol version >= 2.02, a memory layout like the following is suggested: ~ ~ | Protected-mode kernel | 100000 +------------------------+ | I/O memory hole | 0A0000 +------------------------+ | Reserved for BIOS | Leave as much as possible unused ~ ~ | Command line | (Can also be below the X+10000 mark) X+10000 +------------------------+ | Stack/heap | For use by the kernel real-mode code. X+08000 +------------------------+ | Kernel setup | The kernel real-mode code. | Kernel boot sector | The kernel legacy boot sector. X +------------------------+ | Boot loader | <- Boot sector entry point 0000:7C00 001000 +------------------------+ | Reserved for MBR/BIOS | 000800 +------------------------+ | Typically used by MBR | 000600 +------------------------+ | BIOS use only | 000000 +------------------------+
根据 arch/x86/boot/Makefile,bzImage 大内核映像由 setup.elf 和 vmlinux 组成,而 vmlinux 又由 setup.bin 和 vmlinux.bin 组成。vmlinux.bin 会进行压缩存储,变成 vmlinux.bin.gz。因此 bzImage 由 setup.elf、setup.bin、vmlinux.bin.gz 三部分组成。
Line 28: targets := vmlinux.bin setup.bin setup.elf zImage bzImage Line 29: subdir- := compressed Line 30: Line 31: setup-y += a20.o cmdline.o copy.o cpu.o cpucheck.o edd.o Line 32: setup-y += header.o main.o mca.o memory.o pm.o pmjump.o Line 33: setup-y += printf.o string.o tty.o video.o video-mode.o version.o
其中 setup-y 就是 setup.elf,其中引用的 header.o 是从 header.S 汇编而来的。
Line 77: $(obj)/bzImage: IMAGE_OFFSET := 0x100000
Line 86: $(obj)/zImage $(obj)/bzImage: $(obj)/setup.bin \ Line 87: $(obj)/vmlinux.bin $(obj)/tools/build FORCE Line 88: $(call if_changed,image) Line 89: @echo 'Kernel: $@ is ready' ' (#'`cat .version`')' Line 90: Line 91: OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S
大内核情况下的内存分布图:
| vmlinux | 100000 +------------------------+ | setup.elf的setup部分 | 090200 +------------------------+ | setup.elf的启动扇区 | 090000 +------------------------+ | BootLoader | 007c00 +------------------------+ | | 000000 +------------------------+
在进入源代码的世界之前,我们先看看用于控制 arch/x86/boot 下代码进行链接的 setup.ld。
ld 文件用于控制 ld 的链接过程:
- 描述输入文件的各节如何对应到输出文件的各节
- 控制输入文件各节及符号的内存布局
每个对象文件有一个节(section)列表、一个符号列表,一个符号可以是已定义或未定义的。每个已定义的符号有地址。未定义的符号则要在链接时从其他文件中寻找其定义。
- 指定输出文件格式
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
- 指定目标体系结构
OUTPUT_ARCH(i386)
- 设置入口点
ENTRY(_start)
- 输入文件各节到输出文件的映射
SECTIONS { . = 0 // 从 0 开始 .bstext : { *(.bstext) } // 所有输入文件的 .bstext 节组合成输出文件的 .bstext 节 .bsdata : { *(.badata) } // 所有输入文件的 .bsdata 节... . = 497 // 填充 512 字节的 bootloader(见4.2节 header.S) .header : { *(.header) }
在每一部分(header、rodata、data、bss、end)之间,对齐 16 字节内存边界:
. = ALIGN(16);
最后用断言保证链接后的目标文件不太大,且偏移量正确。
4.2 header.S
start2: movw %cs, %ax # CS = 0x7c00 movw %ax, %ds # 初始化段寄存器 movw %ax, %es movw %ax, %ss xorw %sp, %sp sti # 开中断 cld # di++, si++ ................................ msg_loop: # 打印字符例程 ................................ bs_die: # 错误处理例程 .ascii "Direct booting from floppy is no longer supported.\r\n" .ascii "Please use a boot loader program instead.\r\n" .ascii "\n" .ascii "Remove disk and press any key to reboot . . .\r\n" .byte 0
这段代码编译链接后,会生成 512 字节的 bootsector,其中 .section ".header", "a" 中的变量共 15 字节。注意到 setup.ld (Linker script for the i386 setup code) 中加入了 497 字节的空白,事实上恰好凑够 512 字节。
事实上,上一节我们提到,MBR 是由 GRUB 写入的,因此这里的 bootsector 对于硬盘启动是用不到的。GRUB 等 boot loader 将 setup.elf 读到 0x90000 处,将 vmlinux 读到 0x100000 处,然后跳转到 0x90200 开始执行,恰好跳过了 512 字节的 bootsector。
有意思的是,从软盘启动时,header.S 生成的 bootsector 做的惟一一件事就是打印错误信息(bs_die),不支持从软盘启动。
下面就是 0x90200(_start)了,目的就是跳到 start_of_setup。
# Part 2 of the header, from the old setup.S ................................ # End of setup header #####################################################
上面这两行之间的代码是一个庞大的数据结构,与 include/asm/bootparam.h 中的 struct setup_header 一一对应。这个数据结构定义了启动时所需的默认参数,其中一些参数可以通过命令选项 overwrite。下表列出了一些参数的意义。
名称 | 偏移 | 大小(字节) | 意义 |
---|---|---|---|
root_flags | 0x1f2 | 2 | 根目录是否只读,可用 ro 或 rw 选项指定 |
root_dev | 0x1fc | 2 | 默认的 root 设备,即 /boot 所在目录,可用 root= 选项指定 |
boot_flag | 0x1fe | 2 | 0xAA55,即主引导扇区结束标志 |
header | 0x202 | 4 | HdrS (0x53726448),内核标志 |
version | 0x206 | 2 | 启动协议版本号: major * 64 + minor |
kernel_version | 0x20e | 2 | 内核版本号 |
type_of_loader | 0x210 | 1 | Boot loader ID: Boot loader ID * 64 + Version No.
Boot loader IDs: 0 LILO 1 Loadlin 2 bootsect-loader 3 SYSLINUX 4 EtherBoot 5 ELILO 7 GRuB 8 U-BOOT 9 Xen A Gujin B Qemu |
loadflags | 0x211 | 1 | 启动选项的掩码。
|
code32_start | 0x214 | 4 | 内核解压缩前立即跳转到的 32 位 flat-mode 入口 |
ramdisk_image | 0x218 | 4 | initramfs 的 32 位线性地址 |
cmd_line_ptr | 0x228 | 4 | 内核命令行的 32 位线性地址 |
下面我们迎来了真正的起点(start_of_setup),主要流程为:
- 复位硬盘控制器
- 如果 %ss 无效,重新计算栈指针
- 初始化栈,开中断
- 将 cs 设置为 ds,与 setup.elf 的入口地址一致
- 检查主引导扇区末尾标志,如果不正确则跳到 setup_bad
- 清空 bss 段
- 跳到 main(定义在 boot/main.c)
4.3 初始化与保护模式
我们终于暂时离开了汇编代码,走进 “主要” 的启动部分。这一部分在 arch/x86/boot/main.c 中。
main() 中的几个函数调用都有比较详细的注释,主要作用是初始化 boot_params,将来会经常被用到。
include/asm/bootparam.h 中定义的 boot_params 结构体 (即 zeropage) 在此完成初始化:
- copy_boot_params() 初始化 boot_params.hdr (将 hdr 复制过来)
- detect_memory() 初始化 boot_params.e820_map 和 boot_params.e820_entries
- query_apm_bios() 初始化 apm_bios_info、screen_info
go_to_protected_mode() 进入保护模式,代码在 boot/pm.c。
- realmode_switch_hook():boot_params.hdr 中有 realmode_swtch,记录了 hook 函数地址,如果有的话就执行之
- reset_coprecessor(): 重启协处理器
- make_all_interrupts(): 关闭所有旧 PIC 上的中断。其中的 io_delay 等待 I/O 操作完成。
- setup_idt(): 初始化中断描述符表 (空的)
- setup_gdt(): 初始化 GDT:
- GDT_ENTRY_BOOT_CS
- GDT_ENTRY_BOOT_DS
- GDT_ENTRY_BOOT_TSS
其中 GDT_ENTRY_BOOT_CS 和 GDT_ENTRY_BOOT_DS 基地址都为零,段限长都是 4G。
下面是 GDT 数据结构示意:
- protected_mode_jump(): 汇编代码,下面分析。传参说明:进入保护模式后将采用段访问内存地址,因此要将传入的参数转换为线性地址。
下面进入 boot/pmjump.S 中的 protected_mode_jump。
29 protected_mode_jump: 30 movl %edx, %esi # Pointer to boot_params table 31 32 xorl %ebx, %ebx 33 movw %cs, %bx # 将实模式的代码段放入 bx 34 shll $4, %ebx # 转换为线性地址 35 addl %ebx, 2f # 将 in_pm32 的实模式地址转换为线性地址 36 37 movw $__BOOT_DS, %cx # ds 段选择子 38 movw $__BOOT_TSS, %di # tss 段选择子 39 40 movl %cr0, %edx 41 orb $X86_CR0_PE, %dl # Protected mode 42 movl %edx, %cr0 # 将 cr0 的0位置0是进入保护模式的标志 43 jmp 1f # Short jump to serialize on 386/486 44 1: 45 # 下面这段作用是跳转到 in_pm32,由于已经在保护模式,所以需要考虑段的问题 46 # Transition to 32-bit mode 47 .byte 0x66, 0xea # ljmpl opcode 48 2: .long in_pm32 # offset 49 .word __BOOT_CS # segment 50 51 .size protected_mode_jump, .-protected_mode_jump 52 53 .code32 54 .type in_pm32, @function 55 in_pm32: # 下面的注释挺清楚,就不翻译了 56 # Set up data segments for flat 32-bit mode 57 movl %ecx, %ds 58 movl %ecx, %es 59 movl %ecx, %fs 60 movl %ecx, %gs 61 movl %ecx, %ss 62 # The 32-bit code sets up its own stack, but this way we do have 63 # a valid stack if some debugging hack wants to use it. 64 addl %ebx, %esp 65 66 # Set up TR to make Intel VT happy 67 ltr %di # 这个比较有意思 68 69 # Clear registers to allow for future extensions to the 70 # 32-bit boot protocol 71 xorl %ecx, %ecx 72 xorl %edx, %edx 73 xorl %ebx, %ebx 74 xorl %ebp, %ebp 75 xorl %edi, %edi 76 77 # Set up LDTR to make Intel VT happy 78 lldt %cx # 又是一个骗 CPU 的东西 79 # eax 是 protected_mode_jump 的第一个参数,即 header.S 中定义的 boot_params.hdr.code32_start,即 vmlinux 的入口地址 80 jmpl *%eax # Jump to the 32-bit entrypoint 81 82 .size in_pm32, .-in_pm32
4.4 自解压内核
上节末尾的 jmpl 指令把我们带入了 vmlinux 的世界。注意到,vmlinux 是压缩存储的,因此内核首先的工作就是把真正的内核解压出来。
根据 Makefile,linux 内核文件有以下几种:
- vmlinux: 原始的 linux 内核
- zImage: 经过 gzip 压缩后的 vmlinux,解压到 640KB 内存位置
- bzImage: 大内核版的 zImage,解压到 1MB 内存位置,现在我们一般都用这个
- vmlinuz: 指向 zImage 或 bzImage 的链接
- initrd: init ram disk,用于引导 vmlinuz
循着 Makefile 的踪迹,我们找到了 arch/x86/boot/compressed/head_32.S,这就是大内核模式下 0x100000 开始的内存内容。
- 找到 vmlinux 的入口地址,并将其存入 ebp。
- 如果设置了可重入内核,就将 ebp 按照 kernel_alignment 对齐,放入 ebx。
- 确定解压内核的内存地址
- 设置栈
- 将 vmlinux 复制到安全地区(ebx 指定的地方):保存 esi 到栈中,首先计算出需要复制的字节数目,然后4个字节为一组地复制过去,再从栈中恢复 esi。
- 进入 relocated,清空 BSS,初始化解压函数所用的栈
- 将 decompress_kernel 所用的参数入栈:内核加载地址、内核长度、压缩内核安全地址、堆地址、启动参数结构体指针。
- 调用 decompress_kernel 解压内核
- 如果设置了可重入内核,进行一些 relocate
- 跳转到解压后的内核。
至此,arch/x86/boot 下的流程基本分析完毕。
4.5 startup_32
vmlinux 是从哪里来的呢?不知道是否是 Linus 有意为我们增加难度 (其实是我对 make 不熟悉),生成 vmlinux 的命令在源码根目录的隐藏文件 .vmlinux.cmd 中。
md_vmlinux := ld -m elf_i386 --build-id -o vmlinux -T arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_32.o arch/x86/kern el/head32.o arch/x86/kernel/init_task.o init/built-in.o --start-group usr/built-in.o arch/x86/mach-generic/built-in.o arch/x86/kernel/built-in.o arch/x86/mm/built-in.o arch/x86/mach-default/built-in.o arch/x86/crypto/built-in.o arch/x86 /vdso/built-in.o kernel/built-in.o mm/built-in.o fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.o lib/lib.a arch/x86/lib/lib.a lib/built-in.o arch/x86/lib/built-in.o drivers/built-in.o sound/built -in.o arch/x86/pci/built-in.o arch/x86/oprofile/built-in.o arch/x86/power/built-in.o net/built-in.o --end-group .tmp_k allsyms2.o
真正的内核入口是 arch/x86/kernel/head_32.S (为什么也叫 head_32.S?)
汇编函数 startup_32 依次完成以下动作:
- 初始化参数
- 初始化 GDT。boot_gdt_descr 在数据区中记载了 GDT 表首地址。
lgdt pa(boot_gdt_descr)
- 清空 BSS 段
- 复制实模式中的 boot_params 结构体
- 复制命令行参数到 boot_command_line (供 init/main.c 使用)
- 有关虚拟环境的一些配置
- 初始化 GDT。boot_gdt_descr 在数据区中记载了 GDT 表首地址。
- 开启分页机制
尽管我们已经在保护模式中,但只有段机制而没有启用页机制。这里设置全局页目录与页表项,并开启分页机制。
下图示意了 Linux 的分页机制(From ULK)。
- 如果启用了 PAE,即物理地址扩展到 64G 的机制,不作分析。
- 不然,就是通常的 4G 线性地址空间。__PAGE_OFFSET 是内核编译时配置的内核地址空间偏移,默认为 3G。默认配置下,进程的用户态地址空间为 0~3G,高 1G 是内核地址空间。
全局页目录大小为 4KB,每项大小为 4B,可以表示 4MB 的线性范围,因此页目录的大小是 __PAGE_OFFSET >> 20。
page_pde_offset = (__PAGE_OFFSET >> 20);
- 初始化页表首地址 %edi、全局页目录地址 %edx、PTE 属性(页目录和页表的每项 4 Byte 中后 12 位是属性,这里预先填充 0x67)
230 movl $pa(pg0), %edi 231 movl $pa(swapper_pg_dir), %edx 232 movl $PTE_ATTR, %eax
- 下面是一个双层循环,外层循环填充页目录,内层循环填充页表。
233 10: # %edi: 页表首地址 234 leal PDE_ATTR(%edi),%ecx /* Create PDE entry */ # 将页目录项填充到页目录中,%edx 为页目录地址 235 movl %ecx,(%edx) /* Store identity PDE entry */ 236 movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */ # 填充下一个页目录项 237 addl $4,%edx 238 movl $1024, %ecx 239 11: # 内层循环,填充 4KB 的 PTD 240 stosl # es:edi= eax,edi++ # 表面上看是将 0x1000 加到属性上,事实上是 %eax 的后 12 位属性不变,前面的 20 位页地址加 1。 241 addl $0x1000,%eax # 继续内层循环 242 loop 11b 243 /* 244 * End condition: we must map up to and including INIT_MAP_BEYOND_END 245 * bytes beyond the end of our own page tables; the +0x007 is 246 * the attribute bits 247 */ # 计算何时应停止 248 leal (INIT_MAP_BEYOND_END+PTE_ATTR)(%edi),%ebp # 如果 %eax < %ebp,继续外层循环 249 cmpl %ebp,%eax 250 jb 10b
- 添加页目录项的最后一项,页表地址为 swapper_pg_fixmap,用于 fixmap area
251 movl %edi,pa(init_pg_tables_end) 252 253 /* Do early initialization of the fixmap area */ 254 movl $pa(swapper_pg_fixmap)+PDE_ATTR,%eax 255 movl %eax,pa(swapper_pg_dir+0xffc)
- 有关对称多处理器(SMP)的处理
- 一些 CPU 参数相关的判断和处理
- 开启分页机制
# 将页表首地址(swapper_pg_dir)放入 cr3 331 movl $pa(swapper_pg_dir),%eax 332 movl %eax,%cr3 /* set the page table pointer.. */ # 设置 cr0 的 paging 位,打开 cr0 的分页机制 333 movl %cr0,%eax 334 orl $X86_CR0_PG,%eax 335 movl %eax,%cr0 /* ..and set paging (PG) bit */ # 目前已经开启分页机制,完全进入保护模式。 336 ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */
- 初始化 Eflags
- 初始化中断向量表
在实模式中,已经初始化了 IDT,不过现在我们要对保护模式再做一次这样的工作。由于这段代码比较长,放在了单独的函数里。
485 setup_idt: # 默认中断处理例程,后面有定义,做一件事情:如果开启了 CONFIG_PRINTK,就通过 printk 输出内核信息。 486 lea ignore_int,%edx # 这里是内核代码段,注意已经是保护模式了,所以要用代码段选择子 487 movl $(__KERNEL_CS << 16),%eax 488 movw %dx,%ax /* selector = 0x0010 = cs */ 489 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 490 # 载入 IDT 表的首地址 491 lea idt_table,%edi # 共有 256 个中断向量 492 mov $256,%ecx 493 rp_sidt: # 这是一个循环,用默认中断处理例程初始化 256 个中断向量 494 movl %eax,(%edi) 495 movl %edx,4(%edi) 496 addl $8,%edi 497 dec %ecx 498 jne rp_sidt 499 # 设置几个已定义的中断向量 # 宏定义 500 .macro set_early_handler handler,trapno 501 lea \handler,%edx 502 movl $(__KERNEL_CS << 16),%eax 503 movw %dx,%ax 504 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 505 lea idt_table,%edi 506 movl %eax,8*\trapno(%edi) 507 movl %edx,8*\trapno+4(%edi) 508 .endm 509 # 预先设置的中断向量 510 set_early_handler handler=early_divide_err,trapno=0 # 被零除 511 set_early_handler handler=early_illegal_opcode,trapno=6 # 操作码异常 512 set_early_handler handler=early_protection_fault,trapno=13 # 保护错误 513 set_early_handler handler=early_page_fault,trapno=14 # 缺页异常 514 # 后面一段代码定义了这四个中断向量的中断处理例程。 # 它们都调用了 early_fault,即将当前状态、中断向量号等信息通过 early_printk 或 printk 输出。 515 ret
- 检查处理器类型
- 检查是 486 还是 386
- get vendor info
- 如果是 486,就 set AM, WP, NE, MP;如果是 386,就 set MP
- save PG, PE, ET
- check ET for 287/387
- 载入 GDT、IDT
- 重新载入修改 GDT 后的段寄存器
- DS/ES 包含着默认用户段
- 清除 GS、LDT
- i386_start_kernel
如果是 SMP 架构,则由第一个 CPU 调用 start_kernel,其余 CPUs 调用 initialize_secondary
跳转到 i386_start_kernel(在 arch/x86/kernel/head32.c)
head_32.S 中的其余代码是 BSS 段、数据段。
其中,下面这段数据描述了发生未知异常时内核输出的调试信息。
655 int_msg: 656 .asciz "Unknown interrupt or fault at EIP %p %p %p\n" 657 658 fault_msg: 659 /* fault info: */ 660 .ascii "BUG: Int %d: CR2 %p\n" 661 /* pusha regs: */ 662 .ascii " EDI %p ESI %p EBP %p ESP %p\n" 663 .ascii " EBX %p EDX %p ECX %p EAX %p\n" 664 /* fault frame: */ 665 .ascii " err %p EIP %p CS %p flg %p\n" 666 .ascii "Stack: %p %p %p %p %p %p %p %p\n" 667 .ascii " %p %p %p %p %p %p %p %p\n" 668 .asciz " %p %p %p %p %p %p %p %p\n"
下图为 x86 体系结构下的段描述符格式(From ULK)。
arch/x86/kernel/head32.c 中的 i386_start_kernel 只有一条语句 start_kernel(),将跳转到体系结构无关部分的 init/main.c line 534,执行核心数据结构初始化。
5 内核初始化:体系结构无关部分
5.1 核心数据结构初始化
start_kernel 为什么值得开启新的一章呢?因为我们已经跳出了体系结构相关部分,离开了复杂的汇编代码,可以在 C 语言的世界里自由翱翔了。
本节摘抄自参考文献:Linux启动过程综述
start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。
- 输出Linux版本信息(printk(linux_banner))
- 设置与体系结构相关的环境(setup_arch())
- 页表结构初始化(paging_init())
- 使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init())
- 使用alpha_mv结构和entry.S入口初始化系统IRQ(init_IRQ())
- 核心进程调度器初始化(包括初始化几个缺省的Bottom-half,sched_init())
- 时间、定时器初始化(包括读取CMOS时钟、估测主频、初始化定时器中断等,time_init())
- 提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options())
- 控制台初始化(为输出信息而先于PCI初始化,console_init())
- 剖析器数据结构初始化(prof_buffer和prof_len变量)
- 核心Cache初始化(描述Cache信息的Cache,kmem_cache_init())
- 延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay())
- 内存初始化(设置内存上下界和页表项初始值,mem_init())
- 创建和设置内部及通用cache("slab_cache",kmem_cache_sizes_init())
- 创建uid taskcount SLAB cache("uid_cache",uidcache_init())
- 创建文件cache("files_cache",filescache_init())
- 创建目录cache("dentry_cache",dcache_init())
- 创建与虚存相关的cache("vm_area_struct","mm_struct",vma_init())
- 块设备读写缓冲区初始化(同时创建"buffer_head"cache用户加速访问,buffer_init())
- 创建页cache(内存页hash表初始化,page_cache_init())
- 创建信号队列cache("signal_queue",signals_init())
- 初始化内存inode表(inode_init())
- 创建内存文件描述符表("filp_cache",file_table_init())
- 检查体系结构漏洞(对于alpha,此函数为空,check_bugs())
- SMP机器其余CPU(除当前引导CPU)初始化(对于没有配置SMP的内核,此函数为空,smp_init())
- 启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init())
至此,基本的核心环境已经建立起来了。
5.2 设备初始化
本节摘抄自参考文献:Linux启动过程综述
init()函数作为核心线程,首先锁定内核(仅对SMP机器有效),然后调用 do_basic_setup()完成外设及其驱动程序的加载和初始化。过程如下:
- 总线初始化(比如pci_init())
- 网络初始化(初始化网络数据结构,包括sk_init()、skb_init()和proto_init()三部分,在proto_init()中,将调用protocols结构中包含的所有协议的初始化过程,sock_init())
- 创建bdflush核心线程(bdflush()过程常驻核心空间,由核心唤醒来清理被写过的内存缓冲区,当bdflush()由kernel_thread()启动后,它将自己命名为kflushd)
- 创建kupdate核心线程(kupdate()过程常驻核心空间,由核心按时调度执行,将内存缓冲区中的信息更新到磁盘中,更新的内容包括超级块和inode表)
- 设置并启动核心调页线程kswapd(为了防止kswapd启动时将版本信息输出到其他信息中间,核心线调用kswapd_setup()设置kswapd运行所要求的环境,然后再创建 kswapd核心线程)
- 创建事件管理核心线程(start_context_thread()函数启动context_thread()过程,并重命名为keventd)
- 设备初始化(包括并口parport_init()、字符设备chr_dev_init()、块设备 blk_dev_init()、SCSI设备scsi_dev_init()、网络设备net_dev_init()、磁盘初始化及分区检查等等,device_setup())
- 执行文件格式设置(binfmt_setup())
- 启动任何使用__initcall标识的函数(方便核心开发者添加启动函数,do_initcalls())
- 文件系统初始化(filesystem_setup())
- 安装root文件系统(mount_root())
至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后,init()打开/dev/console设备,重定向stdin、stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序。
init()函数到此结束,内核的引导部分也到此结束了,这个由start_kernel()创建的第一个线程已经成为一个用户模式下的进程了。此时系统中存在着六个运行实体:
- start_kernel()本身所在的执行体,这其实是一个"手工"创建的线程,它在创建了init()线程以后就进入cpu_idle()循环了,它不会在进程(线程)列表中出现
- init线程,由start_kernel()创建,当前处于用户态,加载了init程序
- kflushd核心线程,由init线程创建,在核心态运行bdflush()函数
- kupdate核心线程,由init线程创建,在核心态运行kupdate()函数
- kswapd核心线程,由init线程创建,在核心态运行kswapd()函数
- keventd核心线程,由init线程创建,在核心态运行context_thread()函数
参考文献
- 陈香兰老师《Linux内核源代码导读》讲义
- Linux Kernel Documentation
- Understanding the Linux Kernel, Third Edition
- Wikipedia
- CSDN blog: BIOS 启动过程分析
- Windows Internals, Fifth Edition
- IBM developerWorks: Linux 启动过程综述
- FreeBSD 系统结构手册
涉及的代码:
- Linux 2.6.26 Kernel Source
- GRUB source code
- AWARD BIOS source code
Copyright 2012 李博杰 PB10000603
This document is available from http://home.ustc.edu.cn/~boj/courses/linux_kernel/1_boot.html