选择了唯一的Challenge。
Exercise 2
BIOS在初始化%ss与%esp寄存器,并通过一系列in/out指令设置输入输出。
Exercise 3
Question 1
执行ljmp $PROT_MODE_CSEG, $protcseg后进入32位代码。
这是通过seta20.1和seta20.2打开A20,并切换到保护模式实现的。
Question 2
bootloader的最后一条指令是call *0x10018,对应着bootmain()函数的((void (*)(void))(ELFHDR->e_entry))()语句。
内核的第一条指令是movw $0x1234,0x472。
Question 3
内核的第一条指令位于0x10000c。
Question 4
在bootmain函数中,读取elf文件头。然后,根据elf文件头的信息,去读取各个程序段头。最终,通过程序段头的memsz信息,获知每个程序段的大小,并以此在readseg()函数算出扇区数。
Exercise 5
第一条执行错误的指令是lgdt gdtdesc。
第一条“break”的指令是ljmp $PROT_MODE_CSEG,$protcseg,也即ljmp $0x8,$0x7c36。
因为这一行指令不是位置无关代码,而protecseg链接到了错误的位置。
Exercise 6
bootloader的bootmain()函数的for循环的第一次运行,会读取内核.text段的前29058字节到0x100000。这8个字即是内核.text段的前8个字。
Exercise 7
指令jmp *%eax会发生异常。
Exercise 8
在lib/printfmt.c文件中有一vprintfmt()函数,其中switch的case 'o'修改如下:
case 'o':
num = getuint(&ap, lflag);
base = 8;
goto number;Question 1
在kern/printf.c和kern/console.c之间的接口是cputchar()函数。具体来说,是这样的:
kern/console.c中的cputchar()函数实质上是cons_putc()的套壳。后者在控制台中输出一个字符。kern/printf.c中有putch()函数,实质上是cputchar()的套壳。- putch()
的函数指针作为vprintfmt()的第一个参数。后者又被vcprintf()`调用。 vcprintf()又被cprintf()调用。
Question 2
在已输出的字符数大于CRT_SIZE时,将屏幕滚动一行。
Question 3
- 参数
fmt指向格式化字符串"x %d, y %x, z %d\n";ap指向变参结构体。 - text
cprintf() vcprintf() cons_putc('x') cons_putc(' ') va_arg(), ap 由指向 x 变为指向 y cons_putc('1') cons_putc(',') cons_putc(' ') cons_putc('y') cons_putc(' ') va_arg(), ap 由指向 y 变为指向 z cons_putc('3') cons_putc(',') cons_putc(' ') cons_putc('z') cons_putc(' ') va_arg(), ap 由指向 z 变为指向我们不知道的东西 cons_putc('4') cons_putc('\n')
Question 4
输出He110 World。具体过程如下:
输出
H。输出
57616的十六进制e110。输出
Wo。i在小端序机器上,地址从低到高为4个字节:0x72 0x6c 0x64 0x00,(在ASCII码)恰好可以视为长度为3的空终止字符串rld。又考虑到格式化字符串中的转换说明%s,这一步输出rld。如果是大端序,那么
i应当设为0x726c6400。57616不必改变。
Question 5
将会输出我们不知道的值。具体来说,如果参数3的地址为byte *ptr,则会输出ptr + 4指向的,类型为int的值。
解释:在当前ABI中,函数参数全部使用栈传参,且从右向左压栈。因此,在输出x=后,va_arg会获取值3,并将变参列表指针ap向上移动4字节。在输出y=后,va_arg会获取ptr + 4指向的值。
Question 6
cprintf(..., const char *fmt),其中可变参数需要倒着输入。例如,为了输出x和y的值,需要:
cprintf(y, x, "x=%d, y=%d");Exercise 9
在kern/entry.S的relocated函数中,有一行指令:
movl $(bootstacktop),%esp在此初始化内核栈。
在.data段中,静态地保留了KSTKSIZE大小的空间作为内核栈。
栈指针初始化时,指向上述空间的最高处。
Exercise 10
分别是参数x - 1,返回地址、%ebp和%esi。共计4个32位字。
Exercise 11 and 12
对于mon_backtrace编写如下:
int mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
struct Eipdebuginfo debug_info;
cprintf("Stack backtrace:\n");
uint32_t *ebp = (uint32_t *)read_ebp();
while ((uint32_t)ebp != 0)
{
uint32_t eip = *(ebp + 1), arg_1 = *(ebp + 2), arg_2 = *(ebp + 3),
arg_3 = *(ebp + 4), arg_4 = *(ebp + 5), arg_5 = *(ebp + 6);
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n", ebp,
eip, arg_1, arg_2, arg_3, arg_4, arg_5);
debuginfo_eip(eip, &debug_info);
cprintf(" %s:%d: %.*s+%d\n", debug_info.eip_file,
debug_info.eip_line, debug_info.eip_fn_namelen,
debug_info.eip_fn_name, eip - debug_info.eip_fn_addr);
ebp = (uint32_t *)(*ebp);
}
return 0;
}第6行用于获取当前的%ebp。
while循环的终止条件是%ebp == 0,因为relocated的%ebp为0。
第9 ~ 12行是获取和打印%eip以及5个参数的过程。
第14 ~ 18行是获取和打印%eip以及5个参数的过程。
第19行用于获取上一层函数的%ebp,因为这一层函数的第一个语句即是push %ebp。
再在debuginfo_eip补充如下的代码,用于获取对应的行号:
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline <= rline)
info->eip_line = lline;
else
info->eip_line = -1;Challenge
为了在VGA字符模式向屏幕打印字符,我们必须将它写入硬件提供的VGA字符缓冲区。VGA字符缓冲区中的每一个字符单元为2字节,有如下的格式:
| 位 | 值含义 |
|---|---|
| 0 ~ 7 | 字符ASCII码 |
| 8 ~ 11 | 前景色 |
| 12 ~ 14 | 背景色 |
| 15 | 闪烁 |
可以在kern/console.c中的cga_putc()函数看到如下语句:
if (!(c & ~0xFF))
c |= 0x700;此即硬编码的黑底白字。为了更改输出颜色,我添加了一个color命令,类似Windows的COLOR命令,支持以下三种输入:
color恢复默认的黑底白字。color <fg color>改变前景色。<fg color>的值可以为0 ~ f。color <fg color> <bg color>改变前景色和背景色。<fg color>的值可以为0 ~ f,<bg color>的值可以为0 ~ 7。
color命令会检查参数是否合法,并保证前景色与背景色不同。
color命令的详细实现见代码。这里只说思路:
- 加入全局变量
fg_color,bg_color,并将硬编码的黑底白字修改为:
if (!(c & ~0xFF))
c |= (fg_color << 8) | (bg_color << 12);- 使用
color命令修改全局变量fg_color,bg_color。
效果如图:

color命令