Lab1-Exercise9~12
本篇文章未经同意引用或参考了以下连接的内容,需要删除请私信我
https://blog.csdn.net/weixin_41761478/article/details/101102354
Exercise 9
确定内核初始化其堆栈的位置,以及堆栈在内存中的确切位置。内核如何为其堆栈预留空间?堆栈指针初始化指向这个保留区域的哪一端?
参考:https://www.cnblogs.com/fatsheep9146/p/5079177.html
需要注意的点:
KSTKSIZE将在memlayout.h中定义。
bootstacktop在entry.S .data 中,值为0xf0110000,通过调试可知。所以堆栈放在了entry.S的 .data 中
Exercise 10
To become familiar with the C calling conventions on the x86, find the address of the test_backtrace function in obj/kern/kernel.asm,set a breakpoint there, and examine(检查) what happens each time it gets called after the kernel starts.
How many 32-bit words does each recursive nesting level of test_backtrace push on the stack, and what are those words? test_backtrace每次被调用的时候 push 了多少个32位的 words 到stack 中,这些 words 又是什么?
注意,为了使这个练习正常工作,你应该使用在 tool page 或者 Athena 上的打了补丁了版本的qemu。否则,您必须手动将所有断点和内存地址转换为线性地址。
参考:https://www.cnblogs.com/fatsheep9146/p/5079930.html
Exercise 11
上面的练习应该提供了足够的信息让你实现一个 stack backtrace function。这个函数的原型已经在kern/monitor.c中等待您了。你可以完全用C来做,但是你会发现inc/x86.h中的read_ebp()函数很有用。您还必须将这个新函数挂接到kernel monitor's command list中,以便用户可以交互地调用它。
实现的backtrace函数应该以以下格式显示函数调用帧的列表:
Stack backtrace:
ebp f0109e58 eip f0100a62 args 00000001 f0109e80 f0109e98 f0100ed2 00000031
ebp f0109ed8 eip f01000d6 args 00000000 00000000 f0100058 f0109f28 00000061
...
ebp表示函数所使用的stack的base pointer,也就是在进入函数之后,函数初始化堆栈之后base pointer。
eip是函数的返回指令指针,指向程序returns时候的地址。The return instruction pointe 通常指向call 指令的下一条指令(为什么))
args后面列出的5个十六进制值是函数的mon_backtrace函数的前5个参数,他们会在当前函数被调用之前入栈。当然,如果函数调用时参数少于5个,那么并不是所有的5个值都有用。(为什么回溯代码不能检测到底有多少参数?如何修正这个限制?)
接下来看代码:i386_init调用test_backtrace调用mon_backtrace
本次练习要实现的backtrace函数就是mon_backtrace,我们要补充mon_backtrace中的代码,使得输出符合上述条件。
void
i386_init(void)
{
extern char edata[], end[];
// Before doing anything else, complete the ELF loading process.
// Clear the uninitialized global data (BSS) section of our program.
// This ensures that all static/global variables start out zero.
memset(edata, 0, end - edata);
// Initialize the console.
// Can't call cprintf until after we do this!
cons_init();
cprintf("6828 decimal is %o octal!\n", 6828);
// Test the stack backtrace function (lab 1 only)
test_backtrace(5);
// Drop into the kernel monitor.
while (1)
monitor(NULL);
}
// Test the stack backtrace function (lab 1 only)
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);
}
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
return 0;
}
我们要补充monitor.c的代码使得系统输出如下语句:
第一行输出第五次执行test_backtrace的ebp,eip,args分别对应图中的
“第五次调用test_backtrace的下一跳指令的地址”
“ebp5”,
“args 1~5"
第二输出第四次执行test_backtrace的ebp,eip,args分别对应图中的
“第四次调用test_backtrace的下一跳指令的地址”
“ebp4”,
“args 1~5"
第三输出第三次执行test_backtrace的ebp,eip,args分别对应图中的
“第三次调用test_backtrace的下一跳指令的地址”
“ebp3”,
“args 1~5"
依此类推
而且通过ebp5我们可以获取ebp4,通过ebp4可以获取ebp3...
也就是讲通过执行 ebp = *ebp,我们可以回顾上一个函数(调用当前函数的函数)的栈
一直执行 ebp = *ebp,当ebp=0的时候,我们就回到了 i386_init 被调用时的栈,因为在i386_init 被调用的时候,ebp被设置为了0。这个也是我们的循环终止条件。

