MIT JOS Lab1.3 笔记
通过符号表 Debug
调试器中的符号表
Stabs 是程序的一种信息格式, 用于在调试器中描述程序. 在 GNU 中使用“ -g”选项,GCC在.s文件中放入其他调试信息,这些信息由汇编器和链接器稍作转换,并传递到最终的可执行文件中。这些调试信息描述了源文件的功能,例如行号,变量的类型和范围以及函数名称,参数和范围。对于某些目标文件格式,调试信息被封装在称为stab(符号表)指令的汇编程序指令中,该指令散布在生成的代码中。 Stabs 是a.out和XCOFF目标文件格式的调试信息的本机格式。GNU工具还可以以COFF和ECOFF对象文件格式产生 stabs。默认情况下汇编器将stab中的信息添加到要构建的.o文件的符号表和字符串表中的符号信息中。 链接器将.o文件合并为一个可执行文件,其中包含一个符号表和一个字符串表。 调试器使用可执行文件中的符号和字符串表作为有关程序的调试信息的来源。
符号表的基本格式
stab汇编程序指令的总体格式有三种 .stabs
(string), .stabn
(number), .stabd
(dot). ,按stab的第一个单词进行区分。 伪指令的名称描述了以下四个可能的数据字段的组合:
.stabs "string",type,other,desc,value
.stabn type,other,desc,value
.stabd type,other,desc
.stabx "string",value,type,sdb-type
对于.stan
和.stand
,没有字符串, 对于.stabd
,值字段是隐式的,并具有当前文件位置的值。对于.stabx
,sdb-type
字段未用于stabs,并且始终可以设置为零。 另一个字段几乎总是未使用,可以设置为零。
然后回头看一下 /kern/kdebug.c
的文件的内容, 主要是两个函数, 第一个函数我们只要知道功能是什么就可以了,
Given an instruction address, this function finds the single stab entry of type 'type' that contains that address.
//
static void stab_binsearch(const struct Stab *stabs, int *region_left, int *region_right, int type, uintptr_t addr)
// 参数的说明
// stabs 是符号表
// *region_left 是查找的左边地址, *region_right 是查找的右边地址
// type 是类型, 比如说函数, 或者 .c文件
// addr 是要查询的地址
Exercise 12. Modify your stack backtrace function to display, for each
eip
, the function name, source file name, and line number corresponding to thateip
.In
debuginfo_eip
, where do__STAB_*
come from? This question has a long answer; to help you to discover the answer, here are some things you might want to do:
- 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
Complete the implementation of
debuginfo_eip
by inserting the call tostab_binsearch
to find the line number for an address.Add a
backtrace
command to the kernel monitor, and extend your implementation ofmon_backtrace
to calldebuginfo_eip
and print a line for each stack frame of the form: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>
Each line gives the file name and line within that file of the stack frame's
eip
, followed by the name of the function and the offset of theeip
from the first instruction of the function (e.g.,monitor+106
means the returneip
is 106 bytes past the beginning ofmonitor
).Be sure to print the file and function names on a separate line, to avoid confusing the grading script.
Tip: printf format strings provide an easy, albeit obscure, way to print non-null-terminated strings like those in STABS tables.
printf("%.*s", length, string)
prints at mostlength
characters ofstring
. Take a look at the printf man page to find out why this works.You may find that some functions are missing from the backtrace. For example, you will probably see a call to
monitor()
but not toruncmd()
. This is because the compiler in-lines some function calls. Other optimizations may cause you to see unexpected line numbers. If you get rid of the-O2
fromGNUMakefile
, the backtraces may make more sense (but your kernel will run more slowly).
首先看一下 stab 的结构体的定义:
// Entries in the STABS table are formatted as follows.
struct Stab {
uint32_t ; // 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
};
/*
stabstr 是对应的字符串数组
n_strx 是字符串索引,这里是对于文件名来说, 函数名来说, 是存储字符串数组的下标(偏移)
n_type 是符号类型,FUN指函数名,SLINE指在text段中的行号
n_othr 目前没被使用,其值固定为0
n_desc 表示在文件中的行号
n_value 表示地址。特别要注意的是,这里只有FUN类型的符号的地址是绝对地址,SLINE符号的地址是偏移量,
其实际地址为函数入口地址加上偏移量。比如第3行的含义是地址f01000b8(=0xf01000a6+0x00000012)对应文件第34行。
*/
调用stab_binsearch
便能找到某个地址对应的行号了。由于前面的代码已经找到地址在哪个函数里面以及函数入口地址,将原地址减去函数入口地址即可得到偏移量,再根据偏移量在符号表中的指定区间查找对应的记录即可。代码如下所示:
完成 /kern/kdebug.c
的代码,
if (lfun <= rfun) {
// stabs[lfun] points to the function name
// in the string table, but check bounds just in case.
if (stabs[lfun].n_strx < stabstr_end - stabstr)
info->eip_fn_name = stabstr + stabs[lfun].n_strx;
info->eip_fn_addr = stabs[lfun].n_value;
// 被调用函数的地址
addr -= info->eip_fn_addr;
// Search within the function definition for the line number.
lline = lfun;
rline = rfun;
} else {
// Couldn't find function stab! Maybe we're in an assembly
// file. Search the whole file for the line number.
info->eip_fn_addr = addr;
lline = lfile;
rline = rfile;
}
// Ignore stuff after the colon.
info->eip_fn_namelen = strfind(info->eip_fn_name, ':') - info->eip_fn_name;
// 类似于上面的寻找函数与文件名的方法
// 查找得到 N_SLINE 表示 line 类型
// lline 与 rline 都是从前面继承过来的
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline > rline)
return -1;
info->eip_line = stabs[lline].n_desc;
// 对于 line 类型来说, n_desc 存储行号
// Hint:
// There's a particular stabs type used for line numbers.
// Look at the STABS documentation and <inc/stab.h> to find
// which one.
// Your code here.
需要注意的是, 后面还有一段代码,
// 从符号表的 line 部分减到有效的文件部分,
while (lline >= lfile && stabs[lline].n_type != N_SOL && (stabs[lline].n_type != N_SO || !stabs[lline].n_value))
lline--;
if (lline >= lfile && stabs[lline].n_strx < stabstr_end - stabstr)
info->eip_file = stabstr + stabs[lline].n_strx;
这一段代码的作用是, 在查找行数之后要重新查找一次源文件, 因为有可能该函数是内联函数, 来自其他文件, 那种文件的类型定义为 N_SOL, 最后在对应的字符串数组中找到文件名称.
下一个问题:
Add a
backtrace
command to the kernel monitor, and extend your implementation ofmon_backtrace
to calldebuginfo_eip
and print a line for each stack frame of the form: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
文件中的 mon_kerninfo 函数, 上面的部分完成就很明显了, 主要是使用 debuginfo_eip
函数获得文件名, 函数名与行号, 所以定义一个 struct Eipdebuginfo *info
类型的指针即可. 补全的函数如下:
int mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
uint32_t *ebp = (uint32_t *)read_ebp();
uint32_t *eip = (uint32_t *)ebp[1];
uint32_t args[5], i;
struct Eipdebuginfo dbg_info;
for (i = 0; i < 5; i++)
args[i] = ebp[i + 2];
cprintf("Stack_backtrace:\n");
while (ebp != NULL)
{
cprintf(" ebp:0x%08x eip:0x%08x args:0x%08x 0x%08x 0x%08x 0x%08x 0x%08x\n",
ebp, eip, args[0], args[1], args[2], args[3], args[4]);
debuginfo_eip((uintptr_t)eip, &dbg_info);
// 获取信息
cprintf("\t%s:%d %.*s+%d\n", dbg_info.eip_file, dbg_info.eip_line, dbg_info.eip_fn_namelen, dbg_info.eip_fn_name, ebp[1] - dbg_info.eip_fn_addr);
ebp = (uint32_t *)ebp[0];
eip = (uint32_t *)ebp[1];
for (i = 0; i < 5; i++)
args[i] = ebp[i + 2];
}
return 0;
}
至此 Lab1 的内容就全部完成了.
总结开机的过程
我们将整个开机的过程以及对应的地址关系, 以及执行的文件对应一下, 就是下面的内容:
执行的过程 | 执行的物理地址 | 代码所在的虚拟地址 | 对应的文件 | 功能 |
---|---|---|---|---|
BIOS | 0x000F0000 ~ 0x00100000 | 在 ROM 中 | 无 | 开机引导磁盘,找到boot loader文件, 并将其导入物理内存 |
boot loader | 0x7c00 ~ 0x7dff | 系统盘的第一个扇区 | ./boot/boot.S 和 ./boot/main.c | 先将内核的 ELF 头部导入物理地址为 0x10000 处, 然后将内核数据段与代码段导入到物理地址为 0x00100000 处 |
entry | 0x0010000c | 0xf010000c | entry.S | 启用页表机制, 将页目录的物理地址存入 CR0 寄存器 |