HumanCore - CP/M-17 Version 1.0, Date: January 08, 1996
实验准备
首先,建议使用32位ubuntu虚拟机作为实验环境,不建议自己配置,因为会遇到各种坑。我们的主要目的是学习操作系统,不是安装各种环境及工具,下载一个32位的ubuntu桌面镜像,然后配置一个虚拟机即可。我这里使用的是16.04版本的32位ubuntu镜像,使用的虚拟机是vmware。需要的qemu等软件可以在如下网站中进行下载,此处wo’shi’yong
软件安装
编译工具链
输入如下命令
1 | % objdump -i |
The second line should say elf32-i386
.
1 | % gcc -m32 -print-libgcc-file-name |
The command should print something like /usr/lib/gcc/i486-linux-gnu/*version*/libgcc.a
or /usr/lib/gcc/x86_64-linux-gnu/*version*/32/libgcc.a
If both these commands succeed, you’re all set, and don’t need to compile your own toolchain.
If the gcc command fails, you may need to install a development environment. On Ubuntu Linux, try this:
1 | % sudo apt-get install -y build-essential gdb |
On 64-bit machines, you may need to install a 32-bit support library. The symptom is that linking fails with error messages like “__udivdi3
not found” and “__muldi3
not found”. On Ubuntu Linux, try this to fix the problem:
1 | % sudo apt-get install gcc-multilib |
git
请使用git获取实验源文件
1 | mkdir ~/6.828 |
使用git diff origin/lab1
对于代码的改变进行跟踪
qemu
本实验使用qemu作为虚拟机,因此需要安装qemu,archlinux下安装qemu的方法很简单,需要注意的是,安装完成后,需要编辑lab/conf/env.mk
文件,将
1 | QEMU |
改为
1 | QEMU=/usr/bin/qemu-system-i386 |
这样就为QEMU设置了路径。如果没有qemu-system-i386,请到这个网站下载。这里必须使用i386,否则会出现gdb和qemu体系结构不匹配的问题。
作业上传
接下来去这个网页,用自己的邮箱注册一个账号,这样就可以上传作业并进行批改。以后只需要在lab文件中执行make handin,即可提交作业,下面的XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX改为自己邮箱注册后的API
1 | make handin |
作业批改
运行下面的命令进行作业批改
1 | make grade |
需要注意的是,要将./grade-lab1
的权限修改为可运行,此外,如果是linux系统,可能会出现下面的错误:
1 | /usr/bin/env: "python\r": 没有那个文件或目录 |
这个是因为UNIX和Linux上的字符集的差异所造成的,解决方法是vim grade-lab1
,然后输入:set ff=unix
,再保存即可。
第一部分 PC Bootstrap
x86汇编
这里请参考应用于NASM 编译器的汇编教程和Intel/AT&T汇编语法转换。本实验所用的汇编语言是AT&T 语法
x86模拟
我们使用qemu虚拟器对我们所编写的操作系统进行模拟,进入lab后,运行make
,对操作系统进行编译:
1 | make |
然后我们就获得了一个镜像文件kernel.img
,运行make qemu
即可在qemu虚拟机下,运行我们的操作系统,运行成功的结果如下:
1 | Booting from Hard Disk... |
目前这个操作系统提供了两个命令:
1 | help |
机器的物理地址空间
一个计算机(32位)的物理地址空间布局基本如下:
1 |
|
早期的计算机内存很小,可能只有64KB的RAM。从0x000A000到0x000FFFFF这一段被用于硬件用途,例如视频播放缓存等。最重要的是BIOS所在的内存,占据了0x000F0000到0x000FFFFF的64KB空间。早期的BIOS是写死的,现在的计算机BIOS都位于可刷新的闪存区域。关于BIOS的讲解详见。
BIOS
启动过程调试
至此,我们终于可以使用qemu启动我们的程序,并观察启动的过程。在lab文件夹下,执行
1 | make qemu-gdb |
即可令qemu运行在调试模式下,此时qemu作为一个虚拟远程机进入调试模式,打开另一个终端,输入make gdb
,即可看到下面的内容
1 | Type "apropos word" to search for commands related to "word". |
这里又遇到了一个坑,gdb反汇编的结果是有问题的,按照16位汇编的结果,应当CS=0xf000,而IP=0xe05b,但是这里gdb是用32位的模式进行的反汇编,结果就是CS=0x3630,而IP=$0xf000e05b,这不是我们想要的,解决方法如下:
- 在lab文件夹下执行如下命令:
1 | echo '<?xml version="1.0"?><!DOCTYPE target SYSTEM "gdb-target.dtd"><target><architecture>i8086</architecture><xi:include href="i386-32bit.xml"/></target>' > target.xml |
- 然后修改
gdbinit.tmpl
文件,将
1 | 12 if $lastcs == -1 || $lastcs == 8 || $lastcs == 27 |
修改为
1 | 12 if $lastcs == -1 || $lastcs == 8 || $lastcs == 27 |
- 重新make
1 | make clean |
将add-auto-load-safe-path /home/duan/Code/linux/6.828/xv6-public/.gdbinit
添加至/home/user/.gdbinit
文件夹下(如果没有就新建一个),再次执行make gdb
,我们得到了下面的结果:
1 | [f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b |
这次的结果就很完美,是我们想要的,解释一下几个参数
- 系统开始执行的物理地址为[CS:IP]=[f000:fff0],这个区域位于内存的BIOS区域上部,实际物理地址为16*CS+IP=0xffff0(实模式寻址)
- 第一条指令为一个ljmp指令,跳转至CS=0xf000, IP=0xe05b
这样启动后系统进入了BIOS区,可以确保上电后由BIOS接管机器,此时CPU处在实模式(所谓实模式,就是其地址采用真实的物理地址)。0xffff0是BIOS末尾(0x100000)的前16位,所以机器一上电,位于0xffff0,然后赶紧往回跳转,跳到0xfe05b,毕竟16byte什么都做不了。这一段代码是qemu内置的BIOS启动代码,和课程的代码无关,只需要了解他执行了一些上电检查,然后就将控制权交给了Boot loader段。我们只需要知道我们写的boot loader段起始位置在0x7c00即可(关于0x7c00这个魔数请参考9)。
练习:使用gdb si多运行几行命令,猜测命令的作用
在这里我一共运行了五次si,分别记录下了命令和相应的结果
第一次si
第一次si得到的命令为
1 | [f000:xe05b] 0xfe05b: cmpl $0x0, %cs:0x6308 # 此条命令占据7byte |
这是一个长比较命令,对立即数0x0和f000:6308这个地址上的数进行了比较,推测是0x6308上有一些设置需要进行判断。但是具体是什么并不清楚。
第二次si
第二次si得到的命令为
1 | [f000:e062] 0xfe062: jne 0xfd0a2 # 此条命令占据4byte |
如果不相等,就跳转至0xfd0a2,结合第一次si的结果可知,如果cs:0x6308保存的值不为0,那么跳转至0xfd0a2。
第三次si
第三次si得到的命令为
1 | [f000:e066] 0xfe066: xor %dx, %dx # 此条命令占据2byte |
根据命令的地址,我们知道cs:0x6308保存的值确实为1,命令顺序执行,这里进行一个异或操作,清空dx寄存器。
第四次si
第四次si得到的命令为
1 | [f000:e068] 0xfe068: mov %dx, %ss |
将dx寄存器中的值保存至ss寄存器。就是说堆栈段基地址为0x0000
第五次si
第五次si得到的命令为
1 | [f000:e06a] 0xfe06a: mov 0x7000, %esp |
将0x7000保存至esp寄存器,说明堆栈段的地址为0x07000。可以看到,操作系统一开始的工作是进行了一些对内存的分段的操作。
Boot Loader
BIOS完成后,我们要进行Boot Loader,Boot Loader代码位于硬盘上,而硬盘扇区大小为512字节(现在也有4K的,能够加载更大的boot loader),Boot Loader程序就位于硬盘最初的512B上,我们要将整个扇区加载到内存物理地址为0x7c00到0x7dff所在的位置,然后使用jmp命令跳转至CS:IP = [0000:7c00],将控制权移交给Boot Loader。boot loader的程序为boot/boot.S
和boot/main.c
,本节的主要工作就是阅读并理解这两个程序。boot loader主要完成两个功能:
- 一:从实模式切换至32位保护模式,这样软件才能访问处理器的全部物理地址空间,关于保护模式请参考1.2.7和1.2.8节
- 二:boot loader通过x86的特殊io机制,直接访问硬盘设备的寄存器,读取硬盘上的内核
下面给出boot/boot.S
和boot/main.c
的算法,其中,关于A20的控制请参考6。
1 | #include <inc/mmu.h> |
1 | //根据 boot/Makefrag, main.o被加载到 |
练习:使用gdb从boot loader开始调试
跳转至boot loader开始位置
在0x7c00处设置断点,然后执行,停止在0x7c00处
1 | (gdb) br *0x7c00 |
停留的位置即为boot.S
开始处,使用x/N addr进行反汇编,查看此处的代码,反汇编结果如下:
1 | (gdb) x/10 0x7c00 |
可以看到确实是在boot.S开始位置。
研究readsect,弄清楚每一句语句的作用
1 | // 下面是readsect 的具体实现,可以先不看 |
使用断点调试进入bootmain
单步调试执行到call bootmain
后,gdb显示如下:
1 | 0x7c45: call 0x7d19 |
说明bootmain的起始地址是0x7d19,继续执行,后续的几行代码为:
1 | => 0x7d19: push %ebp # 保存堆栈基址,ESP自动减小,栈向下生长 |
在boot main中,会循环读取内核,当内核读完后,程序会去哪里?
对boot main进行反编译,得到如下结果:
1 | 00007d19 <bootmain>: |
从上面的代码中我们能看出,当读取完成后,bootmain会执行(ELFHDR->e_entry))();
在汇编中这句话是调用0x10018这个内存位置上保存的函数,最后执行后代码会转到10000c上。
问题:这里我在调试过程中遇到一个问题,就是没有调试信息,使用GDB打印10000处内存提示没有”Elf”符号,说明符号表没有加载。
练习题
处理器是何时开始执行32位代码的,是什么使得处理器完成了从16位到32位代码的切换?
从下面这句代码开始执行32位代码,
1 | ljmp $PROT_MODE_CSEG, $protcseg |
下面这段代码完成了从16位到32位的切换:
1 | lgdt gdtdesc |
boot loader 最后一句执行的语句是什么?
是((void (*)(void)) (ELFHDR->e_entry))();
,对应的汇编代码为:7d63: ff 15 18 00 01 00 call *0x10018
加载内核后的第一句命令是什么?
是f010000c: 66 c7 05 72 04 00 00 movw $0x1234,0x472
,对应的代码在entry.S
中
内核的第一句指令在内存什么位置?
在0x0010000c
boot loader如何决定从硬盘中读取多少个sector来获得整个内核?他是从哪里得到这个信息的?
boot loader通过ELF头读取相关信息并加载所有的程序头
1 | ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff); |
加载内核
ELF format
在了解内核工作原理前,我们先要明白ELF格式,ELF全称为”Executable and Linkable Format”即可执行与可链接文件格式,关于这个格式的详细描述请参考ELF定义或下面的阅读材料,其中很大一部分工作是支持动态链接库。一个ELF文件的结构如下所示:
对于本课程,我们可以简单地将ELF理解为一个包含了一些载入信息的数据头和一些程序片段所组成的文件,每一个程序片段包含将要被载入到指定内存的数据和代码,Boot loader只是载入这些片段,并不对其进行任何修改。现在我们对产生的ELF格式的kernel进行分析,执行下列语句objdump -h obj/kern/kernel
,得到结果如下:
1 | objdump -h kernel |
可以看到不止6个段,有一些是和debug相关的,不必理会。我们这里重点关注一下.text段的VMA(link address)和LMA(load address)。load address就是某个片段实际被加载到内存当中的地址。而link address是指某个片段将要被执行时所在内存中的位置,可以简单地理解为:
- LMA:程序加载位置
- VMA:程序执行位置
一般来说,程序在哪里加载,就在哪里执行,所以一般情况下VMA=LMA,例如,对于我们的boot loader
1 | objdump -h boot.out |
可以看到VMA = LMA。使用objdump显示kernel文件的程序头,结果如下:
1 | objdump -x kernel |
LOAD就是要被加载到内存中的信息,其他信息还包括虚拟地址(“vaddr”),物理地址(“paddr”),加载空间的大小(“filesz”和”memsz”)。回到我们上面的boot/main.c
,ph->p_pa
域包含了每个段的目标物理地址。BIOS将boot的片段加载到0x7c00所在的内存位置,并从这个位置开始执行。我们可以在boot/Makefrag
文件下修改链接的地址,打开Makefrag文件,其内容如下:
1 | ... |
我们可以看到,链接地址为-Ttext 0x7c00,这个地址是BIOS指定的,我们可以修改这个地址,从而跳过一些特定的指令,来观察boot loader 的表现。
练习:调整boot loader link address的位置,观察现象
通过修改boot/Makefrag
下链接地址,我们可以修改boot loader加载与运行地址,我们将地址前移,观察跳过一些语句后boot loader 会发生什么。将-Ttext 0x7C00
改为-Ttext 0x7BFC
,重新make,没有问题,objdump -h boot.out
,发现该程序的信息如下:
1 | 节: |
可以看到VMA和LMA此时都变成了00007bfc。
直观表现
我总结了一下修改link address后boot loader的直观表现,其中有一些比较奇怪,这里总结一下:
- 对于反汇编后得到的文件
/obj/boot/boot.asm
,起始地址确实是变了,变为
1 | 00007bfc <start>: |
这一点是符合预期的。
- 进行gdb调试,设置断点位置为0x7c00,原本以为0x7c00所在位置的语句会从原先的
cli
变为mov
,但是奇怪的是并没有,x/10
得到的结果如下:
1 | Breakpoint 1, 0x00007c00 in ?? () |
可以看到,0x7c00依然是cli,这一点令我非常疑惑。目前还没有比较好的答案(先跳过不管了),经过我实验,将地址前移或者后移后,均无法正确加载bootloader程序。每次都会在ljmp $0x8,$0x7c32
这条语句处发生跳转,重新回到BIOS第一条语句。
在ELF头中,除了section的信息,还有一个比较重要的是e_entry信息,表示程序的入口地址,我们可以使用objdump -f
查看
1 | objdump -f obj/kern/kernel |
所以我们现在基本摸清了ELF loader的套路,将kernel中的每个section导入内存中,然后跳转至程序入口。
练习6:观察0x00100000内存
分别在BIOS进入boot loader和boot loader进入kernel的位置设置断点,然后运行至断点处,此时使用x/8x打印0x00100000开始的8个word的内存并观察,说明两者有何不同
在BIOS进入boot loader处,0x00100000内存中内容为:
1 | [ 0:7c01] => 0x7c01: cld |
空的,然后在boot loader进入kernel的位置(0x0010000c)设置断点,再次访问0x00100000,其中的内容变为:
1 | (gdb) x/8x 0x00100000 |
说明内核被加载到了0x00100000这个位置,或者说有一部分在这个位置。实际上这里存储了内核的代码。
内核
虚拟内存初步建立
了解了内核引导程序的原理,现在我们正式开始接触内核。在boot loader中,link address和load address是一致的,但是在内核程序中则不是这样,关于内核的链接地址详见/kern/kernel.ld
1 | SECTIONS |
在这个链接文件中,一开头就明确指出,LMA为0x100000,而VMA即链接地址为0xF0100000。说明内核在0x100000处加载,在0xF0100000处执行。但一些计算机可能根本没有0xF0100000这么大位置的内存,所以这里引入了虚拟内存的概念。将虚拟内存0xf0100000映射至0x00100000,所以内核存储在0x00100000的物理地址,然后在0xf0100000的虚拟地址处执行。低位的虚拟内存就留给用户使用。内存映射表详见kern/entrypgdir.c
,这里只映射了4MB的地址。注意,这里实际就是将内核的一部分进行了虚拟化,建立了虚拟地址到物理地址的映射。当完成实验2和实验3后,再回顾这里会有更深的体会。
我们现在看一下entrypgdir.c
中的内容:
1 |
|
这个文件将4MB
大小的物理空间[0,4MB]
映射至[KERNBASE, KERNBASE+4MB]
,启动阶段完全够用了,4MB就是一个页表能容纳的容量。后续的虚拟空间到物理空间的映射我们会在内核代码中完成。这部分内存是内核可写的。映射完成后,页目录的第0项与第KERNBASE >> PDXSHIFT
项均指向页表entry_pgtable
。从这里我们可以看到,页目录每一项指向的页表为4MB,而页目录共1024项,总可映射内存为4GB
练习7:实模式和保护模式下的内存
进入内核然后停留在mov %eax, %cr0
这条语句处,验证此时0x00100000
和0xf0100000
两个内存位置处的内存,然后si
执行mov %eax, %cr0
,再次验证这两个位置的内存,有什么现象?
mov %eax, %cr0
的作用是开启地址的映射,将高位地址映射至低位地址上,所以没有执行这句语句前,0xf0100000
处的内存是不存在的,故显示
1 | 0xf0100000 <_start-268435468>: Cannot access memory at address 0xf0100000 |
而0x00100000
处则存储了内核的代码。当执行mov %eax, %cr0
后,开启了地址映射,会发现0xf0100000
被映射至了0x00100000
处,两者存放的内容相同。
1 | (gdb) x/10 0xf0100000 |
控制台格式化输出
阅读kern/printf.c、lib/printfmt.c以及kern/console.c三个文件,弄清三者之间的关系。我们先针对控制台程序进行整理,对代码的详细分析参考下面的链接
而printfmt是带有格式的输出,printf是printfmt的封装,底层调用的是console中的cputchar,最后留给上层的系统调用为cprintf,所以三个文件的关系如下:
graph BT node1["console"] node2["printfmt"] node3["printf"] node1-->node2 node2-->node3
这也解释了为何printfmt被放在了lib中,因为它是作为库被调用的,而不是直接给用户的系统调用。其中输出八进制的代码被省略了,补充完成后如下:
1 | case 'o': |
然后在monitor.c里加一句:
1 | cprintf("%u decimal is %o octal!\n", num,num); // num is 6828 |
然后执行make grade即可,显示printf: OK,说明代码没有问题。
1 | running JOS: (0.7s) |
回答下面的问题:
解释printf.c和console.c之间的关系,console.c开放了哪些函数,这些函数是如何被printf.c使用的?
观察console.c,可以看到下列函数之前没有加static,即为开放函数:
1 | void serial_intr(void) // 串口中断 |
而在pringf中,使用到的函数为cputchar
,其函数及内部调用的定义如下:
1 | void |
解释下面的console.c中的代码片段
1 | // crt_pos uint16_t 光标位置 |
这段代码出现在cga_putc
中,我们先要明确其中变量、宏定义以及魔数的作用。详细注释见上方代码,总之其功能就是在CGA设备写满了内容后,将内容上滚,并产生新行,在qemu仿真中,终端的窗口大小就是25*80。
单步调试下面的代码:
1
2
3 > int x = 1, y = 3, z = 4;
> cprintf("x %d, y %x, z %d\n", x, y, z);
>
>
在调用cprintf()的过程中,fmt指向哪里?ap指向哪里?
列出每一次对
cons_putc
,va_arg
以及vcprintf
的调用,对于cons_putc
,列出所有的参数;对于va_arg
,列出调用前后ap指针指向的内容,对于vcprintf
,列出两个参数的值。
栈
练习8:栈初始化
阅读内核代码,指出栈是在哪里初始化的?位于哪个内存?如何预留栈空间?栈指针指向哪里?
在entry.S
中,栈初始化语句如下:
1 | movl $(stack + KSTACKSIZE), %esp |
栈大小为KSTACKSIZE,位于data段中,预留的方式是通过.comm
命令对栈空间进行分配,esp指针指向 stack + KSTACKSIZE处。
练习9:test_backtrace函数
找到test_backtrace函数,然后设置断点,观察每次调用这个函数时会发生什么
使用nm命令在kernel中找到test_backtrace,其位置为0xf0100040,在此处设置一个断点,然后执行至此处,可以发现这个函数是一个递归函数,源码如下:
1 | void |
每一次递归调用,在函数栈上增加一层,例如调用test_backtrace(5)并使用gdb调试得到结果如下:
1 | (gdb) bt |
每次调用后会有以下几个push语句:
1 | push %ebp # 保存栈底 |
为了更细致地分析函数调用栈的行为,我们进一步使用gdb进行分析,首先我们分析栈指针%ebp的情况:
栈指针%ebp的情况
在我们进入到test_backtrace函数前,即在enter.S
中,我们能够看到这样一行代码:
1 | # Clear the frame pointer register (EBP) # so that once we get into debugging C code, # stack backtraces will be terminated properly. |
将栈基底设置为0x0,然后将栈顶指针设置为bootstacktop,然后下面一句开始调用i386_init函数,将eip压栈,则f010effc保存着返回地址eip的值,为0xf010003e
,进入函数后,首先要进行栈分配,保存前一栈帧的栈底,即push %ebp
,故0xf010eff8
保存的内容为0x00000000
,即最开始栈的栈底,然后执行mov %esp, %ebp
,所以从这里我们得到一个关键信息:ebp保存着前一个调用栈的栈底ebp’,用数学语言描述如下:
即第$n$个ebp的值为第$n-1$个栈的基地址,而初始的$\%ebp_0$即为第一个站的栈底。所以我们的backtrace结束条件可以写为
1 | if(get_ebp() == 0x0) |
%eip的情况
现在我们来分析另一个重要的寄存器eip的情况,因为函数返回后要返回到恰当的位置继续执行,所以eip的值也要被妥善保存。由于我们在使用call
命令时先将eip进行了push,所以实际上eip先于ebp被保存,在栈中的保存位置如下:
1 | 0xf010effc: 0xf010003e (value of ret eip) //栈是向下增长的 |
输入参数的情况
在调用函数之前,还有一个非常关键的步骤就是给函数提供输入参数,而在实际调试过程中,我们可以发现函数调用时顺序如下:
先
push
参数,从后往前(如果只有一个参数,也可能直接mov $value, %esp
,这个看具体的实现过程)再调用
call
,将返回位置所在的eip
压栈- 调用
call
后,第一条语句就是push %ebp
,将前一个函数的栈底进行保存
至此,我们可以画出一个函数void func(arg1, arg2, ... , argn)
调用过程的栈模型如下:
在实际调试过程中,backtrace的模型确实如此。
练习10:实施一个mon_backtrace函数
在了解了上面的函数调用过程后,我们来实施一个backtrace函数,这个函数能够根据函数的调用深度,打印调用栈中的一些关键信息,包括:当前函数的栈底、当前函数的返回地址以及当前函数的参数,格式如下:
1 | Stack backtrace: |
这个函数位于kern/monitor.c中,写完后记得将其和命令挂钩,这样用户就能通过这个命令查看函数调用栈的情况。
1 | int __attribute__((optimize("O0"))) |
这个函数需要注意以下几点:
- 因为保存的都是32位地址,所以我们采用了unsigned int*类型定义了一个ebp指针
- read_ebp()得到的是ebp寄存器中的内容,即被调用函数的栈基址,我们需要对其进行解引用操作,*ebp为调用者的栈基址
- 使用ebp[1]表示*(ebp+1),注意地址的加法和数的加法是不一样的
- ebp[2]-ebp[6]分别表示五个变量
- _attribute_((optimize(“O0”)))表示关闭优化,因为编译器是有可能先执行read_ebp()函数,再调用mon_backtrace,但是在我这里不可能,因为在read_ebp之前调用了cprintf,所以不会出错,当然保险起见还是加上禁止优化(虽然这么写可能不太规范,还是应该在函数一开始就定义变量)
这个函数目前还是比较简陋的,它只能打印出内存中的内容,不能打印返回函数所在的文件、行数;同时打印的参数个数也是固定的,为5个,因此我们需要对其进行一些修改,使它能够打印更加丰富的信息。
练习11:改进的mon_backtrace
问题描述
我们现在要将mon_backtrace进行改进,使其可以输出更多的信息,包括函数所在文件、行数等。
探究符号表及其载入
这里提供了一个debuginfo_eip()
,可以根据eip
找到对应的符号表,eip的debuginfo定义如下:
1 | // Debug information about a particular instruction pointer |
debuginfo_eip
比较长,总之就是把给定addr
处的指令进行解析,然后填充到info
,成功返回0,否则返回负数。阅读debuginfo_eip
后,回答下面的问题:
在debuginfoeip函数中用到了\_STAB__*(_STAB_BEGIN\_等,表示符号表保存的位置等信息),这个东西是哪里来的?
为了解决这个问题,我们需要做如下几件事:
- 阅读
kern/kernel.ld
,找到_STAB_*(说明符号表是在链接阶段产生的),这里需要对link script的基本写法有一些了解。
在kern/kernel.ld
中:
1 | .stab : { |
- 运行
objdump -h obj/kern/kernel
,结果如下
1 | Sections: |
从中可以看到.stab
的分配情况。
- 运行
objdump -G obj/kern/kernel
,结果如下
1 | obj/kern/kernel: file format elf32-i386 |
结果很长,总之这里记录的是stab段中的内容,给出了符号和对应的地址。
- 运行下列命令
1 | gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c |
然后查看init.S
,可以看到一段非常冗长的代码:
1 | .file "init.c" |
结合这段代码中的.stab*
段以及上面的objdump -G obj/kern/kernel
的输出结果,我们可以看到init.s
中的.stab
段已经被分配了具体的内存,同理其他的段和指令也会被分配相应的内存。也就是说我们的符号表被载入到了内存当中。
- 看看bootloader工作时会不会将符号表载入内存中
这里没搞明白,还有以下几个问题:
- 符号表是何时载入的?
- 载入到了哪里
还需要对符号表进行更深入的了解。总之这里我们知道,kernel加载过程中符号表也被载入,根据符号表,我们能够解析出eip
保存的指令所在文件、行号、参数等相关信息。现在我们来对mon_backtrace
进行改进。
改进mon_backtrace
首先,我们需要改进kern/kdebug.c
中的debuginfo_eip
函数,添加行号搜索功能,根据提示添加如下:
1 | // Search within [lline, rline] for the line number stab. |
然后修改mon_backtrace
,调用debuginfo_eip
并按照格式输出相关信息。修改后的mon_backtrace
如下:
1 | mon_backtrace(int argc, char **argv, struct Trapframe *tf) |
至此,Lab1的内容就全部完成了。
阅读材料
Selection of Operating System Papers
Available on the 6.828 schedule.
UNIX
- Youtube Unix intro
- The UNIX Time-Sharing System, Dennis M. Ritchie and Ken L.Thompson,. Bell System Technical Journal 57, number 6, part 2 (July-August 1978) pages 1905-1930. (local copy) You read this paper in 6.033.
- The Evolution of the Unix Time-sharing System, Dennis M. Ritchie, 1979.
- The C programming language (second edition) by Kernighan and Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8, 1998.
x86 Emulation
QEMU
- A fast and popular x86 platform and CPU emulator.
Bochs
- A more mature, but quirkier and much slower x86 emulator. Bochs is generally a more faithful emulator of real hardware than QEMU.
x86 Assembly Language
PC Assembly Language, Paul A. Carter, November 2003. (local copy)
Intel 80386 Programmer’s Reference Manual, 1987 (HTML). (local copy - PDF) (local copy - HTML)
Much shorter than the full current Intel Architecture manuals below, but describes all processor features used in 6.828.Intel 64 and IA-32 Intel Architecture Software Developer’s Manuals
, Intel, 2007. Local copies:
Multiprocessor references:
AMD64 Architecture Programmer’s Manual.
Covers both the “classic” 32-bit x86 architecture and the new 64-bit extensions supported by the latest AMD and Intel processors.Writing inline assembly language with GCC:
- Brennan’s Guide to Inline Assembly, Brennan “Mr. Wacko” Underwood
- Inline assembly for x86 in Linux, Bharata B. Rao, IBM
- GCC-Inline-Assembly-HOWTO, Sandeep.S
Loading x86 executables in the ELF format:
- Tool Interface Standard (TIS) Executable and Linking Format (ELF).
The definitive standard for the ELF format. - Wikipedia page has a short description.
- Tool Interface Standard (TIS) Executable and Linking Format (ELF).
PC Hardware Programming
General PC architecture information
- Phil Storrs PC Hardware book, Phil Storrs, December 1998.
- Bochs technical hardware specifications directory.
General BIOS and PC bootstrap
- BIOS Services and Software Interrupts, Roger Morgan, 1997.
- “El Torito” Bootable CD-ROM Format Specification, Phoenix/IBM, January 1995.
VGA display -
kern/console.c
- VESA BIOS Extension (VBE) 3.0, Video Electronics Standards Association, September 1998. (local copy)
- VGADOC, Finn Thøgersen, 2000. (local copy - text) (local copy - ZIP)
- Free VGA Project, J.D. Neal, 1998.
Keyboard and Mouse -
kern/console.c
8253/8254 Programmable Interval Timer (PIT) -
inc/timerreg.h
- 82C54 CHMOS Programmable Interval Timer, Intel, October 1994. (local copy)
- Data Solutions 8253/8254 Tutorial, Data Solutions.
8259/8259A Programmable Interrupt Controller (PIC) -
kern/picirq.*
- 8259A Programmable Interrupt Controller, Intel, December 1988.
Real-Time Clock (RTC) -
kern/kclock.*
Phil Storrs PC Hardware book
, Phil Storrs, December 1998. In particular:
CMOS Memory Map, Padgett Peterson, May 1996.
M48T86 PC Real-Time Clock, ST Microelectronics, April 2004. (local copy)
16550 UART Serial Port -
kern/console.c
- PC16550D Universal Asynchronous Receiver/Transmitter with FIFOs, National Semiconductor, 1995.
- Technical Data on 16550, Byterunner Technologies.
- Interfacing the Serial / RS232 Port, Craig Peacock, August 2001.
IEEE 1284 Parallel Port -
kern/console.c
- Parallel Port Central, Jan Axelson.
- Parallel Port Background, Warp Nine Engineering.
- IEEE 1284 - Updating the PC Parallel Port, National Instruments.
- Interfacing the Standard Parallel Port, Craig Peacock, August 2001.
IDE hard drive controller -
fs/ide.c
- AT Attachment with Packet Interface - 6 (working draft), ANSI, December 2001.
- Programming Interface for Bus Master IDE Controller, Brad Hosler, Intel, May 1994.
- The Guide to ATA/ATAPI documentation, Constantine Sapuntzakis, January 2002.
Sound cards (not supported in 6.828 kernel, but you’re welcome to do it as a challenge problem!)
- Sound Blaster Series Hardware Programming Guide, Creative Technology, 1996.
- 8237A High Performance Programmable DMA Controller, Intel, September 1993.
- Sound Blaster 16 Programming Document, Ethan Brodsky, June 1997.
- Sound Programming, Inverse Reality.
E100 Network Interface Card
E1000 Network Interface Card
参考文献
- 1.MIT6828课程视频链接 ↩
- 2.Lab1介绍 ↩
- 3.6828课程表 ↩
- 4.6828课程材料 ↩
- 5.8086端口说明 ↩
- 6.关于 A20的详细介绍 ↩
- 7.x86端口映射表 [^ 8]:111qqz做的实验 [^ 9]:0x7c00这个魔数的含义 ↩