Linux 0.11 boot:main 醒之前

1,344 阅读4分钟

我正在参加「掘金·启航计划」


这篇文章是某蒟蒻的 读 Linux 源码 操作系统课笔记。

linux-boot.png

Linux 0.11 boot:main 醒之前

main 醒之前,即梦醒之前(郊区工地口音下的谐音梗),也就是最简单的部分(梦谁还不会做了):进入系统之前的引导、基础初始化工作。这篇笔记是读源码: /boot 目录下的三个汇编文件。

开机:操作系统 boot:

  • bootsect 载入 kernel code
  • setup:设中断,etc. 从 16b 实模式 -> 32b 保护模式
  • head:kernel 的头

代码在 /boot


开机

CPU 加电:

  • 进入实模式:16 bit (最大寻址 1 MB)

  • 设置第一行代码:

    CS:IP = 0xF000:0xFFF0 
        => 0xFFFF0
        => BIOS
    
    • CS:代码寄存器,IP:指令寄存器(段内 offset)

BIOS 启动

BIOS 启动 -> BIOS 自检 -> BIOS 程序从南桥映射到 mem (启动块、中断向量、中断服务程序)-> 开始执行 BIOS 引导程序:

INT 0x19:加载第一扇区 -> 0x07C00

  • 第一扇区:bootsect:Linux 引导程序

bootsect

