從零實現一個操作系統-day9

我的博客startcraft

調試配置

昨天寫完printk函數後很有可能遇到bug,當遇到bug的時候怎樣來調試呢,現在就來配置一下

gdb調試

qemu可以以調試模式啓動配合gdb來進行調試,當然cgdb更加好用一些
qemu的調試模式命令是

qemu -S -s -fda floppy.img -boot a

-S是讓qemu不要繼續運行,等待gdb的運行指令,-s是開啓1234端口等待gdb連接

開啓gdb後就執行一下命令

file time_kernel
target remote :1234
break kern_entry
c

第一句是指定可執行文件,第二句是連接本地的1234端口,第三句是在kern_entry函數處設置斷點,最後c是continue執行到斷點處
這些可以寫到一個腳本里,然後開啓cgdb後自動執行

cgdb -x scripts/gdbinit

注意gdb加載腳本的時間一定要在qemu打開以後,不然會報錯

打印函數調用棧

我們現在來實現一個當內核出現致命錯誤時自動打印函數調用棧的函數

在boot/boot.s裏的start函數調用kern_entry函數之前,我們把ebx寄存器的值賦給了一個全局變量glb_mboot_ptr。這是一個指向了multiboot_t類型結構體的指針,這個結構體存儲了GRUB在調用內核前獲取的硬件信息和內核文件本身的一些信息。我們先給出具體的結構體的定義如下:

include/multiboot.h

#ifndef INCLUDE_MULTIBOOT_H_
#define INCLUDE_MULTIBOOT_H_

#include "types.h"

typedef
struct multiboot_t {
	uint32_t flags; // Multiboot 的版本信息
	/**
	* 從 BIOS 獲知的可用內存
	*
	* mem_lower 和 mem_upper 分別指出了低端和高端內存的大小,單位是。K
	* 低端內存的首地址是 0 ,高端內存的首地址是 1M 。
	* 低端內存的最大可能值是 640K
	* 高端內存的最大可能值是最大值減去 1M 。但並不保證是這個值。
	*/
	uint32_t mem_lower;
	uint32_t mem_upper;

	uint32_t boot_device; // 指出引導程序從哪個磁盤設備載入的映像BIOSOS
	uint32_t cmdline; // 內核命令行
	uint32_t mods_count; // boot 模塊列表
	uint32_t mods_addr;

	/**
	* ELF 格式內核映像的 section 頭表。包括每項的大小、一共有幾項以及作爲名字索引
	* 的字符串。
	*/
	uint32_t num;
	uint32_t size;
	uint32_t addr;
	uint32_t shndx;

	/**
	* 以下兩項指出保存由 BIOS 提供的內存分佈的緩衝區的地址和長度
	* mmap_addr 是緩衝區的地址, mmap_length 是緩衝區的總大小
	* 緩衝區由一個或者多個下面的 mmap_entry_t 組成
	*/
	uint32_t mmap_length;
	uint32_t mmap_addr;

	uint32_t drives_length; // 指出第一個驅動器結構的物理地址
	uint32_t drives_addr; // 指出第一個驅動器這個結構的大小
	uint32_t config_table; // ROM 配置表
	uint32_t boot_loader_name; // boot loader 的名字
	uint32_t apm_table; // APM 表
	uint32_t vbe_control_info;
	uint32_t vbe_mode_info;
	uint32_t vbe_mode;
	uint32_t vbe_interface_seg;
	uint32_t vbe_interface_off;
	uint32_t vbe_interface_len;
} __attribute__((packed)) multiboot_t;

/**
* size 是相關結構的大小,單位是字節,它可能大於最小值 20
base_addr_low 是啓動地址的低位,32base_addr_high 是高 32 位,啓動地址總共有 64 位
* length_low 是內存區域大小的低位,32length_high 是內存區域大小的高 32 位,總共是 64 位
* type 是相應地址區間的類型,1 代表可用,所有其它的值代表保留區域 RAM
*/
typedef
struct mmap_entry_t {
	uint32_t size; // size 是不含 size 自身變量的大小
	uint32_t base_addr_low;
	uint32_t base_addr_high;
	uint32_t length_low;
	uint32_t length_high;
	uint32_t type;
} __attribute__((packed)) mmap_entry_t;

// 聲明全局的 multiboot_t * 指針
extern multiboot_t *glb_mboot_ptr;

#endif // INCLUDE_MULTIBOOT_H_

我們主要關心ELF那一段

/**
	* ELF 格式內核映像的 section 頭表。包括每項的大小、一共有幾項以及作爲名字索引
	* 的字符串。
	*/
	uint32_t num;
	uint32_t size;
	uint32_t addr;
	uint32_t shndx;

我們先添加elf.h這個頭文件
include/elf.h

#ifndef INCLUDE_ELF_H_
#define INCLUDE_ELF_H_

#include "types.h"
#include "multiboot.h"

#define ELF32_ST_TYPE(i) ((i)&0xf)

// ELF 格式區段頭
typedef
struct elf_section_header_t {
	uint32_t name;
	uint32_t type;
	uint32_t flags;
	uint32_t addr;
	uint32_t offset;
	uint32_t size;
	uint32_t link;
	uint32_t info;
	uint32_t addralign;
	uint32_t entsize;
} __attribute__((packed)) elf_section_header_t;

// ELF 格式符號
typedef
struct elf_symbol_t {
	uint32_t name;
	uint32_t value;
	uint32_t size;
	uint8_t info;
	uint8_t other;
	uint16_t shndx;
} __attribute__((packed)) elf_symbol_t;

// ELF 信息
typedef
struct elf_t {
	elf_symbol_t *symtab;
	uint32_t symtabsz;
	const char *strtab;
	uint32_t strtabsz;
} elf_t;

// 從 multiboot_t 結構獲取信息ELF
elf_t elf_from_multiboot(multiboot_t *mb);

// 查看的符號信息ELF
const char *elf_lookup_symbol(uint32_t addr, elf_t *elf);

#endif // INCLUDE_ELF_H_

這段結構體定義了ELF的區段頭符號表等內容,然後我們要從multiboot_t結構體中提取ELF相關信息
kernel/debug/elf.c

#include "common.h"
#include "string.h"
#include "elf.h"

// 從 multiboot_t 結構獲取信息ELF
elf_t elf_from_multiboot(multiboot_t *mb)
{
	int i;
	elf_t elf;
	elf_section_header_t *sh = (elf_section_header_t *)mb->addr;

	uint32_t shstrtab = sh[mb->shndx].addr;
	for (i = 0; i < mb->num; i++) {
		const char *name = (const char *)(shstrtab + sh[i].name);
		// 在 GRUB 提供的 multiboot 信息中尋找
		// 內核 ELF 格式所提取的字符串表和符號表
		if (strcmp(name, ".strtab") == 0) {
			elf.strtab = (const char *)sh[i].addr;
			elf.strtabsz = sh[i].size;
		}
		if (strcmp(name, ".symtab") == 0) {
			elf.symtab = (elf_symbol_t*)sh[i].addr;
			elf.symtabsz = sh[i].size;
		}
	}
	return elf;
}

// 查看的符號信息ELF
const char *elf_lookup_symbol(uint32_t addr, elf_t *elf)
{
	int i;
	for (i = 0; i < (elf->symtabsz / sizeof(elf_symbol_t)); i++) {
		if (ELF32_ST_TYPE(elf->symtab[i].info) != 0x2) {
			continue;
		}
		// 通過函數調用地址查到函數的名字
		if ( (addr >= elf->symtab[i].value) && (addr < (elf->symtab[i].value + elf->symtab[i].size)) ) {
			return (const char *)((uint32_t)elf->strtab + elf->symtab[i].name);
		}
	}
	return NULL;
}

說實話這些我沒太弄懂,想弄明白自己去看文檔吧,我不太懂沒啥能解釋的2333
用objdump文件反彙編生成的內核

objdump -M intel -d time_kernel

可以簡化kern_entry函數的內容來讓分析更簡單
具體的分析過程文檔寫得很詳細,我就不復述了,需要理解的就是函數開頭那一塊

100028:   55                      push   ebp
100029:   89 e5                   mov    ebp,esp
10002c:   83 ec 04                sub    esp,0x4

esp是棧頂指針,在函數一開始先保存原來的ebp,然後將當前棧頂指針賦值給ebp,然給局部變量分配空間(移動棧頂指針),這樣一來在EBP上方分別是原來的EBP,返回地址和參數EBP下方則是臨時變量
然後返回時只需要將棧頂指針移動回來(mov esp ebp) ,然後恢復ebp的值(pop ebp) 最後ret就行了
所以我們只需要拿到當前ebp的值就可以沿着調用鏈獲取所有函數

include/debug.h

#ifndef INCLUDE_DEBUG_H_
#define INCLUDE_DEBUG_H_

#include "console.h"
#include "vargs.h"
#include "elf.h"

#define assert(x, info) \
	do { \
		if (!(x)) { \
		panic(info); \
		} \
	} while (0)

// 編譯期間靜態檢測
#define static_assert(x) \
switch (x) { case 0: case (x): ; }

// 初始化 Debug 信息
void init_debug();

// 打印當前的函數調用棧信息
void panic(const char *msg);

// 打印當前的段存器值
void print_cur_status();

// 內核的打印函數
void printk(const char *format, ...);

// 內核的打印函數帶顏色
void printk_color(real_color_t back, real_color_t fore, const char *format, ...);

#endif // INCLUDE_DEBUG_H_

kernel/debug/debug.c

#include "debug.h"

static void print_stack_trace();
static elf_t kernel_elf;

void init_debug()
{
	// 從 GRUB 提供的信息中獲取到內核符號表和代碼地址信息
	kernel_elf = elf_from_multiboot(glb_mboot_ptr);
}

void print_cur_status()
{
	static int round = 0;
	uint16_t reg1, reg2, reg3, reg4;

	asm volatile ( "mov %%cs, %0;"
	"mov %%ds, %1;"
	"mov %%es, %2;"
	"mov %%ss, %3;"
	: "=m"(reg1), "=m"(reg2), "=m"(reg3), "=m"(reg4));

	// 打印當前的運行級別
	printk("%d: @ring %d\n", round, reg1 & 0x3);
	printk("%d: cs = %x\n", round, reg1);
	printk("%d: ds = %x\n", round, reg2);
	printk("%d: es = %x\n", round, reg3);
	printk("%d: ss = %x\n", round, reg4);
	++round;
}

void panic(const char *msg)
{
	printk("*** System panic: %s\n", msg);
	print_stack_trace();
	printk("***\n");

	// 致命錯誤發生後打印棧信息後停止在這裏
	while(1);
}

void print_stack_trace()
{
	uint32_t *ebp, *eip;

	asm volatile ("mov %%ebp, %0" : "=r" (ebp));
	while (ebp) {
		eip = ebp + 1;
		printk(" [0x%x] %s\n", *eip, elf_lookup_symbol(*eip, &kernel_elf));
		ebp = (uint32_t*)*ebp;
	}
}

改寫init/entry.c測試一下

#include "console.h"
#include "debug.h"

int kern_entry()
{
	init_debug();

	console_clear();

	printk_color(rc_black, rc_green, "Hello, OS kernel!\n");

	panic("test");

	return 0;
}

最後的效果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章