XV6_lab1

  1. PC启动,QEMU x86仿真器及PC通电启动过程
  2. 理解6.828内核boot loader(位于boot目录下)
  3. 深入探究JOS内核初始化模版(位于kernel目录下)

一 PC启动

模拟x86:用Qemu虚拟及模拟x86,通过qemu和gdb联调对PC启动跟踪测试。

(本人这里源码已经编译过了)

进去lab目录,terminal中键入make qemu命令:

kernel监视器启动成功,其中有两个命令,help和kerninfo

kerninfo显示了start(entry)的物理地址:0010000c

但是entry的虚拟地址为:f010000c,具体原因后续分析。

PC的物理地址空间:
ç©çå°å空é´å¸å±

第一代的PC是基于16位intel 8088处理器,仅能寻址1MB的物理内存。因此早期PC的物理地址空间起于0x00000000,但是止于0x000FFFFF,而不是0xFFFFFFFF。640KB的空间标记为“低地址”,是早期PC仅能使用的唯一RAM。事实上,非常早期的PC仅能分配16KB,32KB,64KB的RAM。

0x000A0000到0x000FFFFF的384KB空间被硬件保留用于诸如视频播放缓冲等特殊用途。这片保留区域最重要的部分就是Basic Input/Output System(BIOS),BIOS占据了从0x000F0000到0x000FFFFF的64KB空间。在早期PC中,BIOS被保存在真正的ROM中,但是现在计算机把BIOS保存在可更新的flash存储器中。BIOS负责执行诸如激活显卡以及检查已装入的内存总量这些基本的系统初始化。执行完初始化之后。BIOS从某个合适的地方,比如软盘/硬盘/光盘以及网络中载入操作系统并将机器的控制权转交给操作系统。

当intel打破了“1MB障碍”之后,为了后向兼容已有的软件,PC设计师保留了最低的1MB地址空间布局。因此,现代PC在物理地址0x000A0000到0x00100000有一个“洞”,把RAM分成了“低的”或“传统的内存”(前面的640KB)和“扩展内存”(所有剩下的)。另外,在32位物理地址空间最顶部,在所有物理RAM之上的空间被BIOS保留用于32位PCI设备。

现在x86处理器可以支持高达4GB的物理RAM,因此RAM可以扩展到0xFFFFFFFF。在这种情况下,BIOS必须留出第二个“洞”,也就是在32位可寻址空间的顶部,留出需要映射的32位设备的地址空间。由于设计上的限制,JOS仅使用开始的256MB物理内存。

BIOS boot loader:

使用 make qemu-gbd 和 make gdb 联调kernel

