盒子
盒子
文章目录
  1. The kernel
  2. Exercise 7.
  3. Exercise 8.
  4. The Stack
    1. Exercise 9.
    2. Exercise 10.
    3. Exercise 11.
    4. Exercise 12.

Mit6.828 Lab1: Part3 Jos Bootstrap -- the kernel

现在,我们将映射前4MB的物理内存,这足以让我们开始运行。我们使用kern / entrypgdir.c中的手写,静态初始化的页面目录和页面表来执行此操作。现在,您不需要了解这个工作的细节,只是它实现的效果。直到kern / entry.S设置CR0_PG标志,内存引用被视为物理地址(严格来说,它们是线性地址,但是boot / boot.S设置从线性地址到物理地址的身份映射,我们永远不会改变)。一旦设置了CR0_PG,内存引用是由虚拟内存硬件转换为物理地址的虚拟地址。 entry_pgdir将0xf0000000至0xf0400000范围内的虚拟地址转换为物理地址0x00000000至0x00400000,以及将虚拟地址0x00000000至0x00400000转换为物理地址0x00000000至0x00400000。任何不在这两个范围之一的虚拟地址将导致硬件异常,因为我们还没有设置中断处理,将导致QEMU转储机器状态并退出(或如果您不使用则会无休止地重新启动QEMU的6.828补丁版本)。

The kernel

Operating system kernels often like to be linked and run at very high virtual address, such as 0xf0100000, in order to leave the lower part of the processor’s virtual address space for user programs to use.

使用objdump -h obj/kern/kernel查看jos内核的link address位于0xf0100000,load address是实际的物理地址0x00100000。从kern/kernel.ld文件中也可以印证这一点。内核开始运行在高虚拟地址空间,主要是为了将低虚拟地址空间留给用户程序。内核开始运行于分段机制的保护模式,但是jos的分段机制”关闭“了,其效果是虚拟地址和物理地址是一样的,为了让内核运行在高虚拟地址空间将低虚拟地址空间留给用户程序,必然对内核的虚拟地址存在一种转换。

我们将使用处理器的内存管理硬件将内核虚拟地址0xf0100000映射到物理地址0x00100000,在内核运行之处,我们仅仅会映射内核最初的4MB的虚拟地址空间:[KERNBASE, KERNBASE+4MB)到物理内存空间[0, 4MB)中(KERNBASE=0xF0000000),因为4MB的内存是一个页表能够映射的内存空间的大小,并且这段虚拟地址空间足够让内核启动运行。完成内核虚拟地址空间最开始的映射在kern/entrypgdir.c中,我们设定了一个手写的静态初始化的页目录和页表:

1
2
3
4
5
6
7
pte_t entry_pgtable[NPTENTRIES];
pde_t entry_pgdir[NPDENTRIES] = {
[0]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
[KERNBASE>>PDXSHIFT]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};

pte_t是页表项类型,NPTENTRIES表示页表项的数目,entry_pgtable就是一个页表映射到物理页,接下来定义页目录entry_pgdir,页目录的索引由虚拟地址右移22位得到,即虚拟地址的高10位。因此只初始化了[0, 4MB)以及[KERNBASE, KERNBASE+4MB)虚拟地址空间的表项。这两段虚拟地址空间指向同一页表,起始物理地址在(uintptr_t)entry_pgtable - KERNBASE。所以[0, 4MB)中的虚拟地址与[KERNBASE, KERNBASE+4MB)中的虚拟地址访问的是同一物理页,但是[0, 4MB)中的虚拟地址访问的物理内存是不可写的,KERNBASE, KERNBASE+4MB)中的虚拟地址访问的物理内存可写。在entry.S中有几条指令的虚拟地址在[0, 4MB)虚拟地址空间内,这个虚拟地址空间的这些指令执行完之后,再也不会使用[0, 4MB)虚拟地址空间。

接下来的代码是手写的、静态初始化的页表项:

1
2
3
4
5
6
pte_t entry_pgtable[NPTENTRIES] = {
0x000000 | PTE_P | PTE_W,
0x001000 | PTE_P | PTE_W,
0x002000 | PTE_P | PTE_W,
...
0x3ff000 | PTE_P | PTE_W,

可见该页表映射到的页的范围:[0,4KB),[4KB,8KB),[8KB,12KB)…[,4MB),即将[0, 4MB)以及[KERNBASE, KERNBASE+4MB)虚拟地址空间映射到[0,4MB)的物理地址空间。所以这最初的内核虚拟地址转换为物理地址:PMA=VMA-KERNBASE。一旦在kern/entry.S中置位CR0寄存器的CR0_PG位,就可以使用这个页表了,虚拟地址的转换由虚拟内存硬件完成。这个页表会用到内核创建真正的页表为止。

从上面还可以看出内核的虚拟地址空间起始于KERNBASE=0xF0000000,这段虚拟地址空间映射到物理内存空间[0, 4MB),但是内核的link address:0xf0100000,load address:0x00100000。说明保留了1MB的物理地址空间,向后兼容。

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.

在exercise3中已经获知内核的起始地址为0x10000c,在0x10000c处设置断点,gdb运行到断点,然后一路si,直到发现命令0x100025: mov %eax,%cr0,查看内存0x00100000以及0xf0100000
turn off page
turn on page
由于此时分页映射并没有开启,此时内存引用采用的是线性地址,高地址0xf0100000是没有映射到任何任何物理地址。当分页开启之后,虚地址0xf0100000映射到物理地址0x00100000,所以二者的内容相同。
comment_cr0
如果注释掉movl %eax, %cr0,jmp要跳转到高位虚拟地址0xf010002c会出错,因为未开启分页,这个虚拟地址没有映射到实际的物理地址。

我们看下kern/entry.S的源代码:

1
2
3
4
5
6
7
entry:
movw $0x1234,0x472 # warm boot
movl $(RELOC(entry_pgdir)), %eax
movl %eax, %cr3
movl %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl %eax, %cr0

第一条movw语句的意义不明,表示warm boot,第2,3条语句表示将手写的,静态的页目录的地址存入%cr3寄存器。最后的三条语句即使能cr0寄存器的CR0_PE,CR0_PG,CR0_WP标识位,这三个标识位分别表示保护模式,分页,写保护,即打开分页。

1
2
mov $relocated, %eax
jmp *%eax

即使现在已经打开分页,kernel仍然运行于低虚拟地址下,需要使用jmp指令改变%EIP寄存器的内容。relocated标识符表示这内核高虚拟地址的起始,接下来执行relocated标识符处的指令:

1
2
3
4
relocated:
movl $0x0,%ebp # nuke frame pointer
movl $(bootstacktop),%esp
call i386_init

初始化内核初始栈帧为$0x0,这样内核栈回溯的: 时候,当遇到为$0x0的栈帧,就知道内核栈中的栈帧都弹出了,然后设置栈指针%esp,查看内核的反汇编代码obj/kern/kernel.asm

1
2
movl $(bootstacktop),%esp
f0100034: bc 00 00 11 f0 mov $0xf0110000,%esp

得知内核栈的初始栈顶为0xf0110000。最后调用i386_init函数执行内核的初始化工作。

Exercise 8.

We have omitted a small fragment of code - the code necessary to print octal numbers using patterns of the form “%o”. Find and fill in this code fragment.

将lib/printfmt.c中的void vprintfmt(…);函数中一处switch语句的case ‘o’下更改为:

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

  1. Explain the interface between printf.c and console.c. Specifically, what function does console.c export? How is this function used by printf.c?
    kern/printf.c中的putch函数调用了console.c中的cputchar函数。cputchar(int c)将整数c输出到控制台。
  2. Explain the following from console.c:
    1
    2
    3
    4
    5
    6
    7
    1 if (crt_pos >= CRT_SIZE) {
    2 int i;
    3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
    4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
    5 crt_buf[i] = 0x0700 | ' ';
    6 crt_pos -= CRT_COLS;
    7 }

当输出的字符即将超过屏幕的尺寸时,需要将显示缓冲crt_buf中偏移为一行的CRT_COLS长度为(CRT_SIZE - CRT_COLS) * sizeof(uint16_t)移到显示缓冲的首部,下面的for循环就是初始化新移入缓冲的CRT_SIZE - CRT_COLSCRT_SIZE的内容。这个就相当于在屏幕中的输出向上移出一行然后移入新的未输出的行,达到滚动的效果。

  1. For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC’s calling convention on the x86. Trace the execution of the following code step-by-step:
    1
    2
    int x = 1, y = 3, z = 4;
    cprintf("x %d, y %x, z %d\n", x, y, z);
  • In the call to cprintf(), to what does fmt point? To what does ap point?
    fmt指向格式化字符串,ap是va_list类型,指向参数列表未确定的部分。这个变量通过调用va_start来初始化,第一个参数是va_list变量的名字,第二个参数是省略号前最后一个有名字的参数。
    va_arg宏接收两个参数,va_list变量和参数列表中下一个参数的类型。当访问完毕最后一个可变参数之后,需要调用va_end
  • List (in order of execution) each call to cons_putc, va_arg, and vcprintf. For cons_putc, list its argument as well. For va_arg, list what ap points to before and after the call. For vcprintf list the values of its two arguments.
    • vcprintf(“x %d, y %x, z %d\n”,ap)
    • cons_putc(‘x’)
    • va_arg // ap调用之前指向x,y,z 3个整数,调用之后指向y,z两个整数
  1. Run the following code.
    1
    2
    unsigned int i = 0x00646c72;
    cprintf("H%x Wo%s", 57616, &i);

What is the output? Explain how this output is arrived at in the step-by-step manner of the previous exercise.
The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

将这两条语句放到kern/monitor.c文件的mon_backtrace函数中,输出如图所示:kernel start backtrace
57616对应的16进制是e110,如果格式符是%X,则会输出E110。多字节对象的存储中,最低有效字节存储在前面称为小端机,反之最高有效字节存储在前的称为大端机;Intel i386机器是小端机。0x72对应的ASCII码是’r’,0x6c对应的ASCII码是’l’,0x64对应的ASCII码是’d’。所以会输出“He110 World”。

  1. In the following code, what is going to be printed after ‘y=’? (note: the answer is not a specific value.) Why does this happen?
    1
    cprintf("x=%d y=%d", 3);

输出结果是x=3 y=-267321544
查看vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)函数源码,看起处理整数的方法:

1
2
3
4
5
6
7
8
9
// (signed) decimal
case 'd':
num = getint(&ap, lflag);
if ((long long) num < 0) {
putch('-', putdat);
num = -(long long) num;
}
base = 10;
goto number;

输出奇怪的原因就在于对数字的解析,即getint函数:

1
2
3
4
5
6
7
8
9
10
static long long
getint(va_list *ap, int lflag)
{
if (lflag >= 2)
return va_arg(*ap, long long);
else if (lflag)
return va_arg(*ap, long);
else
return va_arg(*ap, int);
}

lflag就是表示lld中有几个l,从而确定下个数字的类型是long long或者long或者int。所以cprintf("x=%d y=%d", 3);在正确输出x=3后解析到’y=%d时进入getint函数,执行return va_arg(ap, int);`,此时\ap中已经为空了,所有会输出一个未定义的值。

  1. Let’s say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change cprintf or its interface so that it would still be possible to pass it a variable number of arguments?

参见这个实现方法。

The Stack

In file kern/monitor.c, kernel monitor function that prints a backtrace of the stack: a list of the saved Instruction Pointer (IP) values from the nested call instructions that led to the current point of execution.

x86栈指针(%esp)是往低地址发展的,在32bit模式下,栈只能保存32bit的数值,所以%esp一直被4整除。基址指针寄存器(%ebp)将一直是栈指针在函数开始的位置,所以可以说是对栈指针的常量引用。

Exercise 9.

Determine where the kernel initializes its stack, and exactly where in memory its stack is located. How does the kernel reserve space for its stack? And at which “end” of this reserved area is the stack pointer initialized to point to?

栈指针初始化指向的是保存区域的高地址,设置断点在进入内核处,一路跟踪发现,栈的初始化发生在刚开启分页的时候,栈初始化地址:0xf0110000

1
2
3
4
5
6
7
8
9
10
11
(gdb) si
=> 0x10002d: jmp *%eax
0x0010002d in ?? ()
(gdb) si
=> 0xf010002f <relocated>: mov $0x0,%ebp
relocated () at kern/entry.S:74
74 movl $0x0,%ebp # nuke frame pointer
(gdb) si
=> 0xf0100034 <relocated+5>: mov $0xf0110000,%esp
relocated () at kern/entry.S:77
77 movl $(bootstacktop),%esp

Exercise 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的入口地址在0xf0100040,在此处设置断点,跳转此处执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
(gdb) b *0xf0100040
Breakpoint 2 at 0xf0100040: file kern/init.c, line 13.
(gdb) c
Continuing.
=> 0xf0100040 <test_backtrace>: push %ebp
Breakpoint 2, test_backtrace (x=5) at kern/init.c:13
13 {
(gdb) si
=> 0xf0100041 <test_backtrace+1>: mov %esp,%ebp
0xf0100041 13 {
(gdb) si
=> 0xf0100043 <test_backtrace+3>: push %ebx
0xf0100043 13 {
(gdb) si
=> 0xf0100044 <test_backtrace+4>: sub $0xc,%esp
0xf0100044 13 {
(gdb) ni
=> 0xf0100047 <test_backtrace+7>: mov 0x8(%ebp),%ebx
0xf0100047 13 {
(gdb) ni
=> 0xf010004a <test_backtrace+10>: push %ebx
14 cprintf("entering test_backtrace %d\n", x);
(gdb) ni
=> 0xf010004b <test_backtrace+11>: push $0xf01018a0
0xf010004b 14 cprintf("entering test_backtrace %d\n", x);
(gdb) ni
=> 0xf0100050 <test_backtrace+16>: call 0xf0100907 <cprintf>
0xf0100050 14 cprintf("entering test_backtrace %d\n", x);
(gdb) ni
=> 0xf0100055 <test_backtrace+21>: add $0x10,%esp
15 if (x > 0)
(gdb) ni
=> 0xf0100058 <test_backtrace+24>: test %ebx,%ebx
0xf0100058 15 if (x > 0)
(gdb) ni
=> 0xf010005a <test_backtrace+26>: jle 0xf010006d <test_backtrace+45>
0xf010005a 15 if (x > 0)
(gdb) ni
=> 0xf010005c <test_backtrace+28>: sub $0xc,%esp
16 test_backtrace(x-1);
(gdb) ni
=> 0xf010005f <test_backtrace+31>: lea -0x1(%ebx),%eax
0xf010005f 16 test_backtrace(x-1);
(gdb) ni
=> 0xf0100062 <test_backtrace+34>: push %eax
0xf0100062 16 test_backtrace(x-1);
(gdb) ni
=> 0xf0100063 <test_backtrace+35>: call 0xf0100040 <test_backtrace>
0xf0100063 16 test_backtrace(x-1);

如上所示,调用test_backtrace()最先执行的必然是push %ebpmov %esp,%ebp两条语句,然后压栈ebx寄存器,sub $0xc,%esptest_backtrace()在栈上保留3个32bit的局部变量的存储空间,分别用来存储%ebx,$0xf01018a0,%eax。看看kern/init.ctest_backtrace()的代码。

1
2
3
4
5
6
7
8
9
10
11
// Test the stack backtrace function (lab 1 only)
void
test_backtrace(int x)
{
cprintf("entering test_backtrace %d\n", x);
if (x > 0)
test_backtrace(x-1);
else
mon_backtrace(0, 0, 0);
cprintf("leaving test_backtrace %d\n", x);
}

可知第一次压栈实际是调用cprintf压栈的参数x,第二次压栈的$0xf01018a0实际上是字符串"entering test_backtrace %d\n"的地址,第三次压栈的%eax实际上是x-1

Exercise 11.

Implement the backtrace function as specified above. Use the same format as in the example, since otherwise the grading script will be confused.If you use read_ebp(), note that GCC may generate “optimized” code that calls read_ebp() before mon_backtrace()’s function prologue, which results in an incomplete stack trace (the stack frame of the most recent function call is missing). While we have tried to disable optimizations that cause this reordering, you may want to examine the assembly of mon_backtrace() and make sure the call to read_ebp() is happening after the function prologue.

可以先看inc/x86.h中的read_ebp()函数的定义

1
2
3
4
5
static inline uint32_t read_ebp(void) {
uint32_t ebp;
asm volatile("movl %%ebp,%0" : "=r" (ebp));
return ebp;
}

这种编码方式叫做内联汇编,内联汇编的基本语法模板如下所示:

1
2
3
4
5
6
asm [ volatile ] (
assembler template
[ : output operands ] /* optional */
[ : input operands ] /* optional */
[ : list of clobbered registers ] /* optional */
);

[ ]来表示其对应的内容为可选项,基本语法规则由5部分组成:

  • 关键字asm和volatile,asm为gcc关键字,表示接下来要嵌入汇编代码。为避免keyword asm与程序中其它部分产生命名冲突,gcc还支持asm关键字,与asm的作用等价, volatile为可选关键字,表示不需要gcc对下面的汇编代码做任何优化。
  • assembler template是要嵌入的汇编命令。
  • output operands,该字段为可选项,用以指明输出操作数。
  • input operands, 该字段为可选项,用以指明输入操作数。
  • list of clobbered registers,该字段为可选项,用于列出指令中涉及到的且没出现在output operands字段及input operands字段的那些寄存器。若寄存器被列入clobber-list,则等于是告诉gcc,这些寄存器可能会被内联汇编命令改写。因此,执行内联汇编的过程中,这些寄存器就不会被gcc分配给其它进程或命令使用。.

asm volatile("movl %%ebp,%0" : "=r" (ebp));的作用是将%bp的值返回给%0所引用的C语言变量ebp,根据”=r”约束可知具体的操作流程为:先将%eax值复制给任一通用寄存器,最终由该寄存器将值写入%0所代表的变量中。”r”约束指明gcc可以先将%ebp值存入任一可用的寄存器,然后由该寄存器负责更新内存变量。read_ebp就是将当前%ebp寄存器的内容以uint32_t格式返回。

Exercise 12.

Modify your stack backtrace function to display, for each eip, the function name, source file name, and line number corresponding to that eip.

In debuginfoeip, where do __STAB* come from? This question has a long answer; to help you to discover the answer, here are some things you might want to do:

  • look in the file kern/kernel.ld for _STAB*
  • run i386-jos-elf-objdump -h obj/kern/kernel
  • run i386-jos-elf-objdump -G obj/kern/kernel
  • run i386-jos-elf-gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s.
  • see if the bootloader loads the symbol table in memory as part of loading the kernel binary

Complete the implementation of debuginfo_eip by inserting the call to stab_binsearch to find the line number for an address.Add a backtrace command to the kernel monitor, and extend your implementation of mon_backtrace to call debuginfo_eip and print a line for each stack frame of the form:

1
2
3
4
5
6
7
8
9
K> backtrace
Stack backtrace:
ebp f010ff78 eip f01008ae args 00000001 f010ff8c 00000000 f0110580 00000000
kern/monitor.c:143: monitor+106
ebp f010ffd8 eip f0100193 args 00000000 00001aac 00000660 00000000 00000000
kern/init.c:49: i386_init+59
ebp f010fff8 eip f010003d args 00000000 00000000 0000ffff 10cf9a00 0000ffff
kern/entry.S:70: <unknown>+0
K>

Each line gives the file name and line within that file of the stack frame’s eip, followed by the name of the function and the offset of the eip from the first instruction of the function (e.g., monitor+106 means the return eip is 106 bytes past the beginning of monitor).

Be sure to print the file and function names on a separate line, to avoid confusing the grading script.

Tip: printf format strings provide an easy, albeit obscure, way to print non-null-terminated strings like those in STABS tables. printf(“%.*s”, length, string) prints at most length characters of string. Take a look at the printf man page to find out why this works.

在将backtrace作为shell的一个有效的命令,可以在kern/monitor.c文件中的结构数组static struct Command commands[]添加这样一个对象。

1
{ "backtrace", "Display stack backtrace", mon_backtrace }

kern/kdebug.h文件中查看指令指针的信息:

1
2
3
4
5
6
7
8
9
10
struct Eipdebuginfo {
const char *eip_file; // Source code filename for EIP
int eip_line; // Source code linenumber for EIP
const char *eip_fn_name; // Name of function containing EIP
// - Note: not null terminated!
int eip_fn_namelen; // Length of function name
uintptr_t eip_fn_addr; // Address of start of function
int eip_fn_narg; // Number of function arguments
};

为了让输入backtrace命令的时候得到需要的调试信息,需要在mon_backtrace中增加打印调试信息的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
uintptr_t ebp, eip;
struct Eipdebuginfo eipinfo;
for(ebp = read_ebp(); ebp != 0x0; ebp=*(uintptr_t *)ebp) {
eip = *((uintptr_t *)ebp+1);
debuginfo_eip(eip, &eipinfo);
cprintf("ebp %08x eip %08x args",ebp,eip);
for(int i = 0; i < 5; i++) cprintf(" %08x",*((uintptr_t *)ebp+i+2));
cprintf("\n");
cprintf(" %s:%d: %.*s+%d\n",
eipinfo.eip_file,eipinfo.eip_line,eipinfo.eip_fn_namelen,
eipinfo.eip_fn_name,eipinfo.eip_fn_addr);
}
return 0;
}

目前并没有详细了解链接过程中涉及的调试信息段,所以stab_binsearch的实现方法是参考其他同学的实现,在debuginfo_eip中添加stab_binsearch以搜索行号:

1
2
3
4
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline > rline)
return -1;
info->eip_line = stabs[lline].n_desc;

支持一下
扫一扫,支持buwei