硬核C++实现64位操作系统 内核stack trace panic实现 (2)

Aug 04, 2020

本节介绍如何实现内核的panic函数

有什么用?

OK 那首先演示一下到底panic有什么卵用

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

可以看到panic的上一层函数是Kernel_Main, 这样可以很快速定位出错的位置。

实现

解析ELF符号

由于我们的内核是一个64bit的ELF文件,里面装着一些符号表,例如:

  1. symtab 符号表(记录着上面的panic,Kernel_Main这些的索引)
  2. 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指向的值取*

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.