bootsect:规划内存,加载第二、三批程序(setup、head + kernel,即整个 OS)

  1. 设常量:定一些位置:*SEG

  2. 复制 bootsect:

    • 把自身程序(512B)从 BOOTSEG=0x07C00 -> INITSEG=0x90000:即是主(as code: running)又是客(as data: copied)
    • 跳到新的位置继续跑
    • 这个操作脱 BIOS 支配:OS 按照自己意志安排内存
  3. 设置栈:以后就可以用栈了,之前不可以

  4. 将 setup 程序加载到内存 SETUPSEG=0x90200 (紧挨着 bootsect)

    • INT 0x19:BIOS 的中断服务
    • setup 是 2~5 扇(共 4 * 512 B
  5. 载入第三批程序:系统模块 -> SYSSEG=0x10000

    • 还是 INT 0x19
    • 有 240 扇:多、慢
    • 显示 "Loading system ..."
  6. 确定根设备(不重要)

    • 根设备:根文件系统的设备:others FS 挂接其上

    • Linux 0.11 启动需要:系统内核镜像 + 根文件系统

      root_dev:
          主设备号 = 2
          次设备号 = type * 4 + nr
              
      type {
          2: 1.2  MB
          7: 1.44 MB
      }
      
      nr {
          0 A驱
          1 B驱
          2 C驱
          3 D驱
      }
      
  7. jmpi 0, SETUPSEG:跳到 setup 执行

setup

setup:从 16 位实模式 -> 32 位保护模式

  1. 从 CMOS 拿机器系统数据: ^96f613

    • 510B 数据 -> [0x90000, 0x901FD]
      • 覆盖 bootsect 程序区域(512B)
      • 只差 2B 就到 setup (0x90200)
      • 卸磨杀驴 + 精打细算
    • CMOS 信息是 BIOS 自检写入的
    • 完成数据读取用的还是 BIOS 的中断服务
  2. cli 关中断

    • cli:设置 Reg.EFLAGS.IF = 0
    • 后面很长时间内一直关:setup 关,head 也关,直到 main 末尾才开(sti()
    • 关中断:不是发不出中断,只是 CPU 不理会来的中断
  3. 复制内核代码:0x10000 -> 0x00000

    • 0x10000:SYSSEG:内核 code:head + C 程序
    • 0x00000:覆盖 BIOS 的 INT 表、服务程序
    • 整体左(低)移 0x10000 (64KB)
  4. 做 IDT、GDT

    • IDT:中断描述符表
    • GDT:32 位下的全局(段)描述符表
      • 描述段:基址、限长、特权级
    • 这两个表硬编码在 setup.s 里:
      • IDT: idt_48 此时是空的
      • GDT: gdt_48:有三项(NULL、内核代码段、内核数据段)
    • 用 IDTR、GDTR 寄存器指向两个表
      • 安全:user 不能改:用户做假表无用,CPU 不认

    gdt.jpg


特权级基于段:CPU 执行式,检查段是否有特权

  • 访问控制施加于线性地址

GDT 本身不是段,但 LDT (Local)是段:

  • GDT:没人描述 GDT 的基址、限长
  • LDT:GDT 中描述了 LDT 段

  1. 开 A20:实现 32 位寻址:寻址空间 1MB -> 4GB

    • A20 是第 21 根寻址线,开 A20 就开了寻址线 20 到 31:

      • 20 根线(0xFFFFF:1MB) -> 32 根线(0xFFFFFFFF:4GB)
    • 32 位:最大地址 0x8个F:4GB

    • 但是 Linux 最大物理内存支持 16 MB:4GB 绰绰有余

(至此:32 位开)

  1. 改 8259A:重新映射中断

  2. 开保护模式:Reg.CR0.PE = 1 (PE 是 CR0[0])

(至此:保护模式开)


保护模式:分段保护机制

  • 代码只能同级跳
  • 数据可以高访低

  1. jmpi 0, 8:跳到 head 执行

    • 0:offset

    • 8:seg selector:

      8 = 1 0 00:
      
       1  : GDT[1]: 内核代码段
       0  : (G or L)DT:0  GDT
      00  : 特权级(RPL)       
      

特权级:

  • CPL:current:当前指令的
  • DPL:descriptor:描述符里的
    • code seg 跑起来时 CPL <- DPL
  • RPL:requested:RPL == DPL 才能跳

Reference: Intel64-IA32-SDM Vol3 5.5 (P vol.3A 5-7,PDF 2979/4834)


head

head:做分页

  1. 标号 pg_dir 指向 0x00000

    • 预留:之后把页表目录(data)放这里,覆盖后面的部分 head 程序(code)
  2. 设置 DS、ES、FS、GS 为 0x10:

    • 0x10 = 0001, 0 0 00:选择子(同 jmpi 0, 8 的 8):selector(GDT[2],GDT,0特权)
    • 最终指向内核数据段
  3. 设置系统“用户”栈:

    • 设 esp 指向 C 语言里定义的 user_stack 数组

      ![TODO: user_stack 在内存中的位置](TODO)

  4. 设 IDT:

    • 开 256 项,全指向 ignore_int(就在 head.s 程序里):

    • 实际工作:printk("Unknown interrupt")

    • 中断向量:16 位实模式 v.s. 32 位保护模式: int-16-vs-32.png


  • printk:kernel:在内存中用的
  • printf:format or file:FS 启动后才能用

现在关中断了,在这里急于设置 IDT:

  • 一方面,可以做测试:在后面临时加一行 sli,测试时候中断相应是否正确
  • 另一方面,所有在后面没设置的中断都会走这里设的 ignore_int (这个 handler 代码刚好没被覆盖,永久留下来了)

  1. 设 GDT:

    • NULL
    • 内核代码段:0x08,16MB
    • 内核数据段:0x10,16MB
    • NULL
    • 252 项预留:.fill repeat, size, value 伪指令:原地重复创建 repeat 个 size 大小的空间:每个都置值为 value
  2. 再设 DS、ES、FS、GS 为 0x10:GDT 改了之后这些要刷新,重置可以强制刷新

  3. 检验 A20 时候打开:

    • 用 19 位的最大值 + 1:看是否溢出回滚,即:
    Loop:
        往 0x000000 放值
        if 0x100000 == 0x000000: 
            jmp Loop    # neq 就再测:没开 A20 就死循环
        A20 正确设置 ✅ 
    
  4. check_x87:检查是否有数学协处理器(486 就开始内置了)

  5. 标号(data):pg0pg1pg2pg3:放 4 个内核页表

    • 一个表 4 KB:刚好是 1 页
    • 一个表放 1024 项
  6. main 压入栈:模拟 call (先设好放着,之后再用)

    • 压参数:3 个 0
    • 压返回地址:L6: main 不该返回的,返回就去 L6 死循环
    • 压 main 函数地址
  7. setup_paging:开分页

    1. 内存清零:pg_dir + 4 个页表(pg\d
    2. 4 个页表放到 pg_dir 里:mov $pg0 + 7, _pg_dir
      • $pg0 + 7 是地址 + 信息:
        • 页地址只用 20b (32-12:4KB 对齐)
        • 空出来 12 b 放(控制)属性:7 = 111 存在且用户可读写
          • 2(U/S):0 超级用户、1 用户
            • 为何置 1:后面 move_to_user_mode iret 翻到 PL 3 可以跑!!important!!
          • 1(W/R):0 只读、1 可读写
          • 0(P):1 为页存在
          • 控制加在线性地址上
    3. 填写 4 个页表中的内容:线性 <-> 物理:恒等映射
    4. Reg.CR3(页目录基址寄存器):指向 pg_dir物理地址(少有的物理地址,如果这里再用线性,寻址就套娃了,单程递归)
    5. Reg.CR0.PG:启用分页
      • IA32 开了 PE 才能开 PG

一个 pg_dir 对应一个线性空间

  • 换 pg_dir 就换线形空间:但是 32 位系统,一个就够了

  1. ret:main 出栈运行

ret:
    pop addr
    jmp addr
    
call:
    push ret_addr # ret_addr 即 call 的下一行代码
    jmp  func

Why ret, not call:

  • call 暗示还有回来
  • ret 暗示不回来:boot 真心为 kernel:直接禅让,不僭越

但是好像其实用 jmp 也行。


至此,head 结束,汇编的 boot 结束,转到 C 程序运行了。

此时内存如下:

mem-after-head.png