MIT6.828-Lab1-总结

计算机启动步骤

本lab主要告诉我们如何启动一台计算机,其启动流程分以下三步:
1.BIOS

  • 首先进行硬件自检(Power-On Self-Test),检查硬件能否满足运行的基本条件
  • 根据启动顺序,读取优先级最高的存储设备,(启动顺序可以在BIOS界面中设置)
  • 依次读取扇区(512字节),若最后两个字节为0x550xAA,则表明此扇区为引导扇区,随后将此扇区的512字节读入0x7c00处,此段程序即为bootloader

那么,BIOS的第一条指令为什么在0xffff0处呢?

因为不同厂家的BIOS程序大小不一,所以如果将BIOS放在0x0000处,会导致用户可用的内存不是从0x0000开始,并且8086规定,CPU必须先从0xffff0开始,并且0xffff0处必须是一条无条件转移指令JMP用于跳转到BIOS所在的位置。
2.BootLoader-boot.S

  • 打开A20地址线以便于访问超过1M的地址
  • 构建并加载GDT
  • 寻址模式从real-mode转变为32位protected-modereal-mode用段寄存器里存放的是段基址,但protected-mode中段寄存器里存放的是段选择子,使用段选择子在全局描述符表中获取段基址,所以在此过程中可以加一道特权级检查

为什么BootLoader被加载到内存0x7c00的位置呢?

因为Intel最早的第一台个人电脑芯片8088,搭配的操作系统86-DOS,此操作系统占用内存32KB, 同时芯片本身占用了0x0000-0x03ff的位置用于保存中断处理程序的地址,故只剩下了0x0400-0x7fff, 考虑到在OS被加载到内存中后,BootLoader就不会再被用到了,所以把BootLoader加载到0x7fff - 512B - 512B的位置处便于后续操作系统利用这片内存。(两个512B是因为考虑到BootLoader也会产生数据)

验证BootLoader

0x7c00-0x7dff属于BootLoader0x7e00-0x7fff属于相关数据,使用以下命令检查引导扇区的特征字节

3.BootLoader-main.c

  • 将kernel的ELF文件读入到物理地址0x1000
  • 根据ELF文件跳转到内核代码(load address在0x100000,入口在0x10000c,这两个数值是在kern/kernel.ld中通过ENTRY(_start).text : AT(0x100000)指定的),控制权交给内核
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    struct Proghdr *ph, *eph;

    // read 1st page off disk
    readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

    // is this a valid ELF?ELF
    if (ELFHDR->e_magic != ELF_MAGIC)
    goto bad;

    // load each program segment (ignores ph flags)
    ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff); // e_phoff表示程序头表相对于ELF头的偏移地址
    eph = ph + ELFHDR->e_phnum; // e_phnum表示有几个程序头
    for (; ph < eph; ph++)
    // p_pa is the load address of this segment (as well
    // as the physical address)
    readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

    // call the entry point from the ELF header
    // note: does not return!
    // 使用e_entry跳转到内核代码
    ((void (*)(void)) (ELFHDR->e_entry))();

ELF格式简介

  • 我们上面是将kernel的ELF头加载到0x1000处了,ELF头主要有三个信息: 程序入口地址相关e_entry、程序头表相对于ELF头的偏移地址e_phoff、程序头个数e_phnum
  • 程序头(programheader,缩写为Proghdr),同样主要有三个信息:程序头类型(或者说段类型)p_type、程序段在硬盘中的位置p_offset、程序段在内存的物理地址和虚拟地址p_pa,p_va
  • 使用readelf -l obj\kern\kernel查看kernel的所有程序头信息
  • 使用readelf -h obj/kern/kernel查看程序头表(也可以叫ELF头)

4.内核!启动!- entry.S

  • 创建临时页表,将虚拟地址[0, 4MB)和[KERNBASE, KERNBASE + 4MB)的位置全部映射到物理地址[0, 4MB),这样子在后面可以既在低地址运行也可以在高地址运行内核(因为需要通过reloc才能到高地址运行内核)
  • entry_pgdir的地址赋值给cr3寄存器,并开启cr0寄存器的PG位,这样子内核就可以使用我们定义的页表了

5.call i386_init函数

  • 操作系统的各种初始化,初始化控制台,内存管理,进程初始化,中断向量表初始化等等

GCC calling convention for JOS

让我们看下面这段汇编代码,请记住,在call时,实际上进行两个指令:push %eipmovl $0x12345, %eip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; 假设执行函数前堆栈指针 ESP 为 NN
push p2 ; 参数 2 入栈,ESP -= 4h , ESP = NN - 4h
push p1 ; 参数 1 入栈,ESP -= 4h , ESP = NN - 8h
call test ; 压入返回地址 ESP -= 4h, ESP = NN - 0Ch
;// 进入函数内
{
push ebp ; 保护先前 EBP 指针, EBP 入栈, ESP-=4h, ESP = NN - 10h
mov ebp, esp ; 设置 EBP 指针指向栈顶 NN-10h
mov eax, dword ptr [ebp+0ch] ;ebp+0ch 为 NN-4h, 即参数 2 的位置
mov ebx, dword ptr [ebp+08h] ;ebp+08h 为 NN-8h, 即参数 1 的位置
sub esp, 8 ; 局部变量所占空间 ESP-=8, ESP = NN-18h
...
add esp, 8 ; 释放局部变量,ESP+=8, ESP = NN-10h
pop ebp ; 出栈,恢复 EBP, ESP+=4, ESP = NN-0Ch
ret 8 ;ret 返回,弹出返回地址,ESP+=4, ESP=NN-08h, 后面加操作数 8 为平衡堆栈,ESP+=8,ESP=NN, 恢复进入函数前的堆栈.
}

每当进入函数时,便会执行两条指令,push %ebpmov %esp %ebp(和上面代码不一样是因为我写的是AT&T语法),ebp寄存器用于保存当前所执行的函数的基址,因为我们知道在执行函数是esp的值会不断减小,所以我们需要使用ebp来保存esp变化之前的值,以便于我们读取函数参数。
下面是我画的每次函数执行时,stack的部分图片:

目前为止,物理内存里长啥样?

tips

从一开始的配置环境,到最后完成整个lab,最大的印象是无论如何都要认真地看完每一段话,我仍记得当我发现首次进入内核时地址为0x10000c时的疑惑,直到后面Exercise 4时,要求我们使用objdump -f obj/kern/kernel 来verify时,方能恍然大悟;我仍记得想破脑袋也没想明白为什么bootloader0x7c00开始,直到查阅相关资料才发现这只是历史遗留问题 。

  • 想要真正读懂bootloader(boot.S和main.c)还是得去认真地把xv6-book的附录B部分过一遍。
  • 在entry.S中有一行指令mov $relocated, %eax,直到此命令执行结束后,内核代码才真正地在高memory地区执行,在此之前,如果对高地址进行breakpoint是没有任何效果的。
  • 关于GCC calling conventions for JOS的部分一定要着重理解,只有了解的足够深,才能意识到为什么eip可以通过*((uint32_t *)ebp + 1)来获取(与call指令本质上的push %eip有关),也才能意识到C语言中函数参数是从按照声明顺序从后往前push入栈的。
  • 关于backtrace中的stab.h部分,n_value是用来表示符号地址的(虽然不知道为什么注释要写// value of symbol),n_desc对于N_SLINE类型是表示行号的(虽然只看注释我也看不出来,但我猜对了)。

MIT6.828-Lab1-总结
http://bugeater.space/2024/02/27/MIT6-828-Lab1-总结/
Author
BugEater
Posted on
February 27, 2024
Licensed under