Fix Bug for lab2

在切换到lab3分支后,出现了lab2没有的bug

1
2
3
4
$ make qemu
....
kernel panic at kern/pmap.c:163: PADDR called with invalid kva 00000000
....

根据错误信息,不难找到出问题的代码在哪:
1
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

通过在该行前加入一些代码输出地址信息:

1
2
3
4
5
6
7
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);

cprintf("kern_padir: %08x\n", kern_pgdir);

memset(kern_pgdir, 0, PGSIZE);

cprintf("kern_padir: %08x\n", kern_pgdir);

然后再运行下会得到:

1
2
kern_padir: f018f000
kern_padir: 00000000 // ????

十分迷惑,好像memset()写错了位置。

一个合理的猜想就是kern_pgdir的地址包含到[kern_pgdir, kern_pgdir+PGSIZE)中去了,很可能的原因是“加载到内存”这个过程出了问题,load工作是由loader来做的,而其地址是linker提供的(如果你读过CSAPP的第7章,下面的相信你都很熟悉)

首先kern_pgdir是哪来的?是通过boot_alloc()来的,我们来看下boot_alloc()的代码:

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
static void *
boot_alloc(uint32_t n)
{
static char *nextfree = NULL; // virtual address of next byte of free memory
char *result;

// Initialize nextfree if this is the first time.
// 'end' is a magic symbol automatically generated by the linker,
// which points to the end of the kernel's bss segment:
// the first virtual address that the linker did *not* assign
// to any kernel code or global variables.
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}

if (n == 0) {
return nextfree;
} else if (n > 0) {
result = nextfree;
nextfree = (char*)ROUNDUP((uint32_t)nextfree + n, PGSIZE);

//assert ( - n > KERNBASE);
return result;
}

// Allocate a chunk large enough to hold 'n' bytes, then update
// nextfree. Make sure nextfree is kept aligned
// to a multiple of PGSIZE.
//
// LAB 2: Your code here.

return NULL;
}

透过代码,最值得怀疑的是 end,因为第一个 boot_alloc() 获得的地址就是 end 通过ROUNDUP()提升过来的,根据注释,它是一个magic symbol,由链接器生成,而JOS的链接器是有脚本的。

我们通过objdump可以分析ELF文件,即熟悉的.o之类的,
end 指向 .bss section的尾部,于是我们看下其范围是:

1
2
3
4
5
6
7
8
9
10
$ objdump -h obj/kern/kernel

obj/kern/kernel: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
...
9 .bss 00000f14 f018e100 0018e100 0008f100 2**5
CONTENTS, ALLOC, LOAD, DATA
...

范围为[0xf018e100, 0xf018f014)
实际上,end 并未如期指向 .bss 的尾部,但我们先来分析下kern_pgdir的符号信息

由于kern_pgdir是全局未初始化变量,所以应该在.bss section中,
我们可以通过objdump检测:

1
2
$ objdump -t obj/kern/kernel | ag kern_pgdir
f018f00c g O .bss 00000004 kern_pgdir

问题就出来这,kern_pgdir的地址包含在了[f018f000, f019f000)中,于是memset()就把它写没了

其实,按理说 end.bss 的尾后地址,不应该被覆盖的,但实际上看来并不是,问题究竟出在哪呢?注意:这个 kernel 并不是 make 生成的,而是预先准备的,其 .ld 也是预先写好的。

我们还应该查看下 pmap.o 的symbol table,

1
2
$objdump -t obj/kern/pmap.o | ag kern_pgdir
00000004 O *COM* 00000004 kern_pgdir

你会发现这个与kernel的不一样,为什么呢?
COMMON section,如果读过CSAPP的话应该知道它与.bss的区别,

section meaning
COMMON uninitialized global variables
.bss uninitialized static variables, and global or static variables that are initialized to zero

不过注意这个通过objdump -h是不会显示的,因为COMMON只是pseudosection

至于为什么会出现这种问题,我想可能是编译器版本问题,我用的是gcc 9.3,所以编译得到的有COMMON,而kernel文件是更古老的编译器得到的。

最后,修复问题只要找到kernel.ld.bssend 之间加上 COMMON 就行了:

1
2
3
4
5
6
7
.bss : {
PROVIDE(edata = .);
*(.bss)
*(COMMON) /* new */
PROVIDE(end = .);
BYTE(0)
}

再次make qemu会成功:
image.png