本文最后更新于 2 年前,文中所描述的信息可能已发生改变。
选择了唯一的 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指向变参结构体。
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 命令