硬核C++实现64位操作系统 内核stack trace panic实现 (2)
本节介绍如何实现内核的panic函数
有什么用?
OK 那首先演示一下到底panic有什么卵用

当我们发现内核某些代码出现一些致命错误或者bug, 例如assert了false的表达式的时候,不应该让程序继续运行,正确的做法是:just let it crash, 但我们希望留下一些函数调用栈的信息,以便发现到底是哪里出的panic. 具体效果如下图

可以看到panic的上一层函数是Kernel_Main, 这样可以很快速定位出错的位置。
实现
解析ELF符号
由于我们的内核是一个64bit的ELF文件,里面装着一些符号表,例如:
- symtab 符号表(记录着上面的panic,Kernel_Main这些的索引)
- strtab 字符串表(记录着上面的panic,Kernel_Main这些符号的名字)
multiboot2会给我们提供一些完整的ELF段,具体结构体为multiboot_tag_elf_sections
我们只需要解析一下这些段即可。
首先来一个结构体用来保存上述两个表的指针和表长度
struct ELFDebugSymbol {
Elf64_Sym *symtab;
uint32_t symtabsz;
const char *strtab;
uint32_t strtabsz;
};
通过multiboot2提供的信息,我们按照标准的ELF64段去作解析
case MULTIBOOT_TAG_TYPE_ELF_SECTIONS:
{
auto elf_tag = (multiboot_tag_elf_sections *)tag;
ELFDebugSymbol debug_symbol;
for (int i = 0; i < elf_tag->num; ++i)
{
auto shdr = (Elf64_Shdr *)(elf_tag->sections + i * elf_tag->entsize);
if (shdr->sh_type == SHT_SYMTAB)
{
debug_symbol.symtab = (Elf64_Sym *)Phy_To_Virt(shdr->sh_addr);
debug_symbol.symtabsz = shdr->sh_size;
}
if (shdr->sh_type == SHT_STRTAB)
{
debug_symbol.strtab = (const char *)Phy_To_Virt(shdr->sh_addr);
debug_symbol.strtabsz = shdr->sh_size;
break;
}
}
debug_init(debug_symbol);
break;
}
上述逻辑就是把全部的ELF段扫描一遍,找到SHT_SYMTAB与SHT_STRTAB即可
debug_init只是简单地把debug_symbol变量保存, 没有其他工作
static ELFDebugSymbol elf_symbol;
void debug_init(ELFDebugSymbol symbol)
{
printk("debug_init. \n");
elf_symbol = symbol;
}
stack-trace实现
先看一下panic函数
void panic(const char *msg)
{
asm volatile("cli");
printk("*** System panic: %s\n", msg);
print_stack_trace();
printk("***\n");
while (1)
{
asm volatile(
"cli \n\t"
"hlt \n\t");
};
}
函数首先把中断关掉然后进入print_stack_trace进行栈打印
static void print_stack_trace()
{
uint64_t *rbp, *rip;
// load current rbp
asm volatile("mov %%rbp, %0"
: "=r"(rbp));
auto kernel_stack_start = (uint64_t)&STACK_START;
// we keep poping stack until reaching start_kernel_base
for (int i = 0; i < 10 && *rbp != kernel_stack_start; ++i)
// while (*rbp != start_kernel_base)
{
printk("rbp at %p\n", rbp);
// rip is above the rbp because
// call instruction will push rip and
// the function will exec
// push rbp;
// mov rsp, rbp;
rip = rbp + 1;
// rip points to the stack which it's value *rip is the return address
auto symbol = elf_lookup_symbol(*rip);
printk(" [%x] %s\n", *rip, symbol);
// rbp points to the current position of rbp on stack
// it's value *rbp points to the previous one on stack
// we make rbp points to the previous one on stack
rbp = (uint64_t *)*rbp;
}
}
在print_stack_trace中首先要拿到当前函数的rbp并将其打印
rip = rbp + 1;
rbp与rip类型位uint64_t*
因为在64位下push操作是8个字节大小的
rbp + 1的位置即为上一个函数调用的返回地址
因为每一个函数都有边界,即ELF符号表标明了一个函数的起始地址和结束地址
rip必须是在某一个函数内部,至于是哪一个函数,需要在elf_lookup_symbol寻找
找到函数后将其打印
最后是更新rbp的操作
rbp = (uint64_t *)*rbp;
我们知道当前函数栈的rbp的值,指向的是上一层函数的rbp位置
把rbp更新为上一个函数栈的rbp 即对当前rbp指向的值取*