从零实现一个操作系统-day9

调试配置

昨天写完printk函数后很有可能遇到bug,当遇到bug的时候怎样来调试呢,现在就来配置一下

gdb调试

qemu可以以调试模式启动配合gdb来进行调试,当然cgdb更加好用一些
qemu的调试模式命令是

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

-S是让qemu不要继续运行,等待gdb的运行指令,-s是开启1234端口等待gdb连接

开启gdb后就执行一下命令

1
2
3
4
file time_kernel
target remote :1234
break kern_entry
c

第一句是指定可执行文件,第二句是连接本地的1234端口,第三句是在kern_entry函数处设置断点,最后c是continue执行到断点处
这些可以写到一个脚本里,然后开启cgdb后自动执行

1
cgdb -x scripts/gdbinit

注意gdb加载脚本的时间一定要在qemu打开以后,不然会报错

打印函数调用栈

我们现在来实现一个当内核出现致命错误时自动打印函数调用栈的函数

在boot/boot.s里的start函数调用kern_entry函数之前,我们把ebx寄存器的值赋给了一个全局变量glb_mboot_ptr。这是一个指向了multiboot_t类型结构体的指针,这个结构体存储了GRUB在调用内核前获取的硬件信息和内核文件本身的一些信息。我们先给出具体的结构体的定义如下:

include/multiboot.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#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那一段

1
2
3
4
5
6
7
8
/**
* ELF 格式内核映像的 section 头表。包括每项的大小、一共有几项以及作为名字索引
* 的字符串。
*/
uint32_t num;
uint32_t size;
uint32_t addr;
uint32_t shndx;

我们先添加elf.h这个头文件
include/elf.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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文件反汇编生成的内核

1
objdump -M intel -d time_kernel

可以简化kern_entry函数的内容来让分析更简单
具体的分析过程文档写得很详细,我就不复述了,需要理解的就是函数开头那一块

1
2
3
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#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测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}

最后的效果

本文标题:从零实现一个操作系统-day9

文章作者:

发布时间:2020年04月29日 - 22:04

最后更新:2020年04月30日 - 02:04

原始链接:http://startcraft.cn/post/79a58e88.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------The End-------------