mit6.828 – lab1笔记

  • mit6.828 – lab1笔记已关闭评论
  • 74 次浏览
  • A+
所属分类:linux技术
摘要

安装环境编译qemu打开两个窗口,在第一个窗口中 make qemu-gdb,会启动内核,但在执行第一个指令之前停下;
在第二个窗口中make gdb,实时观察第一个窗口中的执行过程。

安装环境编译qemu

mit6.828 - lab1笔记

1. PC启动

打开两个窗口,在第一个窗口中 make qemu-gdb,会启动内核,但在执行第一个指令之前停下;
在第二个窗口中make gdb,实时观察第一个窗口中的执行过程。

mit6.828 - lab1笔记

从这里可以观察到:

  • IBM PC 在物理地址 0x000ffff0 开始执行, 位于为 ROM BIOS 保留的 64KB 区域的最顶部。
  • PC 的第一个指令执行的是 CS=0xf000 IP=0xfff0
  • 第一条指令是 jmp 指令, 跳转到分段地址 CS = 0xf000 和 IP = 0xe05b。

mit6.828 - lab1笔记

## 为什么第一个指令在这个位置? 这是因为 8088的BIOS 是“硬连线”的 到物理地址范围 0x000f0000-0x000fffff, 从而确保BIOS首先获得对机器的控制 0xffff0 是 BIOS 结束前的 16 个字节 (0x100000),BIOS做的第一件事是向后jmp 到 BIOS 中较早的位置; 

2. bootloader

bootloader 的开始
bootsec 如果磁盘是可启动的, 第一个扇区称为 boot sector, 因为这是引导加载程序代码所在的位置。

当 BIOS 找到可启动软盘或硬盘时, 会将其加载(512字节)至物理地址的内存的0x7c00 0x7dff。然后64KB大小的BOIS的最后一句话即是:
jmp $0x0000,$0x7c00
将控制转交给了 bootloader

mit6.828 - lab1笔记

boot loader 的任务有两个:

  1. 将处理器从实模式切换到保护模式。因为实模式最多只能访问1MB的内存。
  2. 从硬盘读取内核,加载到内存。bootstrap使用特殊I/O指令,直接访问IDE磁盘设备存储器来读取。

boot loader 的实现:
 一个汇编语言源文件,boot/boot.S
 一个 C 源文件 boot/main.c
 反汇编文件: obj/boot/boot.asm

先看代码、然后看反汇编、再调试,摸清楚 boot loader 的流程

阅读源码

boot/boot.S的内容:

  1. 加载全局描述符表 GDT
  2. 开启保护模式:将CR0寄存器的PE_ON位置1
  3. 通过ljmp进入保护模式
  4. 加载各个段描述符
  5. 跳转至 bootmain.c

boot/bootmain.c的内容

  1. 加载kernel的elf文件头:从硬盘1号扇区(第二个扇区)的起始处读取4KB大小的内容至 0x0010_0000处,并将其视为ELF结构体
  2. 将 kernel 的各个段加载至内存

boot/boot.S

boot.S 中有一个令人迷惑的代码:

mit6.828 - lab1笔记

在即将跳转到C语言实现的bootmain的时候,居然将 start标号 给了esp,那么 start 代表了什么?

mit6.828 - lab1笔记

啊,start位于代码的一开始的地方,这里不是应该存代码吗?给了esp,后面栈不得把这下面的代码的都给覆盖了?
稍等下,栈是从高地址向低地址生长的,这里boot.S的代码在ide里看虽然写在start下面,但是在内存里是start更高的地方。从 obj/boot.asm 里来看:

mit6.828 - lab1笔记

start 位于 0x7C00,之后的代码位于0x7C00之上,而栈则向0x7C00下方生长

mit6.828 - lab1笔记

boot/main.c

boot/bootmain.c的内容

  1. 加载kernel的elf文件头:从硬盘1号扇区(第二个扇区)的起始处读取4KB大小的内容至 0x0010_0000处,并将其视为ELF结构体
  2. 将 kernel 的各个段加载至内存

mit6.828 - lab1笔记

其中的循环会逐个将 /obj/kern/kernl 的段加载至对应的物理地址(注意,readseg 的第一个参数是 ph->p_pa),可以通过 objdump -l kernel 查看:

mit6.828 - lab1笔记

最终内存视图如下:

