add challenge 2 in lab1

This commit is contained in:
chyyuu 2014-10-25 19:53:28 +08:00
parent fa97536f7d
commit 5346cc9003
6 changed files with 69 additions and 47 deletions

View File

@ -9,4 +9,4 @@
# 维护者 # 维护者
- yuchen AT tsinghua.edu.cn - yuchen AT tsinghua.edu.cn
- objectkuan AT gmail.com - objectkuan AT gmail.com
- xuyongjiande AT gmail.com - xuyongjiande AT gmail.com

View File

@ -1,4 +1,3 @@
##### 2.6.2.1 双向循环链表 ##### 2.6.2.1 双向循环链表
在“数据结构”课程中,如果创建某种数据结构的双循环链表,通常采用的办法是在这个数据结构的类型定义中有专门的成员变量 data, 并且加入两个指向该类型的指针next和prev。例如 在“数据结构”课程中,如果创建某种数据结构的双循环链表,通常采用的办法是在这个数据结构的类型定义中有专门的成员变量 data, 并且加入两个指向该类型的指针next和prev。例如
@ -12,7 +11,7 @@
双向循环链表的特点是尾节点的后继指向首节点且从任意一个节点出发沿两个方向的任何一个都能找到链表中的任意一个节点的data数据。由双向循环列表形成的数据链如下所示 双向循环链表的特点是尾节点的后继指向首节点且从任意一个节点出发沿两个方向的任何一个都能找到链表中的任意一个节点的data数据。由双向循环列表形成的数据链如下所示
![双向循环链表](../lab0_figs/image007.png "双向循环链表") ![双向循环链表](../lab0_figs/image007.png "双向循环链表")
这种双向循环链表数据结构的一个潜在问题是,虽然链表的基本操作是一致的,但由于每种特定数据结构的类型不一致,需要为每种特定数据结构类型定义针对这个数据结构的特定链表插入、删除等各种操作,会导致代码冗余。 这种双向循环链表数据结构的一个潜在问题是,虽然链表的基本操作是一致的,但由于每种特定数据结构的类型不一致,需要为每种特定数据结构类型定义针对这个数据结构的特定链表插入、删除等各种操作,会导致代码冗余。
在uCore内核从lab2开始中使用了大量的双向循环链表结构来组织数据包括空闲内存块列表、内存页链表、进程列表、设备链表、文件系统列表等的数据组织在[labX/libs/list.h]实现但其具体实现借鉴了Linux内核的双向循环链表实现与“数据结构”课中的链表数据结构不太一样。下面将介绍这一数据结构的设计与操作函数。 在uCore内核从lab2开始中使用了大量的双向循环链表结构来组织数据包括空闲内存块列表、内存页链表、进程列表、设备链表、文件系统列表等的数据组织在[labX/libs/list.h]实现但其具体实现借鉴了Linux内核的双向循环链表实现与“数据结构”课中的链表数据结构不太一样。下面将介绍这一数据结构的设计与操作函数。
@ -31,7 +30,7 @@ uCore的双向链表结构定义为
} free_area_t; } free_area_t;
而每一个空闲块链表节点定义位于lab2/kern/mm/memlayout 而每一个空闲块链表节点定义位于lab2/kern/mm/memlayout
/* * /* *
* struct Page - Page descriptor structures. Each Page describes one * struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions * 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就大量使用了通用的链表插入链表删除等操作函数。有关这些链表操作函数的定义如下。 从上图中我们可以看到这种通用的双向循环链表结构避免了为每个特定数据结构类型定义针对这个数据结构的特定链表的麻烦而可以让所有的特定数据结构共享通用的链表操作函数。在实现对空闲块链表的管理过程参见lab2/kern/mm/default_pmm.c就大量使用了通用的链表插入链表删除等操作函数。有关这些链表操作函数的定义如下。
(1) 初始化 (1) 初始化
uCore只定义了链表节点并没有专门定义链表头那么一个双向循环链表是如何建立起来的呢让我们来看看list_init这个内联函数inline funciton 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的实现。 参看文件default_pmm.c的函数default_init当我们调用list_init(&(free_area.free_list))时就声明一个名为free_area.free_list的链表头时它的next、prev指针都初始化为指向自己这样我们就有了一个表示空闲内存块链的空链表。而且我们可以用头指针的next是否指向自己来判断此链表是否为空而这就是内联函数list_empty的实现。
(2) 插入 (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的实现如下 对链表的插入有两种操作即在表头插入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。 从上述实现可以看出在表头插入是插入在listelm之后即插在链表的最前位置。而在表尾插入是插入在listelm->prev之后即插在链表的最后位置。注list_add等于list_add_after。
(3) 删除 (3) 删除
当需要删除空闲块链表中的Page结构的链表节点时可调用内联函数list_del而list_del进一步调用了__list_del来完成具体的删除操作。其实现为 当需要删除空闲块链表中的Page结构的链表节点时可调用内联函数list_del而list_del进一步调用了__list_del来完成具体的删除操作。其实现为
@ -87,21 +86,21 @@ uCore只定义了链表节点并没有专门定义链表头那么一个双
prev->next = next; prev->next = next;
next->prev = prev; next->prev = prev;
} }
如果要确保被删除的节点listelm不再指向链表中的其他节点这可以通过调用list_init函数来把listelm的prev、next指针分别自身即将节点置为空链状态。这可以通过list_del_init函数来完成。 如果要确保被删除的节点listelm不再指向链表中的其他节点这可以通过调用list_init函数来把listelm的prev、next指针分别自身即将节点置为空链状态。这可以通过list_del_init函数来完成。
(4) 访问链表节点所在的宿主数据结构 (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 通过上面的描述可知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是空闲块管理结构free_area.free_list是空闲块链表头
free_area_t free_area; free_area_t free_area;
list_entry_t * le = &free_area.free_list; //le是空闲块链表头指针 list_entry_t * le = &free_area.free_list; //le是空闲块链表头指针
while((le=list_next(le)) != &free_area.free_list) { //从第一个节点开始遍历 while((le=list_next(le)) != &free_area.free_list) { //从第一个节点开始遍历
struct Page *p = le2page(le, page_link); //获取节点所在基于Page数据结构的变量 struct Page *p = le2page(le, page_link); //获取节点所在基于Page数据结构的变量
…… ……
} }
le2page宏定义位于lab2/kern/mm/memlayout.h的使用相当简单 le2page宏定义位于lab2/kern/mm/memlayout.h的使用相当简单
// convert list entry to page // 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 */ /* Return the offset of 'member' relative to the beginning of a struct type */
#define offsetof(type, member) \ #define offsetof(type, member) \
((size_t)(&((type *)0)->member)) ((size_t)(&((type *)0)->member))
/* * /* *
* to_struct - get the struct from a ptr * to_struct - get the struct from a ptr
* @ptr: a struct pointer of member * @ptr: a struct pointer of member

View File

@ -1 +1 @@
# 实验一:系统软件启动过程 # 实验一:系统软件启动过程

View File

@ -21,16 +21,16 @@
要获取更多有关make的信息可上网查询并请执行 要获取更多有关make的信息可上网查询并请执行
$ man make $ man make
#### 练习2使用qemu执行并调试lab1中的软件。要求在报告中简要写出练习过程 #### 练习2使用qemu执行并调试lab1中的软件。要求在报告中简要写出练习过程
为了熟悉使用qemu和gdb进行的调试工作我们进行如下的小练习 为了熟悉使用qemu和gdb进行的调试工作我们进行如下的小练习
1. 从CPU加电后执行的第一条指令开始单步跟踪BIOS的执行。 1. 从CPU加电后执行的第一条指令开始单步跟踪BIOS的执行。
2. 在初始化位置0x7c00设置实地址断点,测试断点正常。 2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。 3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
4. 自己找一个bootloader或内核中的代码位置设置断点并进行测试。 4. 自己找一个bootloader或内核中的代码位置设置断点并进行测试。
提示:参考附录“启动后第一条执行的指令” 提示:参考附录“启动后第一条执行的指令”
补充材料: 补充材料:
@ -43,11 +43,11 @@
即可连接qemu此时qemu会进入停止状态听从gdb的命令。 即可连接qemu此时qemu会进入停止状态听从gdb的命令。
另外我们可能需要qemu在一开始便进入等待模式则我们不再使用make qemu开始系统的运行而使用make debug来完成这项工作。这样qemu便不会在gdb尚未连接的时候擅自运行了。 另外我们可能需要qemu在一开始便进入等待模式则我们不再使用make qemu开始系统的运行而使用make debug来完成这项工作。这样qemu便不会在gdb尚未连接的时候擅自运行了。
***gdb的地址断点*** ***gdb的地址断点***
在gdb命令行中使用b *[地址]便可以在指定内存地址设置断点当qemu中的cpu执行到指定地址时便会将控制权交给gdb。 在gdb命令行中使用b *[地址]便可以在指定内存地址设置断点当qemu中的cpu执行到指定地址时便会将控制权交给gdb。
***关于代码的反汇编*** ***关于代码的反汇编***
有可能gdb无法正确获取当前qemu执行的汇编指令通过如下配置可以在每次gdb命令行前强制反汇编当前的指令在gdb命令行或配置文件中添加 有可能gdb无法正确获取当前qemu执行的汇编指令通过如下配置可以在每次gdb命令行前强制反汇编当前的指令在gdb命令行或配置文件中添加
@ -57,7 +57,7 @@
end end
即可 即可
***gdb的单步命令*** ***gdb的单步命令***
在gdb中有next, nexti, step, stepi等指令来单步调试程序他们功能各不相同区别在于单步的“跨度”上。 在gdb中有next, nexti, step, stepi等指令来单步调试程序他们功能各不相同区别在于单步的“跨度”上。
@ -66,12 +66,12 @@
nexti 单步一条机器指令,不进入函数。 nexti 单步一条机器指令,不进入函数。
step 单步到下一个不同的源代码行(包括进入函数)。 step 单步到下一个不同的源代码行(包括进入函数)。
stepi 单步一条机器指令。 stepi 单步一条机器指令。
#### 练习3分析bootloader进入保护模式的过程。要求在报告中写出分析 #### 练习3分析bootloader进入保护模式的过程。要求在报告中写出分析
BIOS将通过读取硬盘主引导扇区到内存并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。 BIOS将通过读取硬盘主引导扇区到内存并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
提示需要阅读3.2.1小节“保护模式和分段机制”和lab1/boot/bootasm.S源码了解如何从实模式切换到保护模式。 提示需要阅读3.2.1小节“保护模式和分段机制”和lab1/boot/bootasm.S源码了解如何从实模式切换到保护模式。
#### 练习4分析bootloader加载ELF格式的OS的过程。要求在报告中写出分析 #### 练习4分析bootloader加载ELF格式的OS的过程。要求在报告中写出分析
@ -87,21 +87,21 @@ BIOS将通过读取硬盘主引导扇区到内存并转跳到对应内存中
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数可在lab1中执行 “make qemu”后在qemu模拟器中得到类似如下的输出 我们需要在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 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 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 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 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 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 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 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
<unknow>: -- 0x00007d72 <unknow>: -- 0x00007d72
…… ……
@ -111,7 +111,7 @@ BIOS将通过读取硬盘主引导扇区到内存并转跳到对应内存中
ucore OS源码与机器码的语句和地址等的对应关系。 ucore OS源码与机器码的语句和地址等的对应关系。
要求完成函数kern/debug/kdebug.c::print_stackframe的实现提交改进后源代码包可以编译执行并在实验报告中简要说明实现过程并写出对上述问题的回答。 要求完成函数kern/debug/kdebug.c::print_stackframe的实现提交改进后源代码包可以编译执行并在实验报告中简要说明实现过程并写出对上述问题的回答。
补充材料: 补充材料:
由于显示完整的栈结构需要解析内核文件中的调试符号较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用print_debuginfo函数完成查找对应函数名并打印至屏幕的功能。具体可以参见kdebug.c代码中的注释。 由于显示完整的栈结构需要解析内核文件中的调试符号较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用print_debuginfo函数完成查找对应函数名并打印至屏幕的功能。具体可以参见kdebug.c代码中的注释。
@ -127,16 +127,17 @@ ucore OS源码与机器码的语句和地址等的对应关系。
要求完成问题2和问题3 提出的相关函数实现提交改进后的源代码包可以编译执行并在实验报告中简要说明实现过程并写出对问题1的回答。完成这问题2和3要求的部分代码后运行整个系统可以看到大约每1秒会输出一次”100 ticks”而按下的键也会在屏幕上显示。 要求完成问题2和问题3 提出的相关函数实现提交改进后的源代码包可以编译执行并在实验报告中简要说明实现过程并写出对问题1的回答。完成这问题2和3要求的部分代码后运行整个系统可以看到大约每1秒会输出一次”100 ticks”而按下的键也会在屏幕上显示。
提示可阅读3.3.2小节“中断与异常”。 提示可阅读3.3.2小节“中断与异常”。
#### 扩展练习 Challenge需要编程 #### 扩展练习 Challenge 1(需要编程)
扩展proj4,增加syscall功能即增加一用户态函数可执行一特定系统调用获得时钟计数值当内核初始完毕后可从内核态返回到用户态的函数而用户态的函数又通过系统调用得到内核态的服务通过网络查询所需信息可找老师咨询。如果完成且有兴趣做代替考试的实验可找老师商量。需写出详细的设计和分析报告。完成出色的可获得适当加分。 扩展proj4,增加syscall功能即增加一用户态函数可执行一特定系统调用获得时钟计数值当内核初始完毕后可从内核态返回到用户态的函数而用户态的函数又通过系统调用得到内核态的服务通过网络查询所需信息可找老师咨询。如果完成且有兴趣做代替考试的实验可找老师商量。需写出详细的设计和分析报告。完成出色的可获得适当加分。
提示: 提示:
规范一下 challenge 的流程。 规范一下 challenge 的流程。
kern_init 调用 switch_test该函数如下 kern_init 调用 switch_test该函数如下
```
static void static void
switch_test(void) { switch_test(void) {
print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态 print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态
@ -147,7 +148,29 @@ kern_init 调用 switch_test该函数如下
switch_to_kernel(); // switch to kernel mode switch_to_kernel(); // switch to kernel mode
print_cur_status(); print_cur_status();
} }
```
switch_to_\* 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO\* 中断,并设置好返回的状态。 switch_to_\* 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO\* 中断,并设置好返回的状态。
在 lab1 里面完成代码以后,执行 make grade 应该能够评测结果是否正确。 在 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目录下

View File

@ -120,7 +120,7 @@ trapframe的结构为
<tr> <tr>
</tr> </tr>
</table> </table>
图13 ucore中断处理流程 图13 ucore中断处理流程
至此对整个lab1中的主要部分的背景知识和实现进行了阐述。请大家能够根据前面的练习要求完成所有的练习。 至此对整个lab1中的主要部分的背景知识和实现进行了阐述。请大家能够根据前面的练习要求完成所有的练习。

View File

@ -13,10 +13,10 @@
Intel早期的8086 CPU提供了20根地址线,可寻址空间范围即0~2^20(00000H~FFFFFH)的 1MB内存空间。但8086的数据处理位宽位16位无法直接寻址1MB内存空间所以8086提供了段地址加偏移地址的地址转换机制。PC机的寻址结构是segment:offsetsegment和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......,这显然是不行的,所以在保护模式下,这个开关也必须打开。 Intel早期的8086 CPU提供了20根地址线,可寻址空间范围即0~2^20(00000H~FFFFFH)的 1MB内存空间。但8086的数据处理位宽位16位无法直接寻址1MB内存空间所以8086提供了段地址加偏移地址的地址转换机制。PC机的寻址结构是segment:offsetsegment和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的逻辑结构图如下所示。从软件的角度来看如何控制8042呢早期的PC机控制键盘有一个单独的单片机8042现如今这个芯片已经给集成到了其它大片子中但其功能和使用方法还是一样当PC机刚刚出现A20 Gate的时候估计为节省硬件设计成本工程师使用这个8042键盘控制器来控制A20 Gate但A20 Gate与键盘管理没有一点关系。下面先从软件的角度简单介绍一下8042这个芯片。
![键盘控制器8042的逻辑结构图](../lab1_figs/image012.png "键盘控制器8042的逻辑结构图") ![键盘控制器8042的逻辑结构图](../lab1_figs/image012.png "键盘控制器8042的逻辑结构图")
图13 键盘控制器8042的逻辑结构图 图13 键盘控制器8042的逻辑结构图
@ -29,8 +29,8 @@ Intel早期的8086 CPU提供了20根地址线,可寻址空间范围即0~2^20(000
有两个端口地址60h和64h有关对它们的读写操作描述如下 有两个端口地址60h和64h有关对它们的读写操作描述如下
- 读60h端口读output buffer - 读60h端口读output buffer
- 写60h端口写input buffer - 写60h端口写input buffer
- 读64h端口读Status Register - 读64h端口读Status Register
- 操作Control Register首先要向64h端口写一个命令20h为读命令60h为写命令然后根据命令从60h端口读出Control Register的数据或者向60h端口写入Control Register的数据64h端口还可以接受许多其它的命令 - 操作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的操作及端口定义做一个说明。 除了这些资源外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发送0d0h命令然后从60h读取Output Port的内容
- 写Output Port向64h发送0d1h命令然后向60h写入Output Port的数据 - 写Output Port向64h发送0d1h命令然后向60h写入Output Port的数据
- 禁止键盘操作命令向64h发送0adh - 禁止键盘操作命令向64h发送0adh
- 打开键盘操作命令向64h发送0aeh - 打开键盘操作命令向64h发送0aeh
有了这些命令和知识就可以实现操作A20 Gate来从实模式切换到保护模式了。 有了这些命令和知识就可以实现操作A20 Gate来从实模式切换到保护模式了。
理论上讲我们只要操作8042芯片的输出端口64h的bit 1就可以控制A20 Gate但实际上当你准备向8042的输入缓冲区里写数据时可能里面还有其它数据没有处理所以我们要首先禁止键盘操作同时等待数据缓冲区中没有数据以后才能真正地去操作8042打开或者关闭A20 Gate。打开A20 Gate的具体步骤大致如下参考bootasm.S 理论上讲我们只要操作8042芯片的输出端口64h的bit 1就可以控制A20 Gate但实际上当你准备向8042的输入缓冲区里写数据时可能里面还有其它数据没有处理所以我们要首先禁止键盘操作同时等待数据缓冲区中没有数据以后才能真正地去操作8042打开或者关闭A20 Gate。打开A20 Gate的具体步骤大致如下参考bootasm.S