MIT-JOS系列3:启动内核

使用虚拟内存

为什么要使用虚拟内存?可以参考这篇博客:虚拟内存与物理内存的联系与区别

简单的说可以总结为:

  1. 使比实际物理内存更大的程序能够运行
  2. 使每个进程拥有独立的虚拟地址空间,相互之间不能修改数据
  3. 访问虚拟地址时,如果虚拟地址所对应的物理地址不在物理内存中,则产生缺页中断,此时才真正分配物理地址,同时更新进程的页表

虚拟地址如何寻址?可以参考我之前博客中的保护模式寻址:MIT-JOS系列1:实模式和保护模式下的段寻址方式


操作系统内核通常喜欢将其链接地址设置在非常高的虚拟地址处(关于链接地址,可参考MIT-JOS系列2:bool loader过程 中的“MIT-JOS系列2:bool loader过程”一节),以便将留下虚拟地址的低地址部分给用户程序使用。例如MIT-JOS的试验中就将内核载入到0xf0100000处运行。

但是许多机器在0xf0100000没有物理内存,所以我们必须建立映射表,利用处理器的内存管理硬件完成虚拟地址到物理地址的映射。在boot loader结束、kernel载入完成刚开始执行的时候,此时并没有完成页表目录和页表的建立,也没有开启分页选项 CR0_PG ,因此在kernel执行的一开始 kern/entry.S 中,我们首先使用`kern / entrypgdir.c 中的 entry_pgtable 对前4MB的物理内存(0xf0000000 ~ 0xf0400000)的页表目录和页表进行静态初始化 ,然后设置 CR0_PG 标志

  • 在设置 CR0_PG 标志之前,即在boot loader过程中,我们使用的都是物理地址(严格来说是线性地址,但由于 boot.S 中没有开启分页选项,此时线性地址等同于物理地址)
  • 一旦设置 CR0_PG 标志,对内存的引用由虚拟地址通过虚拟内存硬件转换为物理地址,对虚拟地址0xf0000000 ~ 0xf0400000和0x00000000 ~ 0x00400000的访问被映射到物理地址0x00000000 ~ 0x00400000。若访问的虚拟地址不在这两个范围中将导致硬件异常, 由于此时还未设置中断处理,程序将异常退出(或进入 spin 节无限循环)


此处通过MIT-JOS lab1的 Exercise 7 加深对其理解:

Exercise 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.

首先在 movl %eax, %cr0 处设置断点,使程序执行到这个位置,分别查看 0x00100000 和 0xf0100000 处内存的值:

(gdb) b *0x100025
Breakpoint 1 at 0x100025
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x100025:    mov    %eax,%cr0

Breakpoint 1, 0x00100025 in ?? ()
(gdb) x/8x 0x00100000
0x100000:   0x1badb002  0x00000000  0xe4524ffe  0x7205c766
0x100010:   0x34000004  0x2000b812  0x220f0011  0xc0200fd8
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x00000000  0x00000000  0x00000000  0x00000000
0xf0100010 <entry+4>:   0x00000000  0x00000000  0x00000000  0x00000000

由于还未设置 CR0_PG ,因此此时内存 0xf0100000 处并没有值,内核被加载到 0x100000 处,此处有值。

继续向下执行,再次打印两者的值:

(gdb) si
=> 0x100028:    mov    $0xf010002f,%eax
0x00100028 in ?? ()
(gdb) x/8x 0x00100000
0x100000:   0x1badb002  0x00000000  0xe4524ffe  0x7205c766
0x100010:   0x34000004  0x2000b812  0x220f0011  0xc0200fd8
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x1badb002  0x00000000  0xe4524ffe  0x7205c766
0xf0100010 <entry+4>:   0x34000004  0x2000b812  0x220f0011  0xc0200fd8

在这里就能发现设置了 CR0_PG 后 0x00100000 与 0xf0100000 内存段的值相同,因为映射已经被建立,访问 0xf0100000 与访问 0x00100000 等同。

如果注解掉 movl %eax, %cr0 ,再次编译执行,将会得到错误:

qemu-system-i386: Trying to execute code outside RAM or ROM at 0xf010002c
This usually means one of the following happened:

(1) You told QEMU to execute a kernel for the wrong machine type, and it crashed on startup (eg trying to run a raspberry pi kernel on a versatilepb QEMU machine)
(2) You didn't give QEMU a kernel or BIOS filename at all, and QEMU executed a ROM full of no-op instructions until it fell off the end
(3) Your guest kernel has a bug and crashed by jumping off into nowhere

因为后续执行内核时企图访问内存 0xf0100000 开始的空间,但由于分页模式并未开启,将直接访问地址为 0xf0100000 的物理空间,而这段地址的值为0,不存在内核,因此执行出错


在内核进行虚拟内存映射和设置分页后,它初始化自己的栈空间,初始化方法如下:

relocated:

    # Clear the frame pointer register (EBP)
    # so that once we get into debugging C code,
    # stack backtraces will be terminated properly.
    movl    $0x0,%ebp           # nuke frame pointer

    # Set the stack pointer
    movl    $(bootstacktop),%esp

    # now to C code
    call    i386_init

    # Should never get here, but in case we do, just spin.
spin:   jmp spin


.data
###################################################################
# boot stack
###################################################################
    .p2align    PGSHIFT     # force page alignment
    .globl      bootstack
bootstack:
    .space      KSTKSIZE
    .globl      bootstacktop   
bootstacktop:

这里定义了全局变量 bootstack 作为临时堆栈,它有KSTKSIZE个字节(定义为32K)的区域作为栈空间; bootstacktop 指向栈空间后的第一个字节,由于初始栈为空,因此栈顶为 bootstacktop 指向的位置。栈由高地址向低地址生长,栈底地址最高,栈顶地址最低

栈的功能和使用

在C函数调用时,通过栈保存和恢复现场

栈相关的关键寄存器:

  • esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。push、pop指令会自动调整esp的值
  • ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部
  • eip:存储当前执行指令的下一条指令在内存中的偏移地址

C函数调用过程:

进入函数之前

  1. 将需要的参数压栈
  2. eip压栈(下一条指令在内存中的位置)

进入调用函数(函数内代码执行之前)

  1. ebp压栈
  2. 当前esp赋值给ebp(此时当前ebp指向刚刚压栈的ebp)

此时栈内空间示意如下图:

利用栈和STAB打印递归函数过程中的信息

STAB

首先先了解一下STAB

调试信息的传统格式被称为 STAB(符号表)。STAB 信息保存在 ELF 文件的 .stab.stabstr 部分。

  • .stab节:符号表部分,这一部分的功能是程序报错时可以提供错误信息,具体的在往后的博客中介绍
  • .stabstr节:符号表字符串部分,具体的也会在往后的博客介绍

通过指令 objdump -G obj/kern/kernel 可以看到kernel的.stab节的内容,例如:

qxy@qxy-XPS-13-9360:~/1work/MIT-JOS/lab$ objdump -G obj/kern/kernel

obj/kern/kernel:     文件格式 elf32-i386

.stab 节的内容:

Symnum n_type n_othr n_desc n_value  n_strx String

-1     HdrSym 0      1299   0000197d 1     
0      SO     0      0      f0100000 1      {standard input}
1      SOL    0      0      f010000c 18     kern/entry.S
2      SLINE  0      44     f010000c 0      
3      SLINE  0      57     f0100015 0      
4      SLINE  0      58     f010001a 0      
5      SLINE  0      60     f010001d 0      
......

MIT-JOS Exercise 12:

mon_backtrace 的代码如下:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
    // Your code here.
    uint32_t *ebp;
    // read得到的是ebp寄存器的值,这个值应该作为地址使用,因此进行转换
    ebp = (uint32_t *)read_ebp();
    cprintf("Stack backtrace:\n");
    struct Eipdebuginfo info;
    // when there is nothing in stack, the first value of ebp is 0
    while (ebp != 0) {
        // 第一行的当前ebp和栈环境是mon_backtrace的
        cprintf("ebp %8x eip %8x args %08x %08x %08x %08x %08x ", 
                ebp, ebp[1], ebp[2], ebp[3], ebp[4], ebp[5], ebp[6]);
        
        debuginfo_eip(ebp[1], &info);
        cprintf("%s:%d: %.*s+%d\n", info.eip_file, info.eip_line, info.eip_fn_namelen, 
                info.eip_fn_name, ebp[1]-info.eip_fn_addr);

        ebp = (uint32_t *)ebp[0];
    }
    return 0;
}

debuginfo_eip 增加的的代码如下:

stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline <= rline) {
    info->eip_line = stabs[lline].n_desc;
} else {
    return -1;
}

这里有个比较巧妙的用法:printf("%.*s", length, string); 用来打印string字符串最多前length个字符

running JOS: (1.1s) 
  printf: OK 
  backtrace count: OK 
  backtrace arguments: OK 
  backtrace symbols: OK 
  backtrace lines: OK 
Score: 50/50


设置BSS段

进入i386_init后,程序首先利用memset(edata, 0, end - edata)将未初始化的BSS节的内存设置为0

edata表示bss节在内存中开始的位置,end表示内核可执行程序在内存中结束的位置。从上一篇博客MIT-JOS系列2:bool loader过程对elf文件的介绍和节头表打印 objdump -h obj/kern/kernel 中可以看到,由于comment节不被载入内存,因此bss节是文件在内存中的最后一部分,因此edata和end之间的部分是bss节的长度。


其他初始化工作

在初始化bss节后,调用 cons_init 函数对系统进行一系列的初始化,包括显存初始化、键盘初始化等,此处不进行详细介绍

猜你喜欢

转载自www.cnblogs.com/sssaltyfish/p/10672780.html