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,这样以后执行的地址就在实模式的寻址范围内了。附上物理内存图:

         | 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 0x7C00
001000   +------------------------+
         | Reserved for MBR/BIOS  |
000800   +------------------------+
         | Typically used by MBR  |
000600   +------------------------+
         | BIOS use only          |
000000   +------------------------+

  接下来,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函数调用的第一个函数,它完成了两个工作:

  1. 把header.S中的hdr拷贝到boot_params中。
  2. 如果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就进入保护模式。 这里有两个问题我以前没深入了解过:

  1. 在启动保护模式前,kernel没有page table可以使用。
  2. 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

https://manybutfinite.com/post/how-computers-boot-up/

https://en.wikipedia.org/wiki/Control_register

posted @ 2021-02-26 19:13  Nanachi  阅读(539)  评论(0)    收藏  举报