非常好!!!Linux源代码阅读——内核引导【转】

Linux源代码阅读——内核引导

转自:http://home.ustc.edu.cn/~boj/courses/linux_kernel/1_boot.html

目录

  1. Linux 引导过程综述
  2. BIOS
    • POST
    • 自举过程
  3. Boot loader
    • 主引导扇区结构
    • GRUB stage1
    • GRUB stage2
  4. 内核初始化:体系结构相关部分
    • 内核映像结构
    • header.S
    • 初始化与保护模式
    • 自解压内核
    • startup_32
  5. 内核初始化:体系结构无关部分
    • 核心数据结构初始化
    • 设备初始化

1 Linux引导过程综述

  1. BIOS 
    在 i386 平台中,由 BIOS 作最初的引导工作,执行加电自检、初始化,读取引导设备的主引导扇区并执行。
  2. Boot loader(以 GRUB 为例) 
    MBR 中的、紧随 MBR 后的 phase 1/1.5 boot loader 载入文件系统中的 phase 2 及其配置,显示操作系统选择菜单,执行用户命令,载入选定的操作系统内核与 initrd。
  3. 内核初始化:体系结构相关部分 
    从 header.S 开始,到 main.c 初始化参数,再到 pm.c 进入保护模式,然后载入 vmlinuz 并自解压,在 startup_32.S 中开启分页机制、初始化中断向量表、检测 CPU 类型等,完成 x86 体系结构的保护模式初始化。这是本文重点。
  4. 内核初始化:体系结构无关部分 
    分为核心数据结构初始化(start_kernel)和设备初始化两个阶段。
  5. 用户态初始化 
    以下内容超出了本文范围。用户态的 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 函数过程中实现,主要步骤如下:

  1. 初始化各种主板芯片组
  2. 初始化键盘控制器
  3. 初始化中断向量、中断服务例程
  4. 初始化 VGA BIOS 控制器
  5. 显示 BIOS 的版本和公司名称
  6. 扫描各种介质容量并显示
  7. 读取 CMOS 的启动顺序配置
  8. 调用 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 像一个微型操作系统。

  1. BIOS 加载 GRUB stage1(如果安装到 MBR)到 0x00007C00.

  2. 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
    
  3. 由于 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 接受用户命令,载入选定的操作系统内核。

  1. stage2 的入口点是 asm.s
    #ifdef STAGE1_5
    # define	ABS(x)	((x) - EXT_C(main) + 0x2200)
    #else
    # define	ABS(x)	((x) - EXT_C(main) + 0x8200)
    #endif
    
    1. 初始化一些变量
    2. 跳转到 code_start
    3. 关中断,设置段寄存器和堆栈起始地址
    4. 从实模式切换到保护模式
    5. 清空 bss 段
    6. init_bios_info()
  2. 随后进入 stage2.c,执行 GRUB 的主要功能。

    • cmain(): 主函数,载入配置文件 menu.lst(GRUB 1)或 grub.cfg(GRUB 2),如果成功载入就进入 run_menu(),显示菜单,进入循环倒计时,如果超时就进入第一个,如果用户按了键就停止倒计时。用户作出选择后,跳转到 boot_entry(),清空屏幕、获取入口,通过 find_command 找到的函数指针调用相应的命令。

    • 如果没有成功载入配置文件,就 enter_cmdline(),也是通过 find_command 调用相应的命令。

  3. 每个 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[];
    
  4. 常用 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)
  5. 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)列表、一个符号列表,一个符号可以是已定义或未定义的。每个已定义的符号有地址。未定义的符号则要在链接时从其他文件中寻找其定义。

  1. 指定输出文件格式
    OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
  2. 指定目标体系结构
    OUTPUT_ARCH(i386)
  3. 设置入口点
    ENTRY(_start)
  4. 输入文件各节到输出文件的映射
    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 启动选项的掩码。
  • Bit 0: LOADED_HIGH (1表示保护模式代码加载到 0x100000)
  • Bit 7: CAN_USE_HEAP (为1表示 heap_end_ptr 有效)
code32_start 0x214 4 内核解压缩前立即跳转到的 32 位 flat-mode 入口
ramdisk_image 0x218 4 initramfs 的 32 位线性地址
cmd_line_ptr 0x228 4 内核命令行的 32 位线性地址

下面我们迎来了真正的起点(start_of_setup),主要流程为:

  1. 复位硬盘控制器
  2. 如果 %ss 无效,重新计算栈指针
  3. 初始化栈,开中断
  4. 将 cs 设置为 ds,与 setup.elf 的入口地址一致
  5. 检查主引导扇区末尾标志,如果不正确则跳到 setup_bad
  6. 清空 bss 段
  7. 跳到 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。

  1. realmode_switch_hook():boot_params.hdr 中有 realmode_swtch,记录了 hook 函数地址,如果有的话就执行之
  2. reset_coprecessor(): 重启协处理器
  3. make_all_interrupts(): 关闭所有旧 PIC 上的中断。其中的 io_delay 等待 I/O 操作完成。
  4. setup_idt(): 初始化中断描述符表 (空的)
  5. 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 数据结构示意:

  6. 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 开始的内存内容。

  1. 找到 vmlinux 的入口地址,并将其存入 ebp。
  2. 如果设置了可重入内核,就将 ebp 按照 kernel_alignment 对齐,放入 ebx。
  3. 确定解压内核的内存地址
  4. 设置栈
  5. 将 vmlinux 复制到安全地区(ebx 指定的地方):保存 esi 到栈中,首先计算出需要复制的字节数目,然后4个字节为一组地复制过去,再从栈中恢复 esi。
  6. 进入 relocated,清空 BSS,初始化解压函数所用的栈
  7. 将 decompress_kernel 所用的参数入栈:内核加载地址、内核长度、压缩内核安全地址、堆地址、启动参数结构体指针。
  8. 调用 decompress_kernel 解压内核
  9. 如果设置了可重入内核,进行一些 relocate
  10. 跳转到解压后的内核。

至此,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 依次完成以下动作:

  1. 初始化参数

    • 初始化 GDT。boot_gdt_descr 在数据区中记载了 GDT 表首地址。
      lgdt pa(boot_gdt_descr)
    • 清空 BSS 段
    • 复制实模式中的 boot_params 结构体
    • 复制命令行参数到 boot_command_line (供 init/main.c 使用)
    • 有关虚拟环境的一些配置
  2. 开启分页机制

    尽管我们已经在保护模式中,但只有段机制而没有启用页机制。这里设置全局页目录与页表项,并开启分页机制。

    下图示意了 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 */
      
  3. 初始化 Eflags

  4. 初始化中断向量表

    在实模式中,已经初始化了 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
    
  5. 检查处理器类型

    • 检查是 486 还是 386
    • get vendor info
    • 如果是 486,就 set AM, WP, NE, MP;如果是 386,就 set MP
    • save PG, PE, ET
    • check ET for 287/387
  6. 载入 GDT、IDT

    • 重新载入修改 GDT 后的段寄存器
    • DS/ES 包含着默认用户段
    • 清除 GS、LDT
  7. 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 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

posted @ 2016-07-04 16:59  Sky&Zhang  阅读(8935)  评论(0编辑  收藏  举报