寒假OS学习第三天
- 解决调试的问题
- 完成printk函数与部分string类函数
- 打印函数调用栈
Hurlex学习继续
调试
bochs自带调试功能,但是它只能做到asm级别即汇编级别的调试
使用apt install bochs
下载的bochs本身并没有调试功能,需要从
https://nchc.dl.sourceforge.net/project/bochs/bochs/2.6.11/bochs-2.6.11.tar.gz
下载最新的源代码并进行编译
使用sudo apt purge bochs
卸载
首先使用./configure
程序进行初始化
使用如下命令
./configure --prefix=/usr/bochs/bochs-gdb/ --with-x11 --enable-disasm --enable-pci --enable-pcidev --enable-plugins --enable-ne2000 --enable-pnic --disable-docbook --enable-gdb-stub
进行初始化
注意,--prefix=
后面填的是安装目录
另外,--enable-gdb-stub
是使得新的bochs具有gdb联合调试的功能
它与bochs自身的调试指令--enable-debugger
不兼容
因此,如果想要同时拥有两个不同的版本,可以安装在两个不同的目录,然后再用软链链接到/usr/bin
目录下
我自己的两个版本分别是bochs
和bochs-gdb
但是说实话,gdb的功能比它本身自带的功能好多了
接下来,首先进行编译,编译成功后在bochsrc
里面加上这句话:
# GDB调试
gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0;
打开GDB调试
打开GDB调试之后,它不会马上进入,而是一个小黑框
你需要新建一个控制台,然后进入gdb后,首先加载内核文件,再使用target remote
命令连接到bochs:
> file ymwm_kernel
> target remote localhost:1234
一般情况下,我们使用的调试命令是固定的,因此,为了方便起见,使用脚本.gdbinit
:
file ~/Documents/Envs/kernel/ymwm/ymwm_kernel
target remote :1234
b kern_entry
注意,第一次使用脚本会出现警告,GDB因为安全设置会禁止自动加载,在家目录下添加.gdbinit
,内容如下:
set auto-load safe-path /
就可以自动加载脚本了
完成printk与string类库函数
为了之后的开发方便,需要先事先写一些库函数
先写好写的
#include "string.h"
inline void
memcpy(void *dest, const void *src, uint32_t len)
{
uint8_t *s = dest;
const uint8_t *t = src;
while (len --)
*s ++ = *t ++;
}
inline void
memset(void *dest, uint8_t val, uint32_t len)
{
uint8_t *p = dest;
while (len --)
*p ++ = val;
}
inline void
bzero(void *dest, uint32_t len)
{
uint8_t *p = dest;
while (len --)
*p ++ = 0;
}
inline int
strcmp(const char *s, const char *t)
{
while (*s && *s == *t)
{
s ++;
t ++;
}
return *s - *t;
}
inline char *
strcpy(char *dest, const char *src)
{
char *p = dest;
while ((*dest++ = *src++) != '\0')
;
return p;
}
inline char *
strcat(char *dest, const char *src)
{
char *p = dest;
while (*dest)
dest ++;
while ((*dest++ = *src++) != '\0')
;
return p;
}
inline int
strlen(const char *src)
{
const char *p = src;
while (*src)
src ++;
return src - p;
}
printk函数的实现
要实现printk函数,我个人认为有这么几点难实现:
- 变长参数
- 格式符
变长参数
GCC有一个内建的类型__buildin_va_list
而我们为了方便使用,另外创建了一个头文件来给它包装下:
#ifndef INCLUDE_VARGS_
#define INCLUDE_VARGS_
typedef __buildin_va_list va_list;
#define va_start(ap, last) (__buildin_va_start(ap, last))
#define va_arg(ap, type) (__buildin_va_arg(ap, type))
#define va_end(ap)
#endif
它的用法如下:
- 在调用参数表前,需要定义一个
va_list
类型的变量 - 使用
va_start
进行初始化 - 使用
va_end
结束初始化
但是罪恶的事情是,这个内建的类型居然在我的电脑上跑不了
我只好自己写一个了
#define va_list char *
#define va_start(p, first) (p = (va_list)&first + sizeof(first))
#define va_arg(p, type) (*(type*)((p += sizeof(type) ) - sizeof(type)))
#define va_end(p) (p = (va_list)NULL)
注意,我们写的这个宏是简化版本的,很简化的那种,它不能判断什么时候参数调用完了
我们的printk
函数声明如下:
void print_color(real_color_t back, real_color_t fore, const char *format, ...);
则使用方法为:
void print_color(real_color_t back, real_color_t fore, const char *format, ...)
{
va_list argp;
uint32_t arg;
va_start(argp, format);
while ((arg = va_arg(argp, uint32_t)) != 0)
{
// do something
}
va_end(argp);
}
上面的调用方式有大大的问题,即碰到输入参数为0就结束了。
我们的方法貌似很难获取参数个数。
使用GDB进行调试,发现argp
这个参数有两个成员:gp_offset
和fp_offset
,当两者相等时,说明已经调取了全部参数。
改写为:
void print_color(real_color_t back, real_color_t fore, const char *format, ...)
{
va_list argp;
uint32_t arg;
va_start(argp, format);
while (argp->gp_offset != argp->fp_offset)
{
arg = va_arg(argp, uint32_t);
}
va_end(argp);
}
这样子就能获取所有的可变参数。
实际上,我们的format
字符串里面会告诉你有几个参数是需要调用的,例如"hello, %s, world"
里面就是有一个参数是需要调用的,而"%d %d %d"
就是有两个参数是需要调用的
接下来我们要做的就是想办法去解析它们,这是第二个难点
我参考了这篇博文
https://blog.csdn.net/c1204611687/article/details/86133774
最终写出了这样的代码
https://blog.csdn.net/weixin_45206746/article/details/113107156
打印函数调用栈
目标:当内核遇到致命错误的时候自动打印当前的函数调用栈
我们使用GRUB代替bootloader程序。CPU上电后,BIOS会将第一个可使用设备的第一个扇区即前512个字节加载到0x7c00处,一般来说这512字节能做的事情很有限,所以它所做的第一件事情一般就是把接下来的512字节加载到0x7d00处,等到执行我们的GRUB时,它也需要512字节来执行,因此最终我们的镜像会被加载到0x8000处。
在进行镜像加载后,GRUB会将一个指向multiboot_t类型结构体的指针方在ebx寄存器里面,这个结构体存储了GRUB在调用内核前获取的硬件信息和内核文件的本身的信息
在multiboot_t类型中有一个成员,它指向整个映像的ELF信息,我们利用这个信息可以获取到每个函数的名称以及它们的代码的区域,这样子给出一个地址,就可以判断它是在哪一个函数里面的
要获取函数调用栈,得明确函数的调用链
一个函数调用另一个函数,第一件事情是把下一条指令的地址推到栈里面,然后开一个新的函数栈,存储ebp的值,并且把当前esp的值赋予ebp,esp再减去这个函数栈的大小
是这么回事,函数调用另外一个函数,另外一个函数要返回的时候必定要知道要返回到哪个地方,所以call这个指令会在调用新函数前,保存下一条指令的地址,推入栈中,也就是保存返回地址
另外,ebp记录的是当前函数栈的基址,它属于被调用者保存的寄存器,因此,在新的函数中,首先要保存的就是ebp的地址,然后把esp的地址赋给ebp。因为前面已经把一个返回地址推到栈里面了,esp的地址会比原来小4个字节。
接下来,esp减去一个值,这个值是当前函数栈需要的大小,即给函数栈分配空间
所以,所有的返回地址都存储在了ebp里面,我们要做的事情是把ebp的历史记录一个一个挖出来,判断它是哪个函数里面的地址
总结一下,要实现前面的这些目的,我们需要做的有:
- 获取multiboot_t结构体
- 通过multiboot_t结构体获取整个映像的ELF信息
- 通过ELF信息,构造一个函数,给它一个地址,它能返回该地址对应的函数
- 寻找ebp的所有历史记录
我们不着急,一步一步来
获取multiboot_t结构体
首先,我们在boot.s里面有这么一句话:
[GLOBAL glb_mboot_ptr] ;声明struct multiboot*变量
mov [glb_mboot_ptr], ebx ; 将ebx存入
根据multiboot规范,GRUB在完成引导后,应当将multiboot_t的地址放在ebx里面
因此我们现在可以通过全局变量glb_mboot_ptr
来获取multiboot_t结构体
其次,我们需要知道multiboot_t
的结构
参考https://blog.csdn.net/u013012494/article/details/39178345,、multiboot规范的中文翻译
一个multiboot_t长这个样子
+----------------------+
0 | flags | (要求)
+----------------------+
4 | mem_lower | (如果设置了flags[0],出现)
8 | mem_upper | (如果设置了flags[0],出现)
+----------------------+
12| boot_device |(如果设置了flags[1],出现)
+----------------------+
16| cmdline | (如果设置了flags[2],出现)
+----------------------+
20| mods_count |(如果设置了flags[3],出现)
24| mods_addr | (如果设置了flags[3],出现)
+----------------------+
28| |
- | |
40| syms | (如果设置了flags[4]或flags[5],出现)
+----------------------+
44| mmap_length |(如果设置了flags[6],出现)
48| mmap_addr |(如果设置了flags[6],出现)
+----------------------+
52| drives_length |(如果设置了flags[7],出现)
56| drives_addr |(如果设置了flags[7],出现)
+----------------------+
60| config_table |(如果设置了flags[8],出现)
+----------------------+
64| boot_loader_name |(如果设置了flags[9],出现)
+----------------------+
68| apm_table |(如果设置了flags[10],出现)
+----------------------+
72| vbe_control_info |(如果设置了flags[11],出现)
76| vbe_mode_info |
80| vbe_mode |
82| vbe_interface_seg |
84| vbe_interface_off |
86| vbe_interface_len |
+----------------------+
其中的第28位到第40位与flags第4、5位的取值有关
这两个取值是互斥的,要么取第四个,要么取第五个
如果第四个取值了,那么结构为
+-------------------+
28| tabsize |
32| strsize |
36| addr |
40| reserved (0) |
+-------------------+
它指向一个a.out
格式的可执行文件
如果设置了第五位
+-------------------+
28| num |
32| size |
36| addr |
40| shndx |
+-------------------+
它指向一个ELF格式的可执行文件
我们设置的flags是
; 按页对齐
MBOOT_PAGE_ALIGN equ 1 << 0
; 内存空间的信息包含在multiboot中
MBOOT_MEM_INFO equ 1 << 1
;定义multiboot标记
MBOOT_HEADER_FLAGS equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO
说实话我也不知道FLAGS第四位和第五位设置成了啥。
但是我们的操作系统映像是ELF格式的,那么应该GRUB会帮我们设置成ELF格式对应的东西吧
那么我们的multiboot结构应当是:
#include "types.h"
extern multiboot_t *glb_mboot_ptr; // 全局,从boot.s里面引进
typedef
struct multiboot_t
{
uint32_t flags;
// 从BIOS获取的可用内存
uint32_t mem_lower;
uint32_t mem_upper;
uint32_t boot_device; // 载入映像的设备
uint32_t cmdline;
// boot模块
uint32_t mods_count;
uint32_t mods_addr;
// ELF的section头表
uint32_t num;
uint32_t size;
uint32_t addr;
uint32_t shndx;
// BIOS提供的缓冲区
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; // bootloader的名字
uint32_t apt_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))mboot_t;
这里有一句__attribute__((packed))
这个东西是GCC内建的,意思是按照紧凑的方式进行结构体的排列,而不是对齐的方式
话不多说啊,打印一下看看
#include "debug.h"
int kern_entry()
{
console_clear();
printk("%x\n"
"%x\n"
"%s\n",
glb_mboot_ptr->flags,
glb_mboot_ptr->boot_device,
glb_mboot_ptr->boot_loader_name);
return 0;
}
输出是
0x7E7
oxFFFFFF
GNU GRUB 0.97
很神奇的事情是,我在boot.s里面明明只设置了flags的第一位和第二位,按道理应该是0x0000 0003
,二进制表示0000 0000 0000 0000 0000 0000 0000 0011
但是给我的是0x0000 07E7
,二进制表示0000 0000 0000 0000 0000 0111 1110 0111
只有第三位和第四位是没有选择的
到目前为之,第一步已经完成了
获取映像的ELF信息
要获取映像的ELF信息,首先我们得对ELF格式的文件有所了解
ELF文件由四部分构成:
EFL头、Program header table、Section、Section header table
只有ELF头的位置是固定的
multiboot_t里面的num、size、addr、shndx描述的是ELF的section header table部分
addr指向section header table的部分
size是每一项的大小
num是共有几项
shndx是作为名字索引的字符串表
先打印一下看看
num:9
size:40
addr:0x10F000
shndx:0x8
使用readefl -h
查看映像的信息
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x10000c
Start of program headers: 52 (bytes into file)
Start of section headers: 66988 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 9
Section header string table index: 8
size
对应Size of section headers
num
对应Number of section headers
shndx
对应Section header string table index
,这个地方存放section header名字的字符串
使用readelf -S
查看section header table的信息
There are 9 section headers, starting at offset 0x105ac:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00100000 001000 001000 00 AX 0 0 16
[ 2] .data PROGBITS 00101000 002000 001000 00 WA 0 0 4
[ 3] .bss NOBITS 00102000 003000 009000 00 WA 0 0 32
[ 4] .stab PROGBITS 0010b000 00c000 003000 0c A 5 0 4
[ 5] .stabstr STRTAB 0010e000 00f000 001000 00 A 0 0 1
[ 6] .symtab SYMTAB 00000000 010000 000370 10 7 36 4
[ 7] .strtab STRTAB 00000000 010370 0001fe 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 01056e 00003b 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
很显然,我们需要的是.symtab
和.strtab
,它们一个存储符号、一个存储字符串
因此,我们可以使用multiboot->addr
寻找到ELF文件的section header table,通过section header table可以找到符号表和字符串表。符号表记录了函数、全局变量等等的位置、信息等,字符串表记录了这些变量的名字
思路有了,尝试一下打印所有的函数名吧
首先要定义一下数据类型
在/usr/include/elf.h
中有ELF文件的结构体格式规范
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
我们照抄,最终得到像这样子的代码:
#ifndef INCLUDE_ELF_H_
#define INCLUDE_ELF_H_
#include "types.h"
#include "multiboot.h"
typedef struct
{
uint32_t name; // section名
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;
}elf_section_header_t; // section header table
enum
{
ELF_SEC_NULL = 0,
ELF_SEC_PRODATA = 1,
ELF_SEC_SYMTAB = 2,
ELF_SEC_STRTAB = 3,
ELF_SEC_RELA = 4,
ELF_SEC_HASH = 5,
ELF_SEC_DYNAMIC = 6,
ELF_SEC_NOTE = 7,
ELF_SEC_BBS = 8
};
typedef struct
{
uint32_t name;
uint32_t value;
uint32_t size;
unsigned char
type: 4,
bind: 4;
unsigned char other;
uint16_t shndx; // section index
}elf_symbol_t; // symbol table
enum
{
ELF_STT_NOTYPE = 0,
ELF_STT_OBJECT = 1,
ELF_STT_FUNC = 2,
ELF_STT_SECTION = 3,
ELF_STT_FILE = 4,
ELF_STT_COMMON = 5,
ELF_STT_TLS = 6
};
enum
{
ELF_STB_LOCAL = 0,
ELF_STB_GLOBAL = 1,
ELF_STB_WEAK = 2,
ELF_STB_NUM = 3
};
typedef struct
{
elf_symbol_t *symtab; // 存储symtab的地址
const char *strtab; // 存储符号表的地址
uint32_t symtab_num;
}elf_symstr_t;
elf_symstr_t elf_analyse_multiboot(multiboot_t *mboot); // 从mboot里面获取elf信息
#endif
注意这个定义,
typedef struct
{
uint32_t name;
uint32_t value;
uint32_t size;
unsigned char
type: 4,
bind: 4;
unsigned char other;
uint16_t shndx; // section index
}elf_symbol_t; // symbol table
这里面使用了struct的位域
type占用一个字节的低4位,bind占用一个字节的高四位
接下来,我们需要实现通过multiboot_t加载ELF信息的函数
#include "elf.h"
#include "string.h"
elf_symstr_t elf_analyse_multiboot(multiboot_t *mboot)
{
elf_symstr_t res;
// 获取section header table地址
elf_section_header_t *sh = (elf_section_header_t *)mboot->addr;
// section header的数量
uint32_t num = mboot->num;
uint32_t shstrtab = sh[mboot->shndx].addr;
while (num --)
{
// 获取记录section header名字的字符串
const char *name = (const char *)(shstrtab + sh->name);
if (sh->type == ELF_SEC_SYMTAB)
{
res.symtab = (elf_symbol_t *)sh->addr;
res.symtab_num = sh->size / sizeof(elf_symbol_t);
}
if (strcmp(name, ".strtab") == 0)
res.strtab = (const char *)(sh->addr);
sh ++;
}
return res;
}
实验一下:
#include "debug.h"
int kern_entry()
{
console_clear();
elf_symstr_t func_names = elf_analyse_multiboot(glb_mboot_ptr);
// 打印每一个函数的名字
int i = 0;
while (i < func_names.symtab_num)
{
if (func_names.symtab[i].type == ELF_STT_FUNC)
{
const char *name = func_names.strtab + func_names.symtab[i].name;
printk("%d, %s\n", i, name);
}
i ++;
}
return 0;
}
输出为:
构造返回地址对应的函数字符串的函数
掌握了原理之后,我们接下来就可以写一个这样子的函数:给出一个地址,给出这个地址在哪个函数里面
例如地址0xabc在函数func里面,那么就返回一个字符串func
const char *
elf_lookup_symbol(uint32_t addr, elf_symstr_t *elf)
{
int i;
for (i = 0; i < elf->symtab_num; i ++)
{
if (elf->symtab[i].type == ELF_STT_FUNC &&
addr >= elf->symtab[i].value &&
addr < elf->symtab[i].value + elf->symtab[i].size)
{
return (const char *)(elf->strtab + efl->symtab[i].name);
}
}
return "NOT A FUNC";
}
寻找EBP的所有历史记录
当函数调用暂停之后,只要拿到此时的EBP寄存器的值,就可以循着EBP找到所有调用函数的返回地址
内联汇编参考:https://blog.csdn.net/weixin_41256413/article/details/80444103
static void
print_track_trace()
{
uint32_t *ebp, *eip;
asm volatile(
"mov %%ebp, %0"
: "=r"(ebp)
); // 获取EBP值
while (ebp)
{
eip = ebp + 1;
printk("[%x] %s\n", *eip, elf_lookup_symbol(*eip, &kernel_elf));
ebp = (uint32_t *)*ebp;
}
}
使用这个函数实现寻找所有的调用函数的返回地址
这是怎么做到的??
首先,调用函数,使用call
指令
call
指令分成两部分:
首先,它将下一条指令的所在地址压入栈
然后,跳转到要调用的函数所在的区域
此时栈里面的东西有:
调用者的返回地址
被调用函数所做的第一件事情就是将EBP压入栈
因为EBP是被调用者保存函数,且EBP有特殊的含义:当前的栈帧的基址
此时栈里面的东西有:
调用者的返回地址
调用者的基址
然后,将ESP的值赋予EBP,作为新的函数栈帧的基址
PUSH指令会使得ESP减少4
接下来ESP再减小一定的值,给栈帧扩充空间
那么,此时,EBP的值就是调用者基址的地址,也就是调用者的EBP
则EBP+4之后,它所指向的就是调用者返回地址
我们可以通过这个返回地址找到调用者函数是哪一个
具体结合代码看看
最开始的时候,我们调用的是汇编里面的start
它先初始化了ESP=0x8000、EBP=0
它把0x100025
压进栈,然后跳转到处kern_entry函数
0x7FFC 0x100025 返回地址
此时ESP的值为0x7FFC,因为PUSH会把ESP的值减小4,而EBP仍然为0x0
kern_entry第一件事情是把EBP压进栈
0x7FFC 0x100025 返回地址
0x7FF9 0x000000 调用者基址
然后把此时的ESP的值赋给EBP,EBP=0x7FF9
发现了吗,EBP自身存储的是该栈帧的基址,但是它指向了调用它的函数的EBP,而EBP减去4之后,存储的就是调用它的函数的返回地址
因此,有下列关系:
*(EBP-4)=返回地址
*EBP
=调用者的EBP
下面贴出完整的DEBUG代码
#include "debug.h"
static elf_symstr_t kernel_elf;
static void print_track_trace();
static void
print_track_trace()
{
uint32_t *ebp, *eip;
asm volatile(
"mov %%ebp, %0"
: "=r"(ebp)
); // 获取EBP值
while (ebp)
{
eip = ebp + 1;
printk("[%x] %s\n", *eip, elf_lookup_symbol(*eip, &kernel_elf));
ebp = (uint32_t *)*ebp;
}
}
void
panic(const char *msg)
{
printk("*** System panic: %s\n", msg);
print_track_trace();
printk("***\n");
while (1)
;
}
void
init_debug()
{
kernel_elf = elf_analyse_multiboot(glb_mboot_ptr);
}
进行实验:
#include "debug.h"
int add1(int num);
int min2(int num);
int min2(int num)
{
assert(num, "num is zero!");
printk("num is bigger than zero: %d\n", num);
return add1(num - 2);
}
int add1(int num)
{
assert(num, "num is zero!");
printk("num is bigger than zero: %d\n", num);
return min2(num + 1);
}
int kern_entry()
{
console_clear();
init_debug();
add1(3);
return 0;
}
运行结果: