JOS lab1 booting a PC part 3

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   %ebp

Breakpoint 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. 实现之前首先要明白这个调用链是怎样的。当前函数栈中自顶向上依次为如下内容:

		...
		argument
		argument
		return 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 space
				   for this section */
	}

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

objdump -h obj/kern/kernel

obj/kern/kernel:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00001b59  f0100000  00100000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       00000754  f0101b60  00101b60  00002b60  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00003cfd  f01022b4  001022b4  000032b4  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .stabstr      0000197e  f0105fb1  00105fb1  00006fb1  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         00009300  f0108000  00108000  00009000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .got          00000008  f0111300  00111300  00012300  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  6 .got.plt      0000000c  f0111308  00111308  00012308  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  7 .data.rel.local 00001000  f0112000  00112000  00013000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  8 .data.rel.ro.local 00000060  f0113000  00113000  00014000  2**5
                  CONTENTS, ALLOC, LOAD, DATA
  9 .bss          00000648  f0113060  00113060  00014060  2**5
                  CONTENTS, ALLOC, LOAD, DATA
 10 .comment      00000029  00000000  00000000  000146a8  2**0
                  CONTENTS, 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 name
	uint8_t n_type;         // type of symbol
	uint8_t n_other;        // misc info (usually empty)
	uint16_t n_desc;        // description field
	uintptr_t n_value;	// value of symbol
};

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

objdump -G obj/kern/kernel

obj/kern/kernel:     file format elf32-i386

Contents 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 white
	if (!(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 white
	if(!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 color
		case '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讨论课上进一步深入研究。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值