|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
【17.3.4 8259A芯片的初始化】
lidt指令用于加载IDTR寄存器,该指令的格式和lgdt相同:
lidt m48 ;lidt m16&m32
该指令的操作数是一个内存地址,指向一个包含了48位(6字节)数据的内存区域。在16位实模式下,该地址是16位的;在32位模式下,该地址是32位的。和LGDT指令一样,该指令在实模式下也可以执行,以便于在进入保护模式之前就做好中断有关的准备工作。
在这6字节的内存区域中,前16位是IDT的界限值,高32位是IDT的线性基地址。在初始状态下(计算机启动之后),IDTR的基地址被初始化为0x00000000;界限值为0xFFFF。该指向不影响任何标志位。
825559A芯片主片的中断向量0x08-0x0F和32位处理器的0x08-0x0F异常向量相冲突。
好在8259A(以及I/O APIC)都是可编程的,允许重新设置中断向量。根据Intel公司的建议,中断向量0x20-0xFF(32-255)是用户可以自由分配的部分。那么,我们可以设置8259A的主片,把它的中断向量改成0x20-0x27,这样就没问题了。
对8259A编程需要使用初始化命令字(ICW),以设置它的工作方式,共有4个初始化命令,分别是ICW1-ICW4,都是单字节命令。ICW1用于设置中断请求的触发方式,以及级联的芯片数量;ICW2用于设置每个芯片的中断向量;ICW3用于指定哪个引脚实现芯片的级联;ICW4用于控制芯片的工作方式。
8259A主片的端口号是0x20和0x21,从片的端口号是0xA0和0xA1,要发送初始化命令字给8259A芯片,对于主片来说,需要先向0x20端口发送ICW1,而对于从片来说,这个端口是0xA0。这是一个标志,每次8259A芯片接到ICW1时,都意味着一个新的初始化过程开始了。
从0x20/0xA0端口接受命令字ICW1后,8259A芯片期待从0x21/0xA1端口接受命令字ICW2。但是,它是否期待ICW3和ICW4,还要看ICW1的内容。如图17-17所示,ICW1的位0决定了是否有ICW4命令,位1只是是否为多片级联。如果是多片级联,那么,必定有ICW3命令。这样一来,8259A芯片就知道,在接受了ICW2命令之后,是否还要在相同的端口0x20/0xA1上依次再接受ICW3和ICW4。
注意,在图17-17中,深色的比特位表示它已被保留,或者不用,使用图中所标注的固定值(0或1);有些比特虽然不是深色,但也标注了固定值(0或1)这些位是有意义的,可以设置或改变,具体的含义可参考芯片手册。但是,之所以在这里采用固定值,是因为就目前的应用环境来说,这是比较通用的设立设置。
来看代码17-2。
第921、922行,先向9259A主片发送ICW1,端口号是0x20。从命令上看,这里需要ICW4,而且指定了多芯片级联方式,中断信号的采集用的是边沿触发方式。因为是多芯片级联,故需要ICW3。
第923、924行,通过另一个端口0x21向主片发送ICW2命令。如图17-17所示,ICW2命令用于设置芯片的中断向量号。芯片每个引脚的中断向量号不需要单独设置,值需要一个起始向量号即可。ICW2的低3位不用,固定为0,仅高5位有效。在这里,ICW2的值是0x20,对应着二进制数00100000,高5位是00100,此时,该芯片的8个中断引脚就分别对应着中断向量号0x20-0x27。
再举个例子,如果ICW2的高5位是01101,那么加上低3位的全“0”,它对应的二进制数就是01101000,即0x68,该芯片的中断向量为0x68-0x6F。
第925、926行,依然通过端口0x21向主片发送ICW3命令。如图17-17所示,发送给主片的命令和发送给从片的命令,是不相同的。因为这里是在设置主片,故该命令字的7比特分别表示那个引脚是否连着从片。从命令字上看,是0x04,即二进制的00000100,也就是说,该芯片的第3个引脚连着从片。
第927、928行,依然通过端口0x21向主片发送ICW4命令。如图17-17所示,我们发送的命令字是0x01,这表示要求采用非自动结束方式。对于单片使用的场合,采用自动结束方式较为方便。但多片级联的场合,应当采用非自动结束方式。
第930—937行,这些代码用于设置和主片相连的从片,方法大致相同,读者自行分析。
第940-952行,这段代码专门用于设置和0x70号始终中断有关的硬件状态,包括RTC和9259A。对RTC的设置包括允许它产生哪些中断信号,并读一下它的寄存器C。寄存器C在每次读取后自动清零,如果没有清零,RTC将不会产生中断信号;对于8259A的设置主要是打通它和RTC之间的中断信号通路。这段代码是从第9章原封不动地抄来的。
第954行,用STI指令设置EFLAGS寄存器的IF位,开放硬件中断。
中断是计算机系统中一个必不可少的恶魔,如果处理不当,各种奇怪的程序问题都有可能出现,而且神出鬼没,不容易找到它发生的根源。这是可以理解的,在一个顺序工作的程序中,很容易用调试工具找到错误指令和出错的原因。但是,中断是随机发生的,而且不能确定在中断发生时,处理器将控制转移到了哪里。即使知道出错的位置,也不容易发现错误的原因。很多时候,中断处理过程和被中断的程序有着逻辑上关联,包括状态的依赖和数据的共享和争用,等等。
因为执行了sti,硬件中断会随时得到处理。特别是我们最关注的实时时钟中断,它差不多会在1秒内发生一次。当此中断发生后,过程rtm_0x70_interrupt_handle就会被执行。它会遍历TCB链,找到一个状态为忙的任务,和一个状态为空闲的任务,然后发起任务切换。
就目前的实际情况而言,该中断处理过程不会做太多的事,仅仅是给8259A芯片发送中断结束指令EOI,并读一下RTC芯片的寄存器C,然后执行iretd指令返回,因为目前链表为空。
【17.3.5 平坦模型下的字符串显示例程】
代码清单第17-2第956、957行,在屏幕上显示字符串,表示内核正在保护和分页模式下,内核的地址空间已经被映射到地址0x80000000以上。
在本章中,由于使用了平坦模型,put_string过程也不得不做了大大幅度修改,以适应这种变化。一直以来,该过程接受的参数是DS:EBX。此处,DS是数据段寄存器,要显示的字符串位于它所指向的段中;EBX寄存器的内容是字符串在段内的偏移量。显然,老版本的put_string过程是面向多段模型的。
相反地,该过程的新版本只能工作在平坦模型下,它只需要用EBX寄存器传入字符串的线性地址即可。在平坦模型下,字符串位于任务的4GB虚拟地址空间,它的线性地址是唯一的。
在字符串的显示期间,需要临时关闭硬件中断,即,用cli指令清零EFLAGS寄存器的IF位。如果不这么做,那么,在字符串显示期间,随时会被中断。如果切换到另一个任务,那么,两个任务所显示的内容就有可能在屏幕上交替出现。当然,这还算不上是严重的问题,更严重的是写光标寄存器,举个例子,任务A读光标位置,并在屏幕上写了一个字符。当它正准备用新的数值写光标寄存器时,中段发生,任务B开始执行。任务也读光标位置并在那里写字符。因为任务A实际上并没有完成推进光标的工作,故任务B的字符会覆盖任务A的字符。这种情况发生的几率较低,但并不是不会发生。
因此,第42行,在开始显示字符串之前,先禁止硬件中断;第54行,只有在整个字符串完整地显示完毕之后,才开放硬件中断。这样,每个任务的字符串都能完整地显示。同时,因为本例程中有sti指令,因此,在整个中断系统没有初始化完成之前,不能调用。
在硬件中断关闭期间,put_string过程实际上是调用另一个近过程put_char来逐个显示字符的。Put_char过程是从第62行开始的。
第68-81行的工作是取当前光标位置。取得的光标位置数值位于BX寄存器中,要用于寻址显示缓冲区。因为访问显示缓冲区时用的是32寻址方式,故必须使用EBX寄存器。第81行,用and指令清除EBX寄存器的高16位,仅保留低16位(BX)。
第101行,写字符到显示缓冲区。显示缓冲区的线性基地址是0x800B8000,缓冲区的偏移量是由EBX寄存器提供的,0x800B8000是32位立即数,故必须和EBX寄存器搭配,而不能用BX寄存器。还有,之所以显示缓冲区的线性基地址是0x800B8000,是因为物理内存的低端1MB被完整地映射到从0x80000000开始的高端。因此,线性地址0x800B8000会被处理器的页部件转换成物理地址0x000B8000。
同样的道理,第111-121行,在做屏幕上滚的操作时,要传送的内容在4GB段内的偏移量为0x800B80A0;目标位置在4GB段内的偏移量为0x800B8000。
17.4 内核任务的创建
【17.4.1 创建内核任务的TCB】
回到第960-986行,和往常一样,在屏幕上显示处理器的品牌信息,没有什么好说的。
第989-1007行,在全局描述符表(GDT)中安装调用门,为用户任务提供系统服务。然后,通过调用门在屏幕上显示信息,以测试调用门的安装是否正确。
下面的工作是创建内核任务,也就是我们所说的程序管理器任务。内核任务需要一个任务控制块(TCB),毕竟它也要参与任务轮转。但是,该TCB所需的内存不是动态分配的,而是一段静态的空间,是在内核程序编写的时候保留的。回到前面第519行,在那里声明了标号core_tcb,并初始化了32个为零的双字。TCB不需要这么多空间,但多保留一些也没坏处。
第1010行,首先设置内核任务的状态值为0xFFFF(忙)。实际上,当前正在执行的就是内核,从某种意义上来说就是内核任务,只不过没有办理将其TSS描述符传送到任务寄存器TR的手续而已。
内核占据着它自己的虚拟内存空间的高端,同时也映射到每个任务的虚拟内存空间的高端,具体的起始位置是线性地址0x80000000。从这里开始,前1MB(0x80000000-0x800FFFFF)已经被它自己用完了,实际可以继续分配的空间从线性地址0x80100000开始。因此第1011行,在TCB中设置这个可以分配的起始地址。
每个任务都可以有自己的LDT,如果没有也不要紧。第1013行,设置内核任务的LDT的初始界限值。就本章来看,这个值是用不上的,因为内核任务没有LDT。
对TCB的初始化基本就是这些。第1015行,将内核任务的TCB追加到TCB链表中,向从前一样,在TCB链表中添加新的TCB需要调用过程append_to_tcb_link,该过程位于第845行,在本章已经做了修改,因此适合在平坦模型下工作。
当程序员不是一件容易的事,需要考虑的东西太多。访问和修改TCB链原本不是多大的事情,可要是中断掺和进来,就得小心了。要知道,0x70号实时时钟随时都在发生,那个中断处理过程也在不停地访问同一个TCB链表。如果处理不当,很容易出现问题。请考虑一下,过程append_to_tcb_link主要完成两件事:
1、遍历链表,找到最后一个TCB,修改它的“下一个TCB线性地址”,使它指向新的TCB;
2、清空新TCB的“下一个TCB线性地址”域,表明它是最后一个TCB。
假如现在已经完成了步骤1,新TCB已经成为链表的最后一个节点。但是,在准备执行步骤2时,0x70号中断发生了,该中断处理过程遍历链表。可想而知,因为新TCB的“下一个TCB线性地址”域还没有清零,所以,中断处理过程无法找到链尾,而且会用那个非零的数作为地址,访问到它不该访问的区域(不存在的下一个TCB),处理器产生异常,程序很可能因此崩溃了!
因此,在过程append_to_tcb_link的一开始,也就是第847行,先用cli指令屏蔽硬件中断。找到最后一个TCB之后,第861行,修改它的“下一个TCB的线性地址”域,使其内容为新TCB的线性地址;第862行,将新TCB的“下一个线性地址”域清零。这两行很危险,在它们中间很容易发生中断。而一旦中断发生,麻烦就来了。Cli和sti不必放在该过程的首尾,放到这两条指令的前后就行:
cli
mov [eax],ecx
mov dword [ecx],0x00000000
sti
实际上,这是更合理的做法。记住,CLI指令只在最有必要的时候使用。在一个正常的系统中个,大家都很忙,都需要马不停蹄地投入运行,不要让无所谓的中断屏蔽指令影响到每个程序的正常执行。
【17.4.2 宏汇编技术】
接下来是创建内核任务的TSS。对于一个任务来说,任务状态段TSS是必不可少的。为此,需要在内核的虚拟地址空间内分配内存。
第1018行的作用是分配创建TSS所需要内存空间:
alloc_core_linear
它既不是处理器指令,也不是标号,它是宏。
宏(Macro)是一种简化汇编语言程序编写的强大手段,绝大多数汇编语言编译器都支持宏,因此,这些汇编语言称为宏汇编。
宏并不是处理器指令,但它也不同于编译器提供的伪指令,专业地说,它是预处理指令。我们知道,编译器在编译源程序时,要多边遍扫描,每次都完成不同的工作,这些都可以称为预处理,最后才开始将语句翻译成机器指令。
%define vrm(x) 0xb8000+x
%define用来定义单行的宏,宏的名字叫vrm,带有一个参数x,参数要放在括号中。如果有多个参数,参数之间要用逗号分开。在宏的名字之后,要有空格,一个或者多个空格均可,然后,是一个表达式。从该表达式可以看出这个宏是用来做什么的,有什么意义。
宏的作用是可以代替复杂的表达式。在编译期间,编译器要先将宏展开,再编译成机器指令。因此,在编译期间,上面那条mov指令将被展开为下面的形式:
mov byte [0xb8000+0x02],’h’
%macro用来定义多行的宏:
%macro 宏名字 参数个数
……;宏的具体内容
%endmacro
17.5 用户任务的创建
【17.5.1 准备加载用户程序】
为了能够访问和控制所有的任务,每个任务的TCB都必须创建在内核的地址空间内。第1043行,宏用于创建用户任务的TCB;第1045-1047行,初始化用户任务的TCB,主要包括初始的LDT界限值,以及从哪个线性地址开始在用户任务的局部空间内分配内存(一般是0)。注意,任务的状态值是0x0000,即空闲。
第1049-1051行,在当前栈中压入两个双字参数,并调用load_relocate_program过程以加载和重定位用户程序。
过程load_relocate_program是从第616行开始。
过程几个区别:
1、不再需要压入DS和ES寄存器,因为在平坦模式下;
2、参数的位置稍许发生了变化。
【17.5.2 转换后援缓冲器的刷新】
第625-631行,清空当前页目录表的前半部分,位创建用户任务的页表目录项做准备。在第16章里已经说清楚了,我们是借用内核的页目录来创建用户任务的页目录,毕竟,对于每一个任务来书,页目录表的前半部分对应着它的局部空间,后半部分对应着全局空间,内核用的是其页目录表的后半部分,前半部可以临时用来创建只属于任务自己的页目录项。
第633、634行,重新加载一遍控制寄存器CR3(页目录表基地址寄存器PDBR),其作用是什么呢?开启页功能时,处理器的页部件要把线性地址转换成物理地址,而访问页目录表和页表是相当费时间的。因此,把页表项预先存放到处理器中,可以加快地址转换速度。为此,处理器专门构造了一个特殊的高速缓存装置,叫做转换后援缓冲器(TLB)。事实上,对该缓冲器的命令可谓五花八门,从“转换旁路缓冲器”、“转换后备缓冲区”到“快表”,不一而足。
如图17-19所示,这是TLB的结构。它分为两大部分,第一部分是标记,其内容为线性地址的高20位;第二部分是页表数据,包括属性、访问权和页物理地址的高20位。在分页模式下,当段部件发出一个线性地址时,处理器用线性地址的高20位来查找TLB,如果找到匹配(命中),则直接使用其数据部分作为转换用的地址;如果检索不成功(不中),则处理器还得花时间访问内存中的页目录表和页表,找到那个页表项,然后将它填写到TLB中,以备后用。TLB容量不大,如果它装满了,则必须淘汰掉那些用得较少的项目。
TLB中的属性位来自页表项,比如页表项中的D位(脏位)等;访问权来自页目录项和对应的页表项,比如RW位和US位,等等。问题是,就RW位和US位来说,页目录项和页表项都有这两位,以哪一个为准呢?在分页机制中,对页的访问控制按最严格的访问权执行。对于某个线性地址,如果其页目录项的RW位是“0”而其页表项的RW位是“1”,则按RW位是“0”执行。也就是说,TLB中的访问权,是页目录项和页表项中,对应访问权的逻辑与。
处理器仅仅缓存那些P位是1的页表项,而且,TLB的工作和CR3寄存器的PCD和PWT位无关,不受这两位影响。另外,对于页表项的修改不会同时反映到TLB中。是的,这是很糟糕的,如果内存中的页表项已经修改,但TLB中的对应条目还没有更新,那么,转换后的物理地址必定是错误的。
TLB是软件不可直接访问的,但却有其他办法来刷新它的内容(条目)。比如,将CR3寄存器的内容读出,再原样写入,这样就会使TLB中的所有条目失效。当任务切换时,因为要从新任务中的CR3寄存器域加载页目录表基地址,也会隐式地导致TLB中的所有条目无效。
注意,上述方法对于那些标记为全局(G位为1)的页表项来说无效,不起作用。
【17.5.3 用户任务的创建和初始化】
Invlpg指令用于刷新TLB中的单个条目。当然,要做到这一点,必须指定一个线性地址,处理器用给出的线性地址搜索TLB,找到那个条目,然后从内存中重新加载它。
invlpg m
也就是说,该指令的操作数是一个内存地址。指令执行时,处理器首先确定该线性地址位于哪个页内,然后刷新相应的TLB条目。invlpg是特权指令,在保护模式下执行,当前特权级CPL必须为0。该指令不影响任何标志位。
书本学习终于完结~~~~~~~ |
|