LAB1_Part3 The Kernel
前言
记录一下自己的学习过程
实验内容翻译:
https://gitee.com/cherrydance/mit6.828
该翻译仅供参考
练习9
确定内核初始化其堆栈的位置,以及堆栈在内存中的确切位置。内核如何为其堆栈保留空间?堆栈指针在这个保留区域的哪个“端”初始化指向?
直接查看entry.S文件,发现在文件末尾存在对堆栈的初始化。其中movl $0x0,%ebp
和movl $(bootstacktop),%esp
两条指令设置栈指针。
使用gdb调试运行该点,打印寄存器esp的值
可以得到esp的值是0xf0110000,而在bootstack中可以看到其声明了KSTKSIZE,即8*4096=32KB大小的空间。而栈是由高地址向低地址延伸的。因此栈的地址为
0xf010 8000到0xf110 0000。
练习10
为了熟悉x86架构上的C调用约定,并了解test_backtrace函数的行为,您可以按照以下步骤进行操作:
找到obj/kern/kernel.asm文件中test_backtrace函数的地址,在该地址处设置一个断点,在内核启动后,每次调用test_backtrace函数时,观察发生了什么?对于每个递归嵌套级别的test_backtrace函数,注意观察它在堆栈上推入了多少个32位字(32-bit words),以及这些字是什么。
请注意,为了正确执行此练习,您应该使用工具页面上或Athena上提供的修补过的QEMU版本。否则,您将需要手动将断点和内存地址转换为线性地址。
查看test_backtrace的c语言代码:
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);
}
得知该函数是递归调用。接下来查看kernel.asm文件找到test_backtrace。其代码如下:
我发现我的代码与网上流传的不太一样,没办法只能自己分析。
f0100040: f3 0f 1e fb endbr32
f0100044: 55 push %ebp
f0100045: 89 e5 mov %esp,%ebp
f0100047: 56 push %esi
f0100048: 53 push %ebx
这里是函数调用前的准备代码。在将上个函数的ebp入栈之后将esp的值赋值给ebp。所以这个ebp就是当前函数的栈底。然后入栈两个寄存器保存值。
此时ebp的值为0xf010ffd8。在保存两个寄存器之后esp = 0xf010ffd0。
打印得到结果确实是这样。
f0100057: 83 ec 08 sub $0x8,%esp
f010005a: 56 push %esi
f010005b: 8d 83 58 08 ff ff lea -0xf7a8(%ebx),%eax
f0100061: 50 push %eax
f0100062: e8 69 0a 00 00 call f0100ad0 <cprintf>
if (x > 0)
f0100067: 83 c4 10 add $0x10,%esp
这一段代码执行完之后esp不变
在下一次调用前最后的代码如下
test_backtrace(x-1);
f010006e: 83 ec 0c sub $0xc,%esp
f0100071: 8d 46 ff lea -0x1(%esi),%eax
f0100074: 50 push %eax
f0100075: e8 c6 ff ff ff call f0100040 <test_backtrace>
指令sub $0xc,%esp
是临时空间,再push %eax
,esp减少16。
所以在下一次调用前栈里多了24byte的数据。但是考虑到ebp的数据和返回地址。一次调用应该是有32byte的数据压入。如图,下个ebp比上个ebp小0x20
其地址格式如下图所示:
练习11
请根据上述规定实现backtrace函数。使用与示例中相同的格式,否则评分脚本可能会混淆。当您认为函数已经正确运行时,运行make grade命令,查看输出是否符合我们的评分脚本所期望的格式,如果不符合,则进行修正。在提交了Lab 1的代码之后,您可以随意更改backtrace函数的输出格式。
如果您使用了read_ebp()函数,请注意,GCC可能会生成在mon_backtrace()函数的函数前导代码之前调用read_ebp()的"优化"代码,这会导致堆栈跟踪不完整(最近一次函数调用的堆栈帧缺失)。虽然我们已经尝试禁用导致这种重新排序的优化,但您可能需要检查mon_backtrace()的汇编代码,确保调用read_ebp()的位置在函数前导代码之后。
首先查看提示给出的read_ebp函数。
static inline uint32_t
read_ebp(void)
{
uint32_t ebp;
asm volatile("movl %%ebp,%0" : "=r" (ebp));
return ebp;
}
它会返回ebp的值,所以我们可以用这个函数读取当前栈中ebp的值。根据练习10的图,我们可以得到下一个ebp = *(uint32_t *)ebp
。
eip是函数返回的指令地址,eip = *((uint32_t *)ebp+1)
。同理,其他参数也是这样获取。
以一个循环来读取整个栈的值,那结束条件是啥呢?由前文我们可以得到,ebp的初始值为0,因此只要ebp不等于,那就继续循环读取栈帧。
最终代码如下:
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
cprintf("Stack backtrace:\n");
uint32_t ebp = read_ebp();
for(;ebp != 0; ebp = *(uint32_t *)ebp){
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\n", ebp, *((uint32_t *)ebp + 1),
*((uint32_t *)ebp + 2), *((uint32_t *)ebp + 3), *((uint32_t *)ebp + 4), *((uint32_t *)ebp + 5),
*((uint32_t *)ebp + 6));
}
return 0;
}
运行结果如下:
练习12
要修改您的堆栈回溯函数,以显示每个eip对应的函数名、源文件名和行号。
通过插入对stab_binsearch的调用,完成debuginfo_eip函数的实现,以便根据地址查找行号。
在内核监视器中添加一个backtrace命令,并扩展您对mon_backtrace函数的实现,使其调用debuginfo_eip函数,并打印出每个堆栈帧的行,格式如下:
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>
每行显示堆栈帧的eip所在的文件名和文件中的行号,然后是函数的名称和eip相对于函数开始位置的偏移量(例如,monitor+106表示返回eip距离monitor函数开头106字节)。
看了一遍建议的操作,没看出啥。考虑到说要使用stab_binsearch函数,所以直接看整个函数的参数和返回值。
static void
stab_binsearch(const struct Stab *stabs, int *region_left, int *region_right,
int type, uintptr_t addr)
文件提示详细说明了函数作用。
Stab结构体如图:
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
};
文件给出了代码实现的要求和提示,根据提示,该部分代码如下
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if(lline <= rline){
info->eip_line = stabs[lline].n_desc;
//这里有个坑,提示说要用rline,但一直过不去最后一个test
//只能说离谱,最后改为lline救过了
}else {
info->eip_line = -1;
}
要得出目标结果明显是要我们调用函数debuginfo_eip(uintptr_t addr, struct Eipdebuginfo *info)
,前面的addr我们知道就是eip,所以我们重点关注后面的结构体
// Debug information about a particular instruction pointer
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
};
看到这就明白了,最后我们要打印的输出就是存储在这个结构体中,最终代码如下:
debuginfo_eip(eip, &info);
cprintf(" %s:%d: %.*s+%d\n", info.eip_file,
info.eip_line, info.eip_fn_namelen, info.eip_fn_name,
eip-info.eip_fn_addr);
make grade结果如图:
总结
总算是完成了lab1,感觉头都秃了!!!!!
虽说是完成了,但感觉其中还有很多值得考虑的细节,只能算是勉强过了一遍。
后续lab继续。
有问题欢迎大家指出。