mit6.828 - lab1笔记

 ##### 看反汇编发现了一些有趣的事情: 1. 循环中,调用函数后的递增操作,在汇编层面会在调用之前发生  ![image.png](https://pic-bed-1258913394.cos.ap-nanjing.myqcloud.com/20240501213701.png)   2. 调用前,调用者负责传参,被调者负责保护现场,还原现场;返回后,调用者负责将传参占用的空间还原   

关于ELF和编译链接

在开发者完成一个C语言程序程序 xxx.c ,为了让他跑起来,需要由编译器将其编译成 xxx.o 的对象文件,然后由链接器将所有已经编译的对象文件链接成 xxx 可执行文件。


3. 内核

目的:理解lab1的简易内核的工作过程

任务:阅读 /kern 下的代码。

lab1的内核功能十分简单,如上文中运行起来的那样,他的shell只提供两个功能,help和kerninfo。
内核相关的代码位于 /kern 之下。

entry.S:初始化内存映射,设置页表、栈指针 entrypgdir.c:页表设计  init.c:初始化shell,初始化终端设备、启动shell console.h, console.c:终端功能的实现 printf.c:打印功能的实现 monitor.h, monitor.c:shell功能的实现 

挺好,为了理解 lab1 的内核,接下来就沿着 entry.S 和 init.c 去分析内核。
即,分析entry.S对内存映射的处理、init.c 中终端设备的初始化shell的处理

内存映射的处理

关于内存的处理,lab1目前没有内存管理,只是用起来了虚拟内存,将4MB物理内存映射到原位和高处。即:

  • 0x00000000 至 0x00400000 的物理地址 -> 0x00000000 至 0x00400000 的虚拟地址
  • 0x00000000 至 0x00400000 的物理地址 -> 0xf0000000 至 0xf0400000 的虚拟地址
    毕竟这么大的内存已经足够映射当前内核了。

先来看看怎么映射的

entry.S:加载页表

在 boolloader 阶段,bootmain 最后通过 ((void (*)(void)) (ELFHDR->e_entry))();
将控制转交给了 /kern/entry.S,然后来看看entry.S

mit6.828 - lab1笔记

关于数组 entry_pgdir

entry.S 首先读取了页表 entry_pgdir,这个变量在 /kern/entrypgdir.c 中定义:

pte_t entry_pgtable[NPTENTRIES];  __attribute__((__aligned__(PGSIZE))) pde_t entry_pgdir[NPDENTRIES] = { 	// Map VA's [0, 4MB) to PA's [0, 4MB) 	[0] 		= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P, 	// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB) 	[KERNBASE>>PDXSHIFT] 		= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W };  __attribute__((__aligned__(PGSIZE))) pte_t entry_pgtable[NPTENTRIES] = { 	0x000000 | PTE_P | PTE_W, 	0x001000 | PTE_P | PTE_W, 	0x002000 | PTE_P | PTE_W, 	0x003000 | PTE_P | PTE_W, 	0x004000 | PTE_P | PTE_W, 	0x005000 | PTE_P | PTE_W, 	0x006000 | PTE_P | PTE_W, 	0x007000 | PTE_P | PTE_W, 	0x008000 | PTE_P | PTE_W, 	0x009000 | PTE_P | PTE_W, 	0x00a000 | PTE_P | PTE_W, 	//省略... } 

其中 [0] = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P, 实现了
0x00000000 至 0x00400000 的物理地址 -> 0x00000000 至 0x00400000 的虚拟地址
[KERNBASE>>PDXSHIFT] = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W 实现了
0x00000000 至 0x00400000 的物理地址 -> 0xf0000000 至 0xf0400000 的虚拟地址

关于页表的映射和计算方法,见另一个单独的笔记 "lab1 关于页表的知识"

关于宏 RELOC

从代码中可以看到,在页表加载之前,所有的符号都需要使用宏 RELOC ,其含义是将符号的地址减去 0xF000_0000,即,将虚拟地址转化为真实的物理地址。
这就说明 entry.S 被链接到了 0xF000_0000 上。
通过 objdump -h 来看也确实如此

mit6.828 - lab1笔记

但是对应的makefile是将其指定到 0xf000_0000 上的,可以从 /kern/kernel.ld 中找到

mit6.828 - lab1笔记

关于 bootstack

把目光回到 entry.S 的代码,在代码的最后通过标号 bootstack 和 bootstacktop定义了栈的位置,话说,这里究竟对应的物理地址是哪里呢?

mit6.828 - lab1笔记

可以看到 bootstack 紧邻 .data 段
通过 readelf -s kernel 查看

mit6.828 - lab1笔记

结合 objdump -h kernel

mit6.828 - lab1笔记

确实如此,bootstack 和 .data都位于 0xf010_8000 ,那么物理地址就是 0x0010_8000
栈顶 bootstacktop 的物理地址则是 0x0011_0000
在内存里看呢?

mit6.828 - lab1笔记


init.c:内核初始化

init.c 中最核心的函数是 i386_init

mit6.828 - lab1笔记

关于 清空BSS段

edata[]end[] 是在哪里定义的?这两个变量看起来指的是bss段的开始和结束。
这种问题当然要去看链接脚本了,查看 kern/kernel.ld

mit6.828 - lab1笔记

显示输出的处理

这里涉及的代码有

kern: 	console.h, console.c :涉及终端设备的初始化 	printf.c :涉及printf的实现 lib: 	printfmt.c:支撑printf的实现 	readline.c:实现从终端读取 	string.c:涉及字符串的处理,支撑printf的实现 inc: 	string.h:涉及字符串的处理,支撑printf的实现 
关于 cons_init

这里主要用于初始化终端显示器的硬件设置,其中代码使用汇编,通过in out指令与设备交互,不过多深究了。
mit6.828 - lab1笔记

关于 printf 的实现

printf 的实现这里借大佬的说明图示意:

mit6.828 - lab1笔记

往控制台写字符串,本质还是往物理地址0xB8000开始的显存写数据

jos 的练习提到 printf 的实现需要补充,具体位于 /lib/printfmt.c : vprintfmt 中

mit6.828 - lab1笔记

mit6.828 - lab1笔记

shell的处理

这里涉及的代码有

kern: 	monitor.h, monitor.c :命令的解析、各种命令的实现 
关于monitor的实现

先看看 monitor.h

mit6.828 - lab1笔记

然后看看 monitor.c

mit6.828 - lab1笔记

这么看,只要在 commands[] 中填充 backtrace 的数据就可以补充这个功能了。

mit6.828 - lab1笔记

monitor 是怎么实现的呢?,比较短,直接放代码了

void monitor(struct Trapframe *tf) { 	char *buf;  	cprintf("Welcome to the JOS kernel monitor!n"); 	cprintf("Type 'help' for a list of commands.n");   	while (1) { 		buf = readline("K> "); 		if (buf != NULL) 			if (runcmd(buf, tf) < 0) 				break; 	} } 

本质就是一个循环,打印出 K> 然后接受输入,然后根据输入执行命令。看起来就像是大一C语言课设的XXX管理系统一样。
看看 runcmd 如何实现:

mit6.828 - lab1笔记

挺好,那么现在我们要做的就是实现 backtrace。

堆栈

涉及到的代码:

kern: 	kdebug.h、kdebug.c:涉及Eipdebuginfo和debuginfo_eip的实现 inc: 	stab.h:涉及Stab表的数据结构 	x86.h:涉及读取寄存器的内敛汇编 

这里我们回归到jos的学习任务,研究关于栈帧的处理。并补充一些函数:
/kern/monitor.c:mon_backtrace
/kern/kdebug.c:debuginfo_eip、stab_binsearch

关于backtrace的实现

关于栈帧
栈帧,就是调用函数的时候,处理形参传递和实参存储的数据结构。
在调用函数时,调用者负责传递形参,被调者负责保护现场、恢复现场,最后调用者将形参释放掉。
这之中需要调用者和被调者的约定:
比如 函数列表中的参数,是从右至左的顺序入栈的之类的。

这里继续借用大佬 gatsby123 博客中的图,简单示意,不做深究

mit6.828 - lab1笔记

jos的练习11 让我们完成 mon_backtrace,希望我们将每个栈帧按照这样的格式输出:

Stack backtrace:   ebp f0109e58  eip f0100a62  args 00000001 f0109e80 f0109e98 f0100ed2 00000031   ebp f0109ed8  eip f01000d6  args 00000000 00000000 f0100058 f0109f28 00000061   ... 

不过好在 jos 已经实现了一些函数,供我们调用了,位于 /inc/x86.h
这里提供了一些内联汇编,用于读取各种寄存器的值

mit6.828 - lab1笔记

完成这一步也是很简单啦

mit6.828 - lab1笔记

但是 jos 的练习12上了强度,让我们打印出这样的效果:

mit6.828 - lab1笔记

就是在上面的基础上,显示当前栈帧所在的文件和,以及调用在文件的所在函数的第几行发生。
为了实现这一功能,jos 在kern/kdebug.h 和 kern/kdebug.c 中提供了支持:

mit6.828 - lab1笔记

可以看到 Eipdebuginfo 用于存储当前eip的相关信息。这种功能的背后当然需要编译器的支持,为了方便debug,编译器可以通过stab将这些信息保存下来,

关于stab

按照 exercise12 的提示,通过 kernel.ld 可以看到.stab和 .stabstr 的相关连接选项

mit6.828 - lab1笔记

可以看到其中定义了 __STAB_BEGIN__ __STAB__END__ __STABSTR_BEGIN__ __STABSTR_END__

通过 objdump -h obj/kern/kernel 可以看到 stab 表

mit6.828 - lab1笔记

通过 objdump -G obj/kern/kernel 可以看到stab的内容

mit6.828 - lab1笔记

其中包含1213项,每项包括

symnum:序号 n_type:类型 n_othor:杂项信息 n_desc:描述信息 n_value:表示地址。特别要注意的是,这里只有FUN类型的符号的地址是绝对地址,SLINE符号的地址是偏移量, n_strx:stabstr表中对应的字符串的序号 string:stabstr表中对应的字符串 

在 stab.h中有对应的数据结构:

mit6.828 - lab1笔记
那么这些信息要怎么使用呢,看看kdebug.c

 stab_binsearch(stabs, region_left, region_right, type, addr)  某些符号表项类型按指令地址递增顺序排列。 例如,标记函数的 N_FUN 符号表项(n_type == N_FUN 的符号表项)和标记源文件的 N_SO 符号表项。  给定指令地址后,该函数会查找包含该地址的 "type "类型的符号表项。  搜索范围为[*region_left, *region_right]。  因此,要搜索一整套 N 个符号表项,可以执行以下操作  // left = 0; // right = N - 1; /* 最右边的符号表项 */ // stab_binsearch(stabs, &left, &right, type, addr);  搜索会修改 *region_left 和 *region_right 以括住 "addr"。 *region_left 指向包含'addr'的匹配符号表项,*region_right 指向下一个符号表项之前。 如果 *region_left > *region_right,则表示 "addr "不包含在任何匹配的符号表项中。  // 例如,给定这些 N_SO 符号表项: // 索引类型 地址 // 0 SO f0100000 // 13 SO f0100040 // 117 SO f0100176 // 118 SO f0100178 // 555 SO f0100652 // 556 SO f0100654 // 657 SO f0100849 // 此代码: // left = 0, right = 657; // stab_binsearch(stabs, &left, &right, N_SO, 0xf0100184); // 将退出设置 left = 118, right = 554. 

这里给出了 stab_binsearch 的使用说明,从函数名可以看出来他是使用二分查找算法从stab中查找addr指定的type类型的符号,然后通过left返回出来。来简单看看代码:

mit6.828 - lab1笔记

然后来看看要处理的 debuginfo_eip

mit6.828 - lab1笔记

mit6.828 - lab1笔记

到现在为止,已经找到了所在文件名、所在函数名、所在函数地址、所在函数名长度、相对函数的偏移
就差所在行号了,找行号的代码很好写啊,照着写就行了,这个函数调用,将范围改一下,然后类型改成代码段的行就行了,因为eip只会在代码段里移动。

stab_binsearch(stabs, &lline, &rline, N_SLINE, addr); 

但是,行号究竟是stab中的哪个成员提供的啊?
mit6.828 - lab1笔记

观察一波 objdump -G 的输出

mit6.828 - lab1笔记

目测 n_value对应的是SLINE的内存地址,而n_desc看着更像行号一些,于是:

mit6.828 - lab1笔记

补充一下 monitor.c

mit6.828 - lab1笔记

编译测试一下:

mit6.828 - lab1笔记

看着好像成功了,试试评分

mit6.828 - lab1笔记

收工。