到这里我们可以补充monitor.c的代码了:
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
//ebp = getebp()
//while(ebp = 0){
//cprintf(ebp,ebp[1],ebp[2]...ebp[6])
//ebp = *ebp 这个语句会让堆栈返回
//}
uint32_t *ebp;
ebp = (uint32_t *)read_ebp();
cprintf("Stack backtrace:\r\n");
while (ebp)
{
cprintf(" ebp %08x eip %08x args %08x %08x %08x %08x %08x\r\n",
ebp, ebp[1], ebp[2], ebp[3], ebp[4], ebp[5], ebp[6]);
ebp = (uint32_t *)*ebp;
}
return 0;
}
最后检测正确性:

Exercise 12
此时,backtrace函数应该可以为您提供mon_backtrace()的调用者的地址。然而,在实践中,您通常希望知道与这些地址对应的函数名。例如,您可能想知道哪些函数可能包含导致内核崩溃的错误。
为了帮助您实现这个功能,我们提供了debuginfo_eip()函数,它在符号表中查找eip并返回该地址的调试信息。这个函数在kern/kdebug.c中定义。
练习内容
修改stack backtrace function 以显示每个eip对应的函数名、源文件名和行号。
在debuginfo_eip中,_STAB*来自哪里?
这个问题有一个很长的答案;为了帮助你找到答案,你可以做以下几件事:
- look in the file kern/kernel.ld for _STAB*
- run objdump -h obj/kern/kernel
- run objdump -G obj/kern/kernel
- run gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s.
- see if the bootloader loads the symbol table(符号表) in memory as part of(作为...的一部分) loading the kernel binary 看看bootloader是否将符号表作为kernel二进制文件的一部分加载到内存中
Complete the implementation of debuginfo_eip by inserting the call to stab_binsearch to find the line number for an address.
通过插入对stab_binsearch的调用来查找地址的行号,完成debuginfo_eip的实现。也就是根据eip地址找行号。
Add a backtrace command to the kernel monitor, and extend your implementation of mon_backtrace to call debuginfo_eip and print a line for each stack frame of the form:
向内核监视器添加一个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>
kern/monitor.c:143 代表被调用函数所在文件以及行数 monitor+106代表距离上一个eip的地址距离。(?)
开始实验
其实上面的看不看都行,可以直接看这里。以下的内容大部分抄袭了csdn博主"止此耳耳耳"的贴,原文连接:https://blog.csdn.net/weixin_41761478/article/details/101102354
实验提供了int debuginfo_eip(uintptr_t addr, struct Eipdebuginfo *info)函数,功能是输入一个指令地址addr,和一个Eipdebuginfo结构指针,该函数会查找addr处指令有关的信息,若查找成功则返回0,并把信息填充到该结构中,比如指令所在文件、行号、函数名、函数第一条指令地址等。
要理解并利用这个函数,要先理解stab表。
Stab表
- stab表是什么?
GCC把C语言源文件( ‘.c’ )编译成汇编语言文件( ‘.s’ ), 汇编器把汇编语言文件翻译成目标文件( ‘.o’ )。在目标文件中, 调试信息用 ‘.stab’ 打头的一类汇编指导命令表示, 这种调试信息格式叫’Stab’, 即符号表(Symbol table)。这些调试信息包括行号、变量的类型和作用域、函数名字、函数参数和函数的作用域等源文件的特性。
符号表
符号表在《程序员的自我修养》3.5节中的描述是这样的:
“在链接中,我们将函数和变量统称为符号( Symbol),函数名或变量名就是符号名( Symbol Name)。”
“我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(SymbolTable),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(SymbolValue),对于变量和函数来说,符号值就是它们的地址。”。
在维基百科里面,符号表是这样描述的:“在计算机科学中,符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。”。
- 那么是怎么把调试信息填入目标文件的呢?
在GCC编译源文件时, 如要生成Stab调试信息, 打开编译选项 ‘- gstabs’ 。汇编器处理 ‘.stab’ 打头指导命令, 把Stab中的调试信息填入 ‘.o’ 文件的符号表和串表(string table)中,最后由链接器链接所有的目标文件和有关的库生成可执行文件( ‘a.out’ ),这个可执行文件含有一个符号表和一个串表。
输入命令objdump -h obj/kern/kernel显示可执行文件kernal各个section的头部摘要信息。可以看到有.stab这个符号表和.stabstr这个串表。
进一步验证:
用如下命令:gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c即可生成包含init.c的调试信息的init.s。
在init.s中找到init_i386部分:

然后对比 objdump -G obj/kern/kernel 输出:

可看到调试信息是一一对应的,进一步验证在init.s中生成的.stab信息被放到了可执行文件.stab section中对应的位置。
- .stab 这个表(section)被链接文件kernel.ld原封不动的读入内存,debuginfo_eip函数是如何访问这些调试信息的呢?打开kernel.ld:
![image]()
PROVIDE在ld script中的作用是定义一个变量到.text中去, “.” 代表location counter(LC),LC如果不强制指定的话,它默认就是上一次的LC+中间section的长度
所以PROVIDE(STAB_BEGIN) = . 的效果就是定义__STAB_BEGIN__的值为当前的地址的值,即*(.stab)的值。
而kern/kdebug.c中用extern const struct Stab STAB_BEGIN[]即可访问到这个.stab表的值。
接下来看源码kdebug.c
stab_binsearch函数作用:
主要作用是拿eip和type分别去和stabs section中的n_value和n_type比较,获得一个指定的stab结构体。
debuginfo_eip函数查找过程:
首先找到根据eip包含了该指令的源文件—>找到属于的函数—>指令在该源文件内的行号。
阅读代码可以看到源文件和函数名的查找都是通过调用stab_binsearch(stabs, region_left, region_right, type, addr)
进行二分查找,需要补充的是二分法查找行号的代码。
首先要关注的是type这个参数应该填什么。根据提示打开inc/stab.h,根据注释JOS只使用N_SO, N_SOL, N_FUN和N_SLINE 这几种types,所以查找行号type应该选择N_SLINE。
然后查看关于stab_binsearch函数的注释:*region_left 指向包含指令的stab起始地址,region_right 指向下一个stab之前的地址。如果region_left > *region_right, 则没有找到匹配的stab。如果匹配成功,只是锁定了stabs表中的一个项,而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这个结构体的哪一个域。
事实上,这个结构体的定义和kernel这个可执行文件中的列是一一对应的。
每一列对应一个stab结构体

而在汇编文件init.s中的stab格式如下:
.stabs “STRING”, TYPE, OTHER, DESC, VALUE
.stabn TYPE, OTHER, DESC,VALUE
补充kdebug.c // Your code here.注释下的代码:
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if(lline <= rline)
info->eip_line = stabs[lline].n_desc;
else
info->eip_line = -1;
line和rline可以看作.stab的开始和结束地址,stab_binsearch函数的作用是在lline和rline范围内,寻找一个stab结构体,这个结构体的n_type为N_SLINE,n_value为addr。找到后将该结构体的起始地址赋值给lline。
然后通过stabs[line].desc我们就可以获取指定的行数了。
最后在monitor.c中mon_backtrace函数的添加输出代码:
cprintf(" %s:%d: %.*s+%d\r\n",info.eip_file,info.eip_line,info.eip_fn_namelen,info.eip_fn_name,ebp[1]-info.eip_fn_addr);
就可以了。
运行./grade-lab1评分程序:


浙公网安备 33010602011771号