第一条指令为:[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b

可以知道:

  • PC从0x000ffff0执行,位于64kb  
  • 第一条为跳转指令,跳转到CS=oxf000,IP=0xe05b(跳转到BIOS前面更前的位置)

进入gdb后,可以使用i r指令查看各寄存器的值,通过x指令查看内存。还可以用i r ax查看指定的寄存器。gdb调试的时候,step和next指令都是不能使用的,单行执行汇编指令stepi可用。

可以看到加电后,各个寄存器的情况:大部分寄存器都置0,但是CS IP中的值不同。和我们起始工作地址相同。

二 Boot Loader

PC的磁盘被分成一个个大小位512字节的区域,称为扇区(sector)。一个扇区是磁盘的最小转移粒度:每一次读或写操作大小必须是一个或多个扇区并且要和扇区边界对齐。如果磁盘是可引导的,那么第一个扇区成为引导扇区(boot sector),这个扇区是存放boot loader代码的地方。当BIOS找到可引导磁盘之后,它把512字节的引导扇区加载到物理地址从0x7c00到0x7dff的内存中,并使用jmp指令将CS:IP设置为0000:7c00,将控制权转交给boot loader。和BIOS的载入地址类似,这段地址对于PC来说是固定的和标准化的。

在计算机发展过程中,从CD-ROM引导的能力要出现的晚得多。因此,设计师有机会重新思考引导过程。现在从CD-ROM引导的BIOS有一些复杂,CD-ROM的扇区大小是2048字节,而不是512字节,BIOS可以把更大的引导镜像载入内存。

6.828中,我们使用传统的硬盘引导方式,这意味着我们的boot loader必须满足512字节。xv6的boot loader由一个汇编源文件boot/boot.S和一个C源文件boot/main.c组成。boot loader必须完成两个重要功能:

  • 第一,引导加载器把处理器从实模式切换到32位保护模式,因为只有在该模式下,软件才能访问物理地址空间中所有高于1MB的内存。在保护模式下,分段地址(段地址:偏移地址)到物理地址的转换有所不同,偏移地址是32位,而不再是16位。
  • 第二,引导加载器直接通过特殊的x86 I/O指令访问IDE磁盘设备寄存器来读取内核。

在理解了boot loader的源代码之后,查看一下obj/boot/boot.asm文件,这个文件是GNUMakefile在编译完boot loader之后创建的可执行文件的反汇编。通过这个文件可以清楚的知道boot loader的代码在物理内存的具体位置,也更易于知道在GDB中单步执行boot loader时发生了什么。同样,obj/kern/kernel.asm 是JOS kernel的反汇编,这个文件在调试内核的时候很有帮助。

你可以在GDB中使用b命令设置地址断点。举个例子,b *0x7c00就在地址0x7c00处设置了一个断点。在断点处,你可以使用csi命令继续执行:c命令使QEMU继续运行直到遇到下一个断点;si N可以一次跳过N条指令。

通过在boot loader 起始地址0x7c00设置断点,跳到boot loader的起始地址,使用si单步执行:

可以看到和boot.asm的反汇编文件是一致的,通过单步执行和boot.asm文件可以更好的理解内核运行的代码

观察设置A20地址线:

在设置完毕7c1a处设置断点,观察后续情况:

设置好A20地址线后,设置了bootstrap GDT表,实现了虚拟地址和物理地址的对等映射,进而进入了保护模式,地址为00007c32

此时,地址表示方式发生了变化,变为了0x00007c32

练习3:阅读实验工具指导,尤其是GDB部分。

在0x7c00处设置断点。继续执行,然后单步调试,和obj/boot/boot.asm反汇编进行对照。

跳到bootmain()中的readsect()函数,跟踪这个函数,跳回bootmain()函数,确定从磁盘读取剩余内核扇区的for循环的头和尾。找出for循环结束之后将要执行的代码,在此处设置断点,然后单步执行完剩余的boot loader代码。

设置好栈后,进入bootmain:地址为7d15,先查看一下

1.在什么地方,处理器开始执行32位代码?是什么导致了从16位模式到32位模式的转变?

2.Bootloader执行的最后一条指令是什么,kernel加载的第一条指令是什么?

/boot/main.c bootmain()函数中的最后一条语句为((void (*)(void)) (ELFHDR->e_entry))();,ELFHDR是指向0x10000(被强制转换为struct Elf类型)的指针,通过readseg函数初始化ELFHDR,这个初始化的数据来源就是硬盘上的内核镜像。怎么看ELFHDR->e_entry指向的位置呢?反汇编kernel镜像!

可以看出kernel的起始地址是0x0010000c,GDB设置断点调试,可以看到kernel的第一条指令就是:

3.Bootloader是怎么确定要读取多少扇区以保证可以从磁盘获取整个内核的?它是在哪里找到该信息的呢?

obj/kern/kernel的ELF头:

VMA为虚拟地址,LMA为物理地址

obj/boot/boot.out的ELF头:

注意!对于boot loader的加载地址,VMA==LMA,但是对于kernel的加载地址,VMA不等于LMA

三 内核Kernel

当你观察bootloader的link address和load address时,它们是完全一致的,但是kernel的这两个地址却大不一样。和bootloader类似,内核也以一些汇编代码开始,做好必要准备之后C代码才能正常运行。

为了脱离供用户程序使用的处理器虚拟地址空间较低的部分,操作系统的内核通常连接并运行在很高的虚拟地址上,比如0xf0100000。这样安排的原因在下一个实验中会详细说明。

许多机器在地址0xf0100000处并没有物理内存,因此我们不能指望能够在这种地方存储内核。相反,我们使用处理器的内存管理硬件将虚拟地址0xf0100000(内核代码运行的link address)映射到物理地址0x00100000(bootloader加载内核到内存的地址)。这样,尽管内核的虚拟地址的高度足够给用户进程留出充足的地址空间,内核还是会加载到物理内存RAM的1MB处,就在BIOS ROM之上。

实际上,我们在下一个实验中将把PC物理内存最低的256MB地址空间,从0x00000000到0x0fffffff映射到虚拟地址从0xf0000000到0xffffffff。现在你应该明白为什么JOS只能使用前256MB物理内存了。

到目前为止,我们只映射前4MB物理内存,这足够我们启动和运行了。我们使用kern/entrypgdir.c中手写的,固定初始化的页目录和页表来完成这一映射。到目前为止,你不必知道具体的细节,只需要明白达到的效果。直到kern/entry.S设置好CR0_PG标志之前,内存的引用都是作为物理地址对待(严格的讲,应该是线性地址,但是boot/boot.S建立了从线性地址到物理地址的等价映射,我们永远不会改变这一点)。一旦CR0_PG标志设置好,内存的引用就是通过把虚拟地址映射为物理地址来实现的。entry_pgdir将0xf0000000到0xf0400000范围的虚拟地址和0x00000000到0x00400000范围的虚拟地址映射为从0x00000000到0x00400000的物理地址。任何不在这两个范围内的虚拟地址都将引起硬件异常,因为我们还没建立异常处理,这会导致QEMU宕机或者无限重启。

练习7:Use QEMU and GDB to trace into the JOS kernel and stop at the movl %eax, %cr0. Examine memory at 0x00100000 and at 0xf0100000. Now, single step over that instruction using the stepi GDB command. Again, examine memory at 0x00100000 and at 0xf0100000. Make sure you understand what just happened.

What is the first instruction after the new mapping is established that would fail to work properly if the mapping weren’t in place? Comment out the movl %eax, %cr0 in kern/entry.S, trace into it, and see if you were right.

查看obj/kern/kernel.asm,找到命令:movl %eax, %cr0,地址为f0100025

地址为f0100025,实际上加载到物理地址100025上:

movl %eax, %cr0处设置断点,执行到此处。在执行这条命令之前,发现0x00100000和0xf0100000两个地址的内容是不同的,执行完之后二者就变成相同的了。原因就是在执行这条指令之前,还没有建立分页机制,高地址的内核区域还没有映射到内核的物理地址只有低地址是有效的;执行完这条指令之后,开启了分页,由于有静态映射表(kern/entrypgdir)的存在两块虚拟地址区域都映射到同一块物理地址区域。

注释掉kern/entry.S中的movl %eax, %cr0,这样就无法开启分页,虚拟地址无法映射到物理地址。所以,此时第一条执行失败的命令是 0x100031: mov $0xf0110000,%esp,因为0xf0110000是高的虚拟地址,由于没有分页,CPU不知道访问哪一个物理地址。

此时的内核基本是什么事情都干不了的,内核现在存在内存低地址处,内核加载地址为0x100000,但是内核中的符号的虚拟地址以0xf0100000为开始的,内核虚拟地址在高地址处,现在保护模式下虚拟地址等于物理地址,内核中所有以地址为目标的跳转都将跳转到物理地址的高地址处,而在那里的都是垃圾数据。所以,内核在一开始就必须设置页表,以便之后能够正常跳转和寻址。
 

终端格式化输出

大多数人都把类似于printf()的功能看成理所当然的,有时甚至认为他们是C语言的“基本”。但是,在一个OS内核中,我们不得不亲自实现所有的I/O。

通读kern/printf.clib/printfmt.ckern/console.c这三个文件,确保你理解它们之间的关系。在后续实验中你会明白为什么printfmt.c放在一个单独的lib目录下。

要求补全/lib/printfmt.c中“%o”部分的代码,这是八进制输出,仿照十进制“%d”代码如下:

case 'o':
    num = getuint(&ap, lflag);
    base = 8;
    goto number;

  1. 解释printf.c和console.c之间的接口。尤其是,console.c输出什么函数?printf.c是如何使用该函数的?

printf.c中的cprintf函数调用vcprintf,vcprintf调用lib/printfmt.c中的vprintfmt,vprintfmt调用printf.c中的cputchar函数,最终cputchar函数实现字符打印。

  1. 解释下面console.c的代码片段:
if (crt_pos >= CRT_SIZE) {
              int i;
              memcpy(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
              for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
                      crt_buf[i] = 0x0700 | ' ';
              crt_pos -= CRT_COLS;
      }

这段代码的作用是检测当前屏幕的输出buffer是否满了,注意这里的memmove其实就是把第二个参数指向的地址内容移动n字节到第一个参数指向的地址。n由第三个参数指定。如果buffer满了,把屏幕第一行覆盖掉逐行上移,空出最后一行,并由for循环填充以空格,最后把crt_pos置于最后一行的行首。

栈~

练习9:确定内核是在何处初始化它的栈的,它的栈位于内核中何处?内核是怎么为自己预留栈空间的?初始化栈指针指向这块预留区域的哪个“端”?

在kern/entry.S 中可以发现栈的初始化部分:

内核初始作的工作主要是将寄存器%ebp初始为0,%esp初始化为bootstacktop。(ESP就是一直指向栈顶的指针,而EBP只是存取某时刻的栈顶指针,以方便对栈的操作,如获取函数参数、局部变量等。)

我们发现栈有两部分,第一部分是实际栈空间,一共KSTKSIZE,其大小定义在inc/memlayout.h中,KSTKSIZE = 8 × PGSIZE = 8 × 4096B = 32KB,另一部分是栈底指针bootstacktop,因为它指向栈空间定义完以后的高地址位置。前面我们说过栈是向低地址增长的,所以最高位置就是栈底,这个位置会作为初值传递给%esp。
X86栈指针(esp寄存器)指向当前正在使用的栈的最低位置。在这片栈区域上低于该位置的一切空间都是可用的。栈是向下生长的,出栈入栈具体就不多说了。在32位模式下,栈只能保留32位数据(并不是栈的大小只有32位,而是数据长度为4字节),esp总是能被4整除。

练习10: To become familiar with the C calling conventions on the x86, find the address of the test_backtrace function in obj/kern/kernel.asm, set a breakpoint there, and examine what happens each time it gets called after the kernel starts. How many 32-bit words does each recursive nesting level of test_backtrace push on the stack, and what are those words?

这个练习要求我们熟悉一下栈。打开obj/kern/kernel.asm,主要分析一下test_backtrace(),找到函数的调用入口:

补全kern/monitor.c的代码如下:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
        // Your code here.
    uint32_t *ebp = (uint32_t *)read_ebp();
    uint32_t eip = ebp[1];
    uint32_t arg1= ebp[2];
    uint32_t arg2 = ebp[3];
    uint32_t arg3 = ebp[4];
    uint32_t arg4 = ebp[5];
    uint32_t arg5 = ebp[6];
    cprintf("Stack backtrace:\n");
    while(ebp)
    {
        cprintf("ebp %08x eip %08x args %08x %08x %08x %08x %08x\n",ebp,eip,arg1,arg2,arg3,arg4,arg5);
        ebp = (uint32_t *)ebp[0];
        eip = ebp[1];
        arg1= ebp[2];
        arg2 = ebp[3];
        arg3 = ebp[4];
        arg4 = ebp[5];
        arg5 = ebp[6];
    }

        return 0;
}

猜你喜欢

转载自blog.csdn.net/lgq0409/article/details/85378972