Part 3: Kernel

这部分自然是读kernel并写一些代码。

Using virtual memory to work around position dependence

一般来说操作系统内核都会被链接并运行在最高的虚地址空间,将较低的虚地址空间留给用户程序使用。就像在之前的例子中VMA=0xf0100000,而LMA=0x00100000. 显然不存在0xf0100000对应的物理地址,因此实际上是将该虚拟地址映射到0x00100000对应的物理地址,即BIOS上方的物理地址。下一个lab将把从0x000000000x0fffffff的物理地址映射到从0xf00000000xffffffff虚拟地址(这是JOS只能用256MB物理内存的原因)。
现在则只需要映射最开始4MB的物理内存。这一映射目前还是手动完成的。当设置了CR0_PG标志位之后,enrty_pgdir将从0xf00000000xf0400000和从0x000000000x00400000的虚拟地址映射到从0x000000000x00400000的物理地址,任何其它虚拟地址访问会出现错误。
Exercise 7 要求使用QEMU和GDB研究一下JOS kernel. 在movl %eax, %cr0处设置断点并检查内存地址为0x001000000xf0100000的内存内容。接着用GDB单步调试,再次检查相同内存地址的内容。
回答以下问题:如果虚拟地址到物理地址映射不存在的话,映射指令之后的哪一条指令会出问题?

b *0x100025 #Set a breakpoint when executing movl %eax, %cr0
c #Continue execution until breakpoint
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
(gdb) stepi
=> 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

运行之后,页表已经加载进来了。高的虚拟地址已经映射到低的物理地址上去了。注释掉mov指令后,跳转指令jmp *%eax会崩溃。

Formatted Printing to the Console

这部分主要说kernel怎样实现所有的I/O操作。
首先要读一下 kern/printf.c, lib/printfmt.c, 和kern/console.c, 并做一下Exercise 8.
Exercise 8 要求补全代码,打印%o格式的八进制数(可以照搬十进制的部分)。

num = getuint(&ap, lflag);
base = 8;
goto number;

这部分还需要看代码回答以下几个问题:

  1. 解释printf.cconsole.c之间的接口。特别的,console.c提供了什么函数?它是如何被printf.c使用的?
    printf.c使用了console.c提供的cputchar()函数。由于cprintf()中参数长度是边长的,需要用一个参数列表va_list和格式字符串来决定参数格数。之后调用vcprintf()函数,这个函数根据格式字符串从va_list中读取参数,并使用cputchar()将参数打印出来。
  2. 解释console.c中的下列代码:
if (crt_pos >= CRT_SIZE) {int i;memmove(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;}

作用是当整个屏幕写满的时候,把第二行到最后一行的内容上移一行,把最后一行清空,把开始位置指向最后一行的开头。
3. 单步跟踪以下代码的执行:

int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);

回答以下两个问题

  • 调用cprintf()时fmt和ap分别指向什么?
    fmt指向格式字符串,ap指向x的地址。
  • 按照执行顺序依次列举对cons_putc, va_arg, vcprintf的调用。列举cons_putc调用时的参数,列举va_arg调用前后ap指向,列举vcprintf调用时两个参数的值。
    使用gdb,根据反汇编文件,在进入函数cons_putc, vcprintf, cprintf()的入口处设置断点,并使用bt (backtrace)命令查看函数调用信息。由于va_arg函数已经被内联优化,可以在进入vcprintf函数后为变量ap设置watchpoint. 运行结果如下:
Breakpoint 1, vcprintf (fmt=0xf0101aa0 "x %d, y %x, z %d\n", ap=0xf010ffc4 "\001") at kern/printf.c:18
Breakpoint 2, cons_putc (c=120) at kern/console.c:434
Breakpoint 2, cons_putc (c=32) at kern/console.c:434
Breakpoint 2, cons_putc (c=49) at kern/console.c:434
Breakpoint 2, cons_putc (c=44) at kern/console.c:434
Breakpoint 2, cons_putc (c=32) at kern/console.c:434
Breakpoint 2, cons_putc (c=121) at kern/console.c:434
Breakpoint 2, cons_putc (c=32) at kern/console.c:434
Breakpoint 2, cons_putc (c=51) at kern/console.c:434
Breakpoint 2, cons_putc (c=44) at kern/console.c:434
Breakpoint 2, cons_putc (c=32) at kern/console.c:434
Breakpoint 2, cons_putc (c=122) at kern/console.c:434
Breakpoint 2, cons_putc (c=32) at kern/console.c:434
Breakpoint 2, cons_putc (c=52) at kern/console.c:434
Breakpoint 2, cons_putc (c=10) at kern/console.c:434

输出中看不到ap的变化。虽然已经为ap设置了watchpoint,ap的变化应当是后面比前面加四,指向下一个地址,但是不知道内联会影响结果watch point的输出嘛?

  1. 运行以下代码:
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);

代码输出是什么?一步一步解释输出是如何得到的?如果x86是大端法,需要如何设置i来得到同样的输出?是否需要把57616改成一个不一样的值?
可以把这段代码插在i386_init()之中,然后重新make.可以看到输出是:

He110 World

同样使用gdb,运行结果如下:

Breakpoint 2, cprintf (fmt=0xf0101a97 "H%x Wo%s") at kern/printf.c:27
Breakpoint 3, vcprintf (fmt=0xf0101a97 "H%x Wo%s", ap=0xf010ffd4 <incomplete sequence \341>) at kern/printf.c:18
Breakpoint 4, cons_putc (c=72) at kern/console.c:434
Breakpoint 4, cons_putc (c=101) at kern/console.c:434
Breakpoint 4, cons_putc (c=49) at kern/console.c:434
Breakpoint 4, cons_putc (c=49) at kern/console.c:434
Breakpoint 4, cons_putc (c=48) at kern/console.c:434
Breakpoint 4, cons_putc (c=32) at kern/console.c:434
Breakpoint 4, cons_putc (c=87) at kern/console.c:434
Breakpoint 4, cons_putc (c=111) at kern/console.c:434
Breakpoint 4, cons_putc (c=114) at kern/console.c:434
Breakpoint 4, cons_putc (c=108) at kern/console.c:434
Breakpoint 4, cons_putc (c=100) at kern/console.c:434

大端法的话i应当改为0x726c6400,57616不需要改动,因为它就是一个数字,和大端法小端法无关。

  1. 下面代码中"y="之后会输出什么? (注意回答不是一个特定的值)这是如何发生的?
cprintf("x=%d y=%d", 3);

因为这个函数没有边界检查,默认是3这个数对应的地址后面一个位置的数。若3对应的地址为x,那么第二个输出指向的地址就是x+4。这是一个未定义行为,将输出x+4开始的四个字节的内容。
6. 如果GCC改变了它的调用例程,按照声明顺序将参数放在栈里,即最后一个参数最后压栈。那么要如何修改cprintf来让它接受边长数量的参数?
这样的话把参数倒过来定义,需要把格式字符串放在最后,参数列表也需要反过来写。

The Stack

这部分要求写一个kernel monitor function来打印栈的backtrace
Exercise 9 要求找到kernel初始化栈的位置以及栈在内存中的位置并回答问题:

  • kernel如何为栈保留空间?
  • 栈指针指向保留空间的那一端?

kern/entry.S中为kernel初始化了位置:

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

根据反汇编文件中mov $0xf0110000,%esp确定起始位置为0xf0110000.
kernel在.data段给栈留了KSTKSIZE大小的空间为8*page_size.
栈指针指向栈顶,栈向低地址延申。
Exercise 10 要求x86上C语言的调用例程。找到test_backtrace函数的起始地址,在那里设置断点,并检查kernel启动之后该函数每次被调用时发生的事情。回答每次test_backtrace每一次递归嵌套向栈里压了多少个32位的word以及这些word都是什么?
首先根据反汇编文件确定test_backtrace函数的入口地址为0xf0100040,在这个位置设置断点,根据gdb信息:

