From 5346cc90031a8e2f2191f961492de011bb0d24b3 Mon Sep 17 00:00:00 2001 From: chyyuu Date: Sat, 25 Oct 2014 19:53:28 +0800 Subject: [PATCH] add challenge 2 in lab1 --- README.md | 2 +- lab0/lab0_2_6_2_1_linked_list.md | 23 +++++----- lab1.md | 2 +- lab1/lab1_2_1_exercise.md | 73 ++++++++++++++++++++----------- lab1/lab1_3_3_3_lab1_interrupt.md | 2 +- lab1/lab1_4_lab_requirement.md | 14 +++--- 6 files changed, 69 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 5f230e6..e32c468 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,4 @@ # 维护者 - yuchen AT tsinghua.edu.cn - objectkuan AT gmail.com -- xuyongjiande AT gmail.com \ No newline at end of file +- xuyongjiande AT gmail.com diff --git a/lab0/lab0_2_6_2_1_linked_list.md b/lab0/lab0_2_6_2_1_linked_list.md index c5daaed..76b10a8 100644 --- a/lab0/lab0_2_6_2_1_linked_list.md +++ b/lab0/lab0_2_6_2_1_linked_list.md @@ -1,4 +1,3 @@ - ##### 2.6.2.1 双向循环链表 在“数据结构”课程中,如果创建某种数据结构的双循环链表,通常采用的办法是在这个数据结构的类型定义中有专门的成员变量 data, 并且加入两个指向该类型的指针next和prev。例如: @@ -12,7 +11,7 @@ 双向循环链表的特点是尾节点的后继指向首节点,且从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个节点的data数据。由双向循环列表形成的数据链如下所示: ![双向循环链表](../lab0_figs/image007.png "双向循环链表") - + 这种双向循环链表数据结构的一个潜在问题是,虽然链表的基本操作是一致的,但由于每种特定数据结构的类型不一致,需要为每种特定数据结构类型定义针对这个数据结构的特定链表插入、删除等各种操作,会导致代码冗余。 在uCore内核(从lab2开始)中使用了大量的双向循环链表结构来组织数据,包括空闲内存块列表、内存页链表、进程列表、设备链表、文件系统列表等的数据组织(在[labX/libs/list.h]实现),但其具体实现借鉴了Linux内核的双向循环链表实现,与“数据结构”课中的链表数据结构不太一样。下面将介绍这一数据结构的设计与操作函数。 @@ -31,7 +30,7 @@ uCore的双向链表结构定义为: } free_area_t; 而每一个空闲块链表节点定义(位于lab2/kern/mm/memlayout)为: - + /* * * struct Page - Page descriptor structures. Each Page describes one * physical page. In kern/mm/pmm.h, you can find lots of useful functions @@ -49,7 +48,7 @@ uCore的双向链表结构定义为: 图 空闲块双向循环链表 从上图中我们可以看到,这种通用的双向循环链表结构避免了为每个特定数据结构类型定义针对这个数据结构的特定链表的麻烦,而可以让所有的特定数据结构共享通用的链表操作函数。在实现对空闲块链表的管理过程(参见lab2/kern/mm/default_pmm.c)中,就大量使用了通用的链表插入,链表删除等操作函数。有关这些链表操作函数的定义如下。 - + (1) 初始化 uCore只定义了链表节点,并没有专门定义链表头,那么一个双向循环链表是如何建立起来的呢?让我们来看看list_init这个内联函数(inline funciton): @@ -60,7 +59,7 @@ uCore只定义了链表节点,并没有专门定义链表头,那么一个双 } 参看文件default_pmm.c的函数default_init,当我们调用list_init(&(free_area.free_list))时,就声明一个名为free_area.free_list的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个表示空闲内存块链的空链表。而且我们可以用头指针的next是否指向自己来判断此链表是否为空,而这就是内联函数list_empty的实现。 - + (2) 插入 对链表的插入有两种操作,即在表头插入(list_add_after)或在表尾插入(list_add_before)。因为双向循环链表的链表头的next、prev分别指向链表中的第一个和最后一个节点,所以,list_add_after和list_add_before的实现区别并不大,实际上uCore分别用__list_add(elm, listelm, listelm->next)和__list_add(elm, listelm->prev, listelm)来实现在表头插入和在表尾插入。而__list_add的实现如下: @@ -73,7 +72,7 @@ uCore只定义了链表节点,并没有专门定义链表头,那么一个双 } 从上述实现可以看出在表头插入是插入在listelm之后,即插在链表的最前位置。而在表尾插入是插入在listelm->prev之后,即插在链表的最后位置。注:list_add等于list_add_after。 - + (3) 删除 当需要删除空闲块链表中的Page结构的链表节点时,可调用内联函数list_del,而list_del进一步调用了__list_del来完成具体的删除操作。其实现为: @@ -87,21 +86,21 @@ uCore只定义了链表节点,并没有专门定义链表头,那么一个双 prev->next = next; next->prev = prev; } - + 如果要确保被删除的节点listelm不再指向链表中的其他节点,这可以通过调用list_init函数来把listelm的prev、next指针分别自身,即将节点置为空链状态。这可以通过list_del_init函数来完成。 - + (4) 访问链表节点所在的宿主数据结构 - + 通过上面的描述可知,list_entry_t通用双向循环链表中仅保存了某特定数据结构中链表节点成员变量的地址,那么如何通过这个链表节点成员变量访问到它的所有者(即某特定数据结构的变量)呢?Linux为此提供了针对数据结构XXX的le2XXX(le, member)的宏,其中le,即list entry的简称,是指向数据结构XXX中list_entry_t成员变量的指针,也就是存储在双向循环链表中的节点地址值, member则是XXX数据类型中包含的链表节点的成员变量。例如,我们要遍历访问空闲块链表中所有节点所在的基于Page数据结构的变量,则可以采用如下编程方式(基于lab2/kern/mm/default_pmm.c): //free_area是空闲块管理结构,free_area.free_list是空闲块链表头 - free_area_t free_area; + free_area_t free_area; list_entry_t * le = &free_area.free_list; //le是空闲块链表头指针 while((le=list_next(le)) != &free_area.free_list) { //从第一个节点开始遍历 struct Page *p = le2page(le, page_link); //获取节点所在基于Page数据结构的变量 …… } - + le2page宏(定义位于lab2/kern/mm/memlayout.h)的使用相当简单: // convert list entry to page @@ -113,7 +112,7 @@ le2page宏(定义位于lab2/kern/mm/memlayout.h)的使用相当简单: /* Return the offset of 'member' relative to the beginning of a struct type */ #define offsetof(type, member) \ ((size_t)(&((type *)0)->member)) - + /* * * to_struct - get the struct from a ptr * @ptr: a struct pointer of member diff --git a/lab1.md b/lab1.md index 75fc90b..d12d9af 100644 --- a/lab1.md +++ b/lab1.md @@ -1 +1 @@ -# 实验一:系统软件启动过程 \ No newline at end of file +# 实验一:系统软件启动过程 diff --git a/lab1/lab1_2_1_exercise.md b/lab1/lab1_2_1_exercise.md index 9c1e3f5..f4f4880 100644 --- a/lab1/lab1_2_1_exercise.md +++ b/lab1/lab1_2_1_exercise.md @@ -21,16 +21,16 @@ 要获取更多有关make的信息,可上网查询,并请执行 $ man make - + #### 练习2:使用qemu执行并调试lab1中的软件。(要求在报告中简要写出练习过程) 为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习: -1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。 +1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。 2. 在初始化位置0x7c00设置实地址断点,测试断点正常。 3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。 -4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。 - +4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。 + 提示:参考附录“启动后第一条执行的指令” 补充材料: @@ -43,11 +43,11 @@ 即可连接qemu,此时qemu会进入停止状态,听从gdb的命令。 另外,我们可能需要qemu在一开始便进入等待模式,则我们不再使用make qemu开始系统的运行,而使用make debug来完成这项工作。这样qemu便不会在gdb尚未连接的时候擅自运行了。 - + ***gdb的地址断点*** 在gdb命令行中,使用b *[地址]便可以在指定内存地址设置断点,当qemu中的cpu执行到指定地址时,便会将控制权交给gdb。 - + ***关于代码的反汇编*** 有可能gdb无法正确获取当前qemu执行的汇编指令,通过如下配置可以在每次gdb命令行前强制反汇编当前的指令,在gdb命令行或配置文件中添加: @@ -57,7 +57,7 @@ end 即可 - + ***gdb的单步命令*** 在gdb中,有next, nexti, step, stepi等指令来单步调试程序,他们功能各不相同,区别在于单步的“跨度”上。 @@ -66,12 +66,12 @@ nexti 单步一条机器指令,不进入函数。 step 单步到下一个不同的源代码行(包括进入函数)。 stepi 单步一条机器指令。 - + #### 练习3:分析bootloader进入保护模式的过程。(要求在报告中写出分析) BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。 -提示:需要阅读3.2.1小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式。 +提示:需要阅读3.2.1小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式。 #### 练习4:分析bootloader加载ELF格式的OS的过程。(要求在报告中写出分析) @@ -87,21 +87,21 @@ BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中 我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出: …… - ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096 + ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096 kern/debug/kdebug.c:305: print_stackframe+22 - ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8 + ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8 kern/debug/kmonitor.c:125: mon_backtrace+10 - ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84 + ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84 kern/init/init.c:48: grade_backtrace2+33 - ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029 + ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029 kern/init/init.c:53: grade_backtrace1+38 - ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d + ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d kern/init/init.c:58: grade_backtrace0+23 - ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000 + ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000 kern/init/init.c:63: grade_backtrace+34 - ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53 + ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53 kern/init/init.c:28: kern_init+88 - ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 + ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 : -- 0x00007d72 – …… @@ -111,7 +111,7 @@ BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中 ucore OS源码与机器码的语句和地址等的对应关系。 要求完成函数kern/debug/kdebug.c::print_stackframe的实现,提交改进后源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对上述问题的回答。 - + 补充材料: 由于显示完整的栈结构需要解析内核文件中的调试符号,较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用print_debuginfo函数完成查找对应函数名并打印至屏幕的功能。具体可以参见kdebug.c代码中的注释。 @@ -127,16 +127,17 @@ ucore OS源码与机器码的语句和地址等的对应关系。 要求完成问题2和问题3 提出的相关函数实现,提交改进后的源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对问题1的回答。完成这问题2和3要求的部分代码后,运行整个系统,可以看到大约每1秒会输出一次”100 ticks”,而按下的键也会在屏幕上显示。 提示:可阅读3.3.2小节“中断与异常”。 - -#### 扩展练习 Challenge(需要编程) + +#### 扩展练习 Challenge 1(需要编程) 扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务(通过网络查询所需信息,可找老师咨询。如果完成,且有兴趣做代替考试的实验,可找老师商量)。需写出详细的设计和分析报告。完成出色的可获得适当加分。 - + 提示: 规范一下 challenge 的流程。 - + kern_init 调用 switch_test,该函数如下: - + +``` static void switch_test(void) { print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态 @@ -147,7 +148,29 @@ kern_init 调用 switch_test,该函数如下: switch_to_kernel(); // switch to kernel mode print_cur_status(); } - +``` + switch_to_\* 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO\* 中断,并设置好返回的状态。 - + 在 lab1 里面完成代码以后,执行 make grade 应该能够评测结果是否正确。 + +#### 扩展练习 Challenge 2(需要编程) +用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 +基本思路是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。 + +注意: + + 1.关于调试工具,不建议用lab1_print_cur_status()来显示,要注意到寄存器的值要在中断完成后tranentry.S里面iret结束的时候才写回,所以再trap.c里面不好观察,建议用print_trapframe(tf) + + 2.关于内联汇编,最开始调试的时候,参数容易出现错误,可能的错误代码如下 + ``` + asm volatile ( "sub $0x8, %%esp \n" + "int %0 \n" + "movl %%ebp, %%esp" + : ) + ``` + 要去掉参数int %0 \n这一行 + +3.软中断是利用了临时栈来处理的,所以有压栈和出栈的汇编语句。硬件中断本身就在内核态了,直接处理就可以了。 + +4. 参考答案在mooc_os_lab中的mooc_os_2014 branch中的labcodes_answer/lab1_result目录下 diff --git a/lab1/lab1_3_3_3_lab1_interrupt.md b/lab1/lab1_3_3_3_lab1_interrupt.md index 440764d..53affac 100644 --- a/lab1/lab1_3_3_3_lab1_interrupt.md +++ b/lab1/lab1_3_3_3_lab1_interrupt.md @@ -120,7 +120,7 @@ trapframe的结构为: - + 图13 ucore中断处理流程 至此,对整个lab1中的主要部分的背景知识和实现进行了阐述。请大家能够根据前面的练习要求完成所有的练习。 diff --git a/lab1/lab1_4_lab_requirement.md b/lab1/lab1_4_lab_requirement.md index 3055b06..098994a 100644 --- a/lab1/lab1_4_lab_requirement.md +++ b/lab1/lab1_4_lab_requirement.md @@ -13,10 +13,10 @@ Intel早期的8086 CPU提供了20根地址线,可寻址空间范围即0~2^20(00000H~FFFFFH)的 1MB内存空间。但8086的数据处理位宽位16位,无法直接寻址1MB内存空间,所以8086提供了段地址加偏移地址的地址转换机制。PC机的寻址结构是segment:offset,segment和offset都是16位的寄存器,最大值是0ffffh,换算成物理地址的计算方法是把segment左移4位,再加上offset,所以segment:offset所能表达的寻址空间最大应为0ffff0h + 0ffffh = 10ffefh(前面的0ffffh是segment=0ffffh并向左移动4位的结果,后面的0ffffh是可能的最大offset),这个计算出的10ffefh是多大呢?大约是1088KB,就是说,segment:offset的地址表示能力,超过了20位地址线的物理寻址能力。所以当寻址到超过1MB的内存时,会发生“回卷”(不会发生异常)。但下一代的基于Intel 80286 CPU的PC AT计算机系统提供了24根地址线,这样CPU的寻址范围变为 2^24=16M,同时也提供了保护模式,可以访问到1MB以上的内存了,此时如果遇到“寻址超过1MB”的情况,系统不会再“回卷”了,这就造成了向下不兼容。为了保持完全的向下兼容性,IBM决定在PC AT计算机系统上加个硬件逻辑,来模仿以上的回绕特征,于是出现了A20 Gate。他们的方法就是把A20地址线控制和键盘控制器的一个输出进行AND操作,这样来控制A20地址线的打开(使能)和关闭(屏蔽\禁止)。一开始时A20地址线控制是被屏蔽的(总为0),直到系统软件通过一定的IO操作去打开它(参看bootasm.S)。很显然,在实模式下要访问高端内存区,这个开关必须打开,在保护模式下,由于使用32位地址线,如果A20恒等于0,那么系统只能访问奇数兆的内存,即只能访问0--1M、2-3M、4-5M......,这显然是不行的,所以在保护模式下,这个开关也必须打开。 -当A20 地址线控制禁止时,则程序就像在8086中运行,1MB以上的地是不可访问的。在保护模式下A20地址线控制是要打开的。为了使能所有地址位的寻址能力,必须向键盘控制器8042发送一个命令。键盘控制器8042将会将它的的某个输出引脚的输出置高电平,作为 A20 地址线控制的输入。一旦设置成功之后,内存将不会再被绕回(memory wrapping),这样我们就可以寻址整个 286 的 16M 内存,或者是寻址 80386级别机器的所有 4G 内存了。 +当A20 地址线控制禁止时,则程序就像在8086中运行,1MB以上的地是不可访问的。在保护模式下A20地址线控制是要打开的。为了使能所有地址位的寻址能力,必须向键盘控制器8042发送一个命令。键盘控制器8042将会将它的的某个输出引脚的输出置高电平,作为 A20 地址线控制的输入。一旦设置成功之后,内存将不会再被绕回(memory wrapping),这样我们就可以寻址整个 286 的 16M 内存,或者是寻址 80386级别机器的所有 4G 内存了。 键盘控制器8042的逻辑结构图如下所示。从软件的角度来看,如何控制8042呢?早期的PC机,控制键盘有一个单独的单片机8042,现如今这个芯片已经给集成到了其它大片子中,但其功能和使用方法还是一样,当PC机刚刚出现A20 Gate的时候,估计为节省硬件设计成本,工程师使用这个8042键盘控制器来控制A20 Gate,但A20 Gate与键盘管理没有一点关系。下面先从软件的角度简单介绍一下8042这个芯片。 - + ![键盘控制器8042的逻辑结构图](../lab1_figs/image012.png "键盘控制器8042的逻辑结构图") 图13 键盘控制器8042的逻辑结构图 @@ -29,8 +29,8 @@ Intel早期的8086 CPU提供了20根地址线,可寻址空间范围即0~2^20(000 有两个端口地址:60h和64h,有关对它们的读写操作描述如下: -- 读60h端口,读output buffer -- 写60h端口,写input buffer +- 读60h端口,读output buffer +- 写60h端口,写input buffer - 读64h端口,读Status Register - 操作Control Register,首先要向64h端口写一个命令(20h为读命令,60h为写命令),然后根据命令从60h端口读出Control Register的数据或者向60h端口写入Control Register的数据(64h端口还可以接受许多其它的命令)。 @@ -51,9 +51,9 @@ Status Register的定义(要用bit 0和bit 1): 除了这些资源外,8042还有3个内部端口:Input Port、Outport Port和Test Port,这三个端口的操作都是通过向64h发送命令,然后在60h进行读写的方式完成,其中本文要操作的A20 Gate被定义在Output Port的bit 1上,所以有必要对Outport Port的操作及端口定义做一个说明。 - 读Output Port:向64h发送0d0h命令,然后从60h读取Output Port的内容 -- 写Output Port:向64h发送0d1h命令,然后向60h写入Output Port的数据 -- 禁止键盘操作命令:向64h发送0adh -- 打开键盘操作命令:向64h发送0aeh +- 写Output Port:向64h发送0d1h命令,然后向60h写入Output Port的数据 +- 禁止键盘操作命令:向64h发送0adh +- 打开键盘操作命令:向64h发送0aeh 有了这些命令和知识,就可以实现操作A20 Gate来从实模式切换到保护模式了。 理论上讲,我们只要操作8042芯片的输出端口(64h)的bit 1,就可以控制A20 Gate,但实际上,当你准备向8042的输入缓冲区里写数据时,可能里面还有其它数据没有处理,所以,我们要首先禁止键盘操作,同时等待数据缓冲区中没有数据以后,才能真正地去操作8042打开或者关闭A20 Gate。打开A20 Gate的具体步骤大致如下(参考bootasm.S):