博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
MIT-JOS系列3:启动内核
阅读量:5102 次
发布时间:2019-06-13

本文共 7142 字,大约阅读时间需要 23 分钟。

使用虚拟内存

为什么要使用虚拟内存?可以参考这篇博客:

简单的说可以总结为:

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

虚拟地址如何寻址?可以参考我之前博客中的保护模式寻址:

操作系统内核通常喜欢将其链接地址设置在非常高的虚拟地址处(关于链接地址, 中的“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 *0x100025Breakpoint 1 at 0x100025(gdb) cContinuing.The target architecture is assumed to be i386=> 0x100025:    mov    %eax,%cr0Breakpoint 1, 0x00100025 in ?? ()(gdb) x/8x 0x001000000x100000:   0x1badb002  0x00000000  0xe4524ffe  0x7205c7660x100010:   0x34000004  0x2000b812  0x220f0011  0xc0200fd8(gdb) x/8x 0xf01000000xf0100000 <_start+4026531828>: 0x00000000  0x00000000  0x00000000  0x000000000xf0100010 
: 0x00000000 0x00000000 0x00000000 0x00000000

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

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

(gdb) si=> 0x100028:    mov    $0xf010002f,%eax0x00100028 in ?? ()(gdb) x/8x 0x001000000x100000:   0x1badb002  0x00000000  0xe4524ffe  0x7205c7660x100010:   0x34000004  0x2000b812  0x220f0011  0xc0200fd8(gdb) x/8x 0xf01000000xf0100000 <_start+4026531828>: 0x1badb002  0x00000000  0xe4524ffe  0x7205c7660xf0100010 
: 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 0xf010002cThis 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,不存在内核,因此执行出错

总的来说,kern/entry.S做的事儿如下:

  • 载入简易页表到cr3,映射虚拟地址0xf0000000-0xf0400000和0x00000000-0x00400000到物理地址0x00000000-0x00400000
  • 修改cr0开启分页模式
  • 初始化内核栈
  • 跳转进入c函数i386_init进行其他初始化

Q:载入简易页表并开启分页模式后,寻址的过程发生了什么变化?为什么GDTR中一开始存放的物理地址在这个过程中没有改变,却还是能找到正确的GDT表进行地址变换?

A:之前在boot阶段开启了保护模式并载入GDT表到GDTR。由于载入是在保护模式开启之前,即仍处于实地址模式,因此载入到GDTR中的地址是GDT表的物理地址,保护模式开启后被视为虚拟地址,但此时还没开启分页模式,因此虚拟地址等同于物理地址。在kern/entry.S中开启分页模式后,对所有地址都认为是虚拟地址,并通过页表转换得到真实的物理地址。简易页表同时建立了虚拟内存0x00000000-0x00400000到物理地址0x00000000-0x00400000的映射,因此GDTR中虽然存放的是物理地址一直没有变过,但在分段、分页后仍然可以作为虚拟地址找到正确的物理地址

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

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      bootstackbootstack:    .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)

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

1550130-20190408192222447-1676978195.png

利用栈和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/kernelobj/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.S2      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 的代码如下:

intmon_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表示内核可执行程序在内存中结束的位置。从上一篇博客对elf文件的介绍和节头表打印 objdump -h obj/kern/kernel 中可以看到,由于comment节不被载入内存,因此bss节是文件在内存中的最后一部分,因此edata和end之间的部分是bss节的长度。

其他初始化工作

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

转载于:https://www.cnblogs.com/sssaltyfish/p/10672780.html

你可能感兴趣的文章
一些方便系统诊断的bash函数
查看>>
【转载】基于vw等viewport视区相对单位的响应式排版和布局
查看>>
<转>关于MFC的多线程类 CSemaphore,CMutex,CCriticalSection,CEvent
查看>>
jquery中ajax返回值无法传递到上层函数
查看>>
css3之transform-origin
查看>>
[转]JavaScript快速检测浏览器对CSS3特性的支持
查看>>
Master选举原理
查看>>
[ JAVA编程 ] double类型计算精度丢失问题及解决方法
查看>>
小别离
查看>>
微信小程序-发起 HTTPS 请求
查看>>
WPF动画设置1(转)
查看>>
backgound-attachment属性学习
查看>>
个人作业——关于K米的产品案例分析
查看>>
基于node/mongo的App Docker化测试环境搭建
查看>>
java web 中base64传输的坑
查看>>
java 中的线程(一)
查看>>
秒杀9种排序算法(JavaScript版)
查看>>
素数判断BFS之“Prime Path”
查看>>
Activiti入门 -- 环境搭建和核心API简介
查看>>
struts.convention.classes.reload配置为true,tomcat启动报错
查看>>