Breakpoint 1, test_backtrace (x=5) at kern/init.c:13
13      {(gdb) i r
eax            0x0      0
ecx            0x3d4    980
edx            0x3d5    981
ebx            0xf0111308       -267316472
esp            0xf010ffcc       0xf010ffcc
ebp            0xf010fff8       0xf010fff8
esi            0x10094  65684
edi            0x0      0
eip            0xf0100040       0xf0100040 <test_backtrace>
eflags         0x46     [ PF ZF ]
cs             0x8      8
ss             0x10     16
ds             0x10     16
es             0x10     16
fs             0x10     16
gs             0x10     16
(gdb) c
Continuing.
=> 0xf0100040 <test_backtrace>: push   %ebpBreakpoint 1, test_backtrace (x=4) at kern/init.c:13
13      {(gdb) i r
eax            0x4      4
ecx            0x3d4    980
edx            0x3d5    981
ebx            0xf0111308       -267316472
esp            0xf010ffac       0xf010ffac
ebp            0xf010ffc8       0xf010ffc8
esi            0x5      5
edi            0x0      0
eip            0xf0100040       0xf0100040 <test_backtrace>
eflags         0x96     [ PF AF SF ]
cs             0x8      8
ss             0x10     16
ds             0x10     16
es             0x10     16
fs             0x10     16
gs             0x10     16

可知压入了8个32位的word。打印以下栈顶的这8个word:

(gdb) x/8x 0xf010ffac
0xf010ffac:     0xf01000a1      0x00000004      0x00000005      0xf010fff8
0xf010ffbc:     0xf010004a      0xf0111308      0x00010094      0xf010fff8

经过一番汇编语言的比对,可以确定这8个word自顶向下一次是:

%ebp
%esi
%ebx
Return address of __x86.get_pc_thunk.bx
%esi
%eax
value of x
Return address of test_backtrace(x-1)

Exercise 11 要求实现把backtrace function. 实现之前首先要明白这个调用链是怎样的。当前函数栈中自顶向上依次为如下内容:

     ...argumentargumentreturn address of current function
%esp---->base pointer of previous function (also previous %ebp)

此时%ebp中存放的是此时%esp的值。
如果发生了对下一个函数的调用,那么下一个函数在入口处会将刚才的%ebp寄存器中的值压栈,然后在%ebp寄存器中记录当前%esp寄存器的值,仍然保持刚才所说的栈的结构。于是我们可以通过对base pointer像列表一样不断地访问来找到每个函数入口处栈指针所指的位置,函数的返回地址,以及参数内容。
在了解了%ebp的调用逻辑之后,完善backtrace函数就非常简单了(注意在打印的时候需要使用%08x补齐高位的0),代码如下:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{uint32_t* ebp = (uint32_t*)read_ebp();cprintf("Stack backtrace:\n");while (ebp){cprintf("  ebp %08x  eip %08x  args %08x %08x %08x %08x %08x\n", ebp, *(ebp+1),*(ebp+2),*(ebp+3),*(ebp+4),*(ebp+5),*(ebp+6));ebp = (uint32_t*)(*ebp);}return 0;
}

Exercise 12要求补全函数debuginfo_eip(). 这个函数在符号表中查询%eip并返回关于那个位置的debugging信息。具体而言就是插入对stab_binsearch的调用来找到对应地址的行数,在monitor中添加新的backtrace功能,扩展之前的mon_backtrace函数,调用debuginfo_eip函数并按照给定格式打印栈帧信息。
首先修改commands数组内容。

static struct Command commands[] = {{ "help", "Display this list of commands", mon_help },{ "kerninfo", "Display information about the kernel", mon_kerninfo },{ "backtrace","Display stack backtrace information", mon_backtrace},
};

接着补全函数debuginfo_eip()

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

最后修改mon_backtrace函数:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{struct Eipdebuginfo info;uint32_t* ebp = (uint32_t*)read_ebp();cprintf("Stack backtrace:\n");while (ebp){cprintf("  ebp %08x  eip %08x  args %08x %08x %08x %08x %08x\n", ebp, *(ebp+1),*(ebp+2),*(ebp+3),*(ebp+4),*(ebp+5),*(ebp+6));if(debuginfo_eip(*(ebp+1), &info)==0){cprintf("         %s:%d: ",info.eip_file,info.eip_line);cprintf("%.*s", info.eip_fn_namelen, info.eip_fn_name); //eip_fn_name may end without zero so it's necessary to specify the maximal length to be printed out.cprintf("+%d\n",((*(ebp+1))-(uint32_t)info.eip_fn_addr));}ebp = (uint32_t*)(*ebp);}return 0;
}

这样我们的lab就做完了。
最后还有三个坑要补:

  1. 如何在内联优化之下定位某个被优化的变量
    2. 最后一个exercise讲了什么故事
    3. lab1的challenge

先回答以下问题2.
首先它讲了一个关于stabs的故事。 stabs取名于symbol table strings. GNU C 编译器把c源代码编译成汇编语言.s文件, .s文件然后被翻译成.o文件,.o文件被链接成可执行文件。如果编译的时候加了-g 参数 , gcc 会添加额外的调试信息到.s文件中。 这些调试信息最终被带到可执行文件中,最为ELF文件中.stab节存在

/* Include debugging information in kernel memory */.stab : {PROVIDE(__STAB_BEGIN__ = .);*(.stab);PROVIDE(__STAB_END__ = .);BYTE(0)        /* Force the linker to allocate spacefor this section */}

kernel.ld文件中的这段代码让链接器为.stab节分配了内存空间,然后将.stab节一并加载到内核内存之中。运行以下命令可以看到预留了这一节的空间。

objdump -h obj/kern/kernelobj/kern/kernel:     file format elf32-i386Sections:
Idx Name          Size      VMA       LMA       File off  Algn0 .text         00001b59  f0100000  00100000  00001000  2**4CONTENTS, ALLOC, LOAD, READONLY, CODE1 .rodata       00000754  f0101b60  00101b60  00002b60  2**5CONTENTS, ALLOC, LOAD, READONLY, DATA2 .stab         00003cfd  f01022b4  001022b4  000032b4  2**2CONTENTS, ALLOC, LOAD, READONLY, DATA3 .stabstr      0000197e  f0105fb1  00105fb1  00006fb1  2**0CONTENTS, ALLOC, LOAD, READONLY, DATA4 .data         00009300  f0108000  00108000  00009000  2**12CONTENTS, ALLOC, LOAD, DATA5 .got          00000008  f0111300  00111300  00012300  2**2CONTENTS, ALLOC, LOAD, DATA6 .got.plt      0000000c  f0111308  00111308  00012308  2**2CONTENTS, ALLOC, LOAD, DATA7 .data.rel.local 00001000  f0112000  00112000  00013000  2**12CONTENTS, ALLOC, LOAD, DATA8 .data.rel.ro.local 00000060  f0113000  00113000  00014000  2**5CONTENTS, ALLOC, LOAD, DATA9 .bss          00000648  f0113060  00113060  00014060  2**5CONTENTS, ALLOC, LOAD, DATA10 .comment      00000029  00000000  00000000  000146a8  2**0CONTENTS, READONLY

汇编器把stabs信息添加到目标文件的符号表和字串表中。链接器再把所有目标文件中的符号表和字串表合并成一个。调试器会最终使用可执行文件中的这两个表最为调试信息的来源。
可以在inc/stab.h文件中查看stab结构体的定义:

// Entries in the STABS table are formatted as follows.
struct Stab {uint32_t n_strx;   // index into string table of nameuint8_t n_type;         // type of symboluint8_t n_other;        // misc info (usually empty)uint16_t n_desc;        // description fielduintptr_t n_value;   // value of symbol
};

运行以下命令可以看到kernel中.stab的文件内容:

objdump -G obj/kern/kernelobj/kern/kernel:     file format elf32-i386Contents of .stab section:Symnum n_type n_othr n_desc n_value  n_strx String-1     HdrSym 0      1300   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
6      SLINE  0      61     f0100020 0
7      SLINE  0      62     f0100025 0
8      SLINE  0      67     f0100028 0
9      SLINE  0      68     f010002d 0
10     SLINE  0      74     f010002f 0
11     SLINE  0      77     f0100034 0
12     SLINE  0      80     f0100039 0
13     SLINE  0      83     f010003e 0
14     SO     0      2      f0100040 31     kern/entrypgdir.c
15     OPT    0      0      00000000 49     gcc2_compiled.
...

debuginfo_eip()函数实现的就是在.stab节中z找包含%eip的源文件,再在源文件中找到包含%eip的函数,再在函数中找到包含%eip的行,将相应信息记录在Eipdebuginfo结构体之中。到此这个故事差不多讲完了。

再来看一下lab1的challenge:
Challenge要求实现彩色打印。
首先找到console.c中的控制color的部分

// if no attribute given, then use black on whiteif (!(c & ~0xFF))c |= 0x0700;

可以猜测变量c低八位以上的部分都是控制颜色的部分。于是我们可以在inc/color_printer.h新添加一个控制颜色的全局变量COLOR:

#include <inc/types.h>
uint32_t COLOR;

修改刚刚console.c中的部分代码:

// if COLOR is not defined, then use black on whiteif(!COLOR) COLOR = 0x0700;if (!(c & ~0xFF))c |= COLOR;  //else use COLOR as CGA output color.

可以使用%C控制颜色:

switch (ch = *(unsigned char *) fmt++) {//flags to control the output colorcase 'C':num = getint(&ap, lflag);COLOR = (uint32_t)num;break;...

同时记得在一次输出结束之后将COLOR重置:

while (1) {while ((ch = *(unsigned char *) fmt++) != '%') {if (ch == '\0'){COLOR = 0x0700; //reset the COLOR as black on white.return;}putch(ch, putdat);}...

如此就实现了彩色打印,challenge完结撒花。

问题1暂时还没有想到什么好的方法,只能通过读反汇编文件,确定内联之后的地址,然后在该地址处设置breakpoint实现。可以以后在lab讨论课上进一步深入研究。

JOS lab1 booting a PC part 3相关推荐

  1. 《MIT JOS Lab1: Booting a PC》实验报告

    目录 1  主要阅读汇编语言资料. 2 使用GDB命令跟踪BIOS做了哪些事情 2.1 先做好准备工作 2.1.1 下载好练习JOS系统 2.1.2 下载好QEMU模拟器并编译(如已经编译过可以忽略此 ...

  2. Lab 1: Booting a PC

    这个实验是基于MIT的2017的6.826课程,搭建环境的时候踩了几个坑,但是当时没有记录下来,可惜~ Part 1: PC Bootstrap 介绍了如何安装qemu以及如同通过qemu来模拟操作系 ...

  3. MIT6.828——LAB1:Booting a PC

    MIT6.828--LAB1:Booting a PC Part1:PC Bootstrap 练习1: 熟悉X86汇编语言 The PC's Physical Address Space 电脑的物理地 ...

  4. MIT 操作系统实验 MIT JOS lab1

    JOS lab1 首先向MIT还有K&R致敬! 没有很好的开源环境我不可能拿到这么好的东西. 向每一个与我一起交流讨论的programmer致谢!没有道友一起死磕,我也可能会中途放弃. 跟丫死 ...

  5. 2022-2-16 MIT 6.828 Lab1:Booting a PC part1-part2

    Part 1: PC Bootstrap Getting Started with x86 assembly Exercise 1. Familiarize yourself with the ass ...

  6. 2022-2-27 MIT 6.828 Lab 1: Booting a PC | Part 3: The Kernel | The Stack |exercise 9 - 11

    Exercise 9. Determine where the kernel initializes its stack, and exactly where in memory its stack ...

  7. MIT JOS 6.828 Lab1学习笔记

    官网:https://pdos.csail.mit.edu/6.828/2018/schedule.html 参考资料: https://blog.csdn.net/bysui/category_62 ...

  8. Xv6 Lab1手记

    环境: Ubuntu_LTS18.04 _64位 课程:https://pdos.csail.mit.edu/6.828/2018/schedule.html  (2018年秋季) 我跟着官方文件来的 ...

  9. 计算机科班学习 OS 的正确姿势!

    来源 | 编程指北 操作系统发展到今天,已经非常复杂了,像 Windows.Linux 任意一个都是几千万行代码级别,想靠个人完全搞懂,几乎是不可能的了. 所以需要先明确一下我们学习的目的,不同的学习 ...

最新文章

  1. android程序贴吧,【Android 教程总结贴】归纳所有android贴
  2. myeclipse:web项目不能显示Web App Libraries
  3. stm32F103的systick时间不准终于找到原因了
  4. (转)输入pipt提示:AttributeError: 'module' object has no attribute 'HTTPSConnection'
  5. Jacobian 和 Hessian 矩阵
  6. 2012.7.24---C#(2)
  7. AcWing之重建二叉树
  8. Mysql8.0 的sql修改成 Mysql7.X的sql
  9. 数列分块入门 1(LibreOj-6277)
  10. 2021-10-25
  11. AttributeError: module ‘hanlp.utils.rules‘ has no attribute ‘tokenize_english‘
  12. 听说最近知识变现,测一测程序员的知识广度?
  13. XSell和Xftp的简单使用方法
  14. 微软认证一览表(附图)
  15. Redis-master节点宕机后的处理方式
  16. python的标准库turtle_Python标准库使用之使用turtle绘制奥林匹克五环
  17. 工业数字化转型 — 工业自动化和控制系统
  18. Excel自动化办公(一) | 满足你对Excel数据的所有幻想,python-office一键生成模拟数据
  19. 支持通话/音量加减/接听功能TypeC线控耳机方案开发
  20. php fpm apache nginx_nginx/apache+php-fpm环境

热门文章

  1. linux 休眠定时唤醒_Linux 下定时唤醒计算机
  2. 《斜杠青年:如何开启你的多重身份》读书笔记
  3. Linux挂载nfts分区
  4. 购买虚拟服务器费用是办公费吗,T+费用单能做报销之类的么?是不是要自己添加例如办公费,差旅费什么的。老板想要在APP上查看库存,销售情况是不是要购买订阅报销,订阅报销怎么收费?...
  5. 洛谷:P1536 村村通
  6. 【网络爬虫】爬取神奇宝贝Pokemon图鉴图片大全
  7. 为什么北欧的顶级程序员数量远超中国?
  8. js-根据时间戳计算发布时间
  9. python截取浏览器图片
  10. 代码覆盖率之 sonar