时间:2009-08-16 点击: 次 来源:本站原创 作者:佚名 - 小 + 大
| 操作系统实践 码字辛苦,转载请注明出处 从年初三开始看《自己动手写操作系统》一书,到现在20天,学到了很多东西,特此记录下来。 我喜欢动手写代码,所以看《自己动手写操作系统》一书我选择了按书上的进度自己用MASM来写代码,而不是机械的将书上的代码抄一遍,其间遇到了一系列莫名其妙的问题,不过得益于此,真正理解了很多底层的东西,将汇编(年前学的)和操作系统整个理通了。 首先说一下目前为止我参考的资料和用到的一些软件:
参考资料: 1. 《自己动手写操作系统》 以后简称《自》 2. 《80x86汇编语言程序设计教程》 以后简称《80》 3. 《LINUX内核0.11详细注释》 以后简称《L》 4. NASM中文手册 5. 关于各种文件系统、文件头等的说明资料(百度的,很零散,一大堆)
软件: 1. MASM 我用的是6.11,当然还包括相应的LINK和ML 6.15 2. NASM 《自》一书是用NASM写汇编 3. 反汇编工具 我用的是ndisasm和W32Dasm 4. VMware Workstation 5. Microsoft Virtual PC 写出来的东西总得运行吧,所以VM和VPC必备 6. bochs 调试代码利器,断点、单步执行、读内存...... 7. winimage 方便读写软盘 8. PDF阅读器 我用的是Foxit Reader 2.0 9. 能写软盘指定扇区的工具 自己用VC写了一个 10. UltraEdit 用来写汇编代码,这个要多说几句,详见补充1
除了9,其他都能在网上很容易找到。所有工具的基本使用方法都很简单,bochs相对复杂一点,所以在补充2中介绍一下。我写的这个工具可以在这里下载:http://download.csdn.net/source/1019735 ,因为太简单了,所以没有使用说明,只有功能描述。
补充1:UE安装后wordfile没有MASM汇编的语法支持,需要到UE官网去下(http://www.ultraedit.com/files/wf/masm.txt)。直接复制打开的文本,粘贴到UE安装目录下wordfile.uew文件的最后面。实际上UE并不是读的这个文件,而是用户文件夹下Application Data\IDMComp\UltraEdit里面的wordfile.uew文件,具体位置也可以这样确定:运行UE –> 高级 -> 配置 -> 导航 -> 编辑器显示 -> 语法高亮,右边窗口中“词语列表的完整路径”就是需要的路径了,将wordfile.uew复制过去后,UE会根据打开文件的扩展名自动选择相应的语法高亮。
起先UE被我当成了记事本在用,没有语法高亮,一片黑白,因为影响不是太大,所以没在意,然后发生了一件搞笑的事情使我开始用语法高亮功能了:有天当我写 C++ (VC6里边)的时候,发现运行结果总是不对,就一段很短很简单的代码,我怎么看都没发现哪错了。最后发现是由于最近都是写汇编,所以习惯性的用分号来注释,如此一来编译器是不会报错的,结果理所当然的不对了。因为写汇编的时候没有语法高亮,形成了习惯,所以在找C++代码错误的时候,分号后面的语句被我直接过滤掉了……
补充2:我用的bochs版本是2.2.pre1。安装好以后建立一个名为XXX.bxrc的文件(记得显示扩展名),然后将下面的内容复制进去(修改自《自》一书):
-----------------------------------XXX.bxrc------------------------------------------------ megs: 32 romimage: file=$BXSHARE/BIOS-bochs-latest, address=0xf0000 vgaromimage: $BXSHARE/VGABIOS-elpin-2.40 floppya: 1_44=BOOT.IMG, status=inserted boot: a mouse: enabled=0 log: bochsout.txt keyboard_mapping: enabled=1, map=$BXSHARE/keymaps/x11-pc-us.map ------------------------------------ XXX.bxrc 完------------------------------------------
注释我都去掉了,其中floppya: 1_44=BOOT.IMG, status=inserted这一句中的BOOT.IMG需要改为自己的软盘映像名,如果软盘映像和XXX.bxrc不在同一文件夹下,那么还得加上路径。log: bochsout.txt这一句是指定生成的LOG文件名,我看不懂LOG文件,所以直接将它路径指向回收站……XXX.bxrc可以放在任何地方,直接双击就可以运行。不过直接双击相当于是在用VM或VPC从BOOT.IMG软盘启动,并没有调试功能,要调试需要用bochsdbg.exe来运行,于是可以写一个YYY.BAT文件(同样修改自《自》一书):
-------------------------------------YYY.BAT------------------------------------------------ ZZZ\bochsdbg.exe -q -f bochsrc.bxrc ------------------------------------ YYY.BAT 完------------------------------------------
ZZZ是bochs的安装路径,比如 C: \Bochs-2.2.pre1 . 直接双击YYY.BAT会出现一个命令行窗口,就可以开始调试了。下面是我认为最有用的调试命令(由于百度空间对表格支持相当糟糕,所以只好贴图了):
这里有几个注意的地方: 1. 所有的大小写都是不等效的,目前为止没有用过大写字母,所有的一切都是小写。 2. 16进制数加0x前缀,不是h后缀。 3. 这里列出的所有指令中用到的地址都是物理地址,不是线性地址也不是虚拟地址。操作线性地址,虚拟地址的指令是不同的。如果安装bochs的时候安装了帮助手册那么“安装路径\ docs\user\internal-debugger.html”就是所有指令的详细说明。 4. 不管是中断还是单步执行,显示出来的指令都是没有执行的,所以查看的任何信息都是指令执行前的。 5. xp/nuf addr中nuf的n表示有多少个单元数据需要显示, u表示1个单元的大小,可取值为b(1个字节)、h(2个字节)、w(4个字节)、g(8个字节), f表示输出格式,可取值为x(16进制)、d(10进制)、u(无符号10进制)、o(8进制)、t(2进制)。nuf都是可选的,u和f默认和上一次使用该指令时的取值一样或者默认是w和x(第一次使用时),n默认为1。如果nuf都使用默认值,那么输入指令的时候就应该去掉斜线。addr也是可选的,默认是下一个地址值。指令获得的值,单元之间是按地址由低到高排列,而单元内是由高到低排列。例如使用xp/2wx 0x7c00 输出 0x5ab000b7 0x05b947b3 那么按写代码的习惯由高到低是 0x05b947b3 5ab000b7 而按内存中由低到高就是 b7 00 b0 5a b3 47 b9 05
这算是前言吧,到这里差不多结束了,最后要说的是,本分类下所有文章都是基于自己的理解(甚至是推测),如果有不同的观点欢迎讨论,如果有错误请指正。 原文网址: http://hi.baidu.com/liuqizealot/blog/item/d8b68982e35fa097f703a66c.html 操作系统实践1--软盘引导扇区初接触 在本文中,我们将写出第一个不依赖操作系统运行的程序,它的作用是调用BIOS的10H号中断9号功能在光标处输出5个红底白字的“Z”,之所以没有像《自》一书中输出“Hello, OS world!”,是因为这样输出比较简单,毕竟它没什么用。 我会详细解释代码,如果解释比较少就以注释形式放在代码里面,如果需要说的比较多,就以补充形式将解释放到最后。还要说明的是,虽然本系列文章中的汇编代码是用MASM编译,但是所有代码都会脱离MSDOS,不依赖任何操作系统运行,如果需要在MSDOS下运行,那么你可以自己加上条件汇编指令,代码的差异我会在解释代码的时候提及。 下面的代码OSHello.asm可以不做任何修改编译出来在MSDOS下运行,只是这和我们写这段代码的目的相悖。 -----------------------------------OSHello.asm------------------------------------------------ ;调用10H号中断的9号功能在光标处输出5个红底白字的“Z” JMP $ ;见补充3 补充1:众所周知这是程序的入口点,它其实是一个普通标号,在这里是没有用处的,因为引导扇区将被加载到7C00H处,然后直接跳转过来开始执行,也就是说7C00H就是程序的入口点,当然加上也不会错,为了保持习惯就留着了。至于入口标号的作用,暂时放一放,下一篇文章介绍MZ文件头的时候再解释。 补充3:这是一个无限跳转,因为后面没指令了,所以只好让程序停在这里。前面这些我知道地球人都知道,我只是想强调一点:大多数情况下用 JMP $ 可以节省很多调试程序的时间,因为它可以迅速定位问题代码位置。最后说一下在用 JMP $ 定位上我犯的一个浪费了我很长时间的愚蠢错误,看代码: 补充4:先讲讲计算机的启动,《L》一书中的原话:当PC的电源打开后,80x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。PC机的BIOS将执行某些系统的检测,并在物理地址0处开始初始化中断向量。此后,它将可启动设备的第一个扇区(磁盘引导扇区,512字节)读入内存地址0x7C00处,并跳转到这个地方。另外所有的资料都指出引导扇区的第511字节必须是55H 第512字节必须是0AAH,因为BIOS要靠此来验证是否引导扇区。这就是代码最后加上“DW 0AA55H”的原因(经过我的测试在VM和VPC下不加0AA55H也能正常运行,bochs下会出错,实机没有测试)。“DW 0AA55H”解释完了,现在解释“ORG 510”,它的作用就是将剩下的空间用0填充,直到510字节,这样做的目的只是为了能在最后两字节写上 0AA55H而已。到这里所有的代码都解释完了,但是关于ORG我还有很多话要说,先看《自》一书中的第一个引导扇区代码boot.asm: -----------------------------------boot.asm------------------------------------------------ 注意第一行的红色代码,初接触NASM汇编,我实在想不通为什么有这么一行代码,后来看NASM手册才知道NASM中的ORG和MASM的ORG是不同的:MASM中ORG是改写地址计数器为指定的值,它可不仅仅是只能将值变大中间补0这么简单,它还可以将值变小,然后覆盖已经产生的代码(很好很强大);NASM中的ORG只是简单的将所有内部的地址引用增加一个段内偏移值。 再看看《L》一书中这句话:此后,它将可启动设备的第一个扇区(磁盘引导扇区,512字节)读入内存地址0x7C00处,并跳转到这个地方。这里并没有明确说明是跳转到7C0H:0还是0:7C00H。然后LINUX0.11的bootsect.s中有这样一个符号定义 BOOTSEG = 0x7c0 对这个符号的注释是 original address of boot-sector bootsect的原始地址(是段地址,以下同)看到这里我将开始的跳转错误理解为7C0H:0(实际上是0:7C00H)。 因为这里同时有两个错误的理解,所以想不明白boot.asm编译出来运行的结果为什么和想像中不一样。按我的理解,去掉“org 07c00h”这一句以后也能正常执行,但实际执行的结果却是去掉以后显示出来的是一串乱码,也就是说程序其他指令是正常的,只是BootMessage这个变量的偏移错了。后来在NASM手册中看到了对ORG的说明,知道了ORG的正确含义,对两种运行结果进行分析,得出结论,程序是被加载到0:7C00H处,而不是7C0H:0处。上网一搜果然是这样,后来我发现《自》一书中明确讲了是跳转到0:7C00H,只是我一直没看到,浪费了不少时间。 这里我还想说一下,去掉“org 07c00h”后 CALL DispStr 和 JMP $ 不会错误跳转的原因:BootMessage编译的时候是以第一条指令(MOV AX,CS)为原点的偏移,以数值形式写入指令中,比如这里为 001FH(也就是说MOV AX,BootMessage被编译成了 MOV AX,001FH),由于程序在运行的时候被加载到了 0:7C00H 处,所以"Hello, OS world!"被加载到了 0*10H + 07C00H + 001FH 处,可见地址相差了07C00H读取的数据当然错了。再看看CALL和JMP,这里的CALL是一个段内直接调用,执行到CALL的时候会将IP入栈(IP总是指向下一条指令的偏移),然后给当前IP加上一个差值,该差值是一个以IP为原点的偏移。返回的时候IP出栈,子程序结束。JMP $ 相对CALL来说就是少了出入栈两个过程而已,跳转过程是当前IP加上以IP为原点的偏移。可以看出他们都是以自身所在位置为原点跳转,所以不会错。 最后说说OSHello.asm(不是boot.asm)的编译和运行,为了方便,将OSHello.asm和MASM放同一路径。MASM是DOS程序(NASM也一样),所以编译过程是 运行CMD -> 进入MASM所在目录(必须和LINK、ML放一起) -> 输入“ml OSHello.asm” 即可完成编译。编译出来的文件名是默认的OSHello.exe,接下来只要写入软盘就可以运行了,但是这个MASM编译LINK链接出来的OSHello.exe是一个带MZ文件头(下篇介绍)的可执行程序,所以需要去掉MZ头,可以使用我写的那个工具,在写入软盘的时候会自动判断MZ头并去掉,于是可以随便新建一个文件,将扩展名改为IMG,将OSHello.exe写入,如果准备用VPC运行还需要点一下“1.44M映像”按钮。软盘做好了,用虚拟机加载映像并启动就可以看到结果了。 本篇到这里就结束了,下一篇讲的是同一个16位代码段中实模式和保护模式的互相跳转。 操作系统实践2--同一个段下实模式和保护模式相互跳转 先看代码,代码不会解释的太详细,只把相关代码作用提一下,需要详细解释的仍然以补充形式给出。 general.asm定义了一些宏和符号常量 ;GDTR用伪描述符结构 ;Descriptor 段描述符 8字节 ATDW = 92H ;存在的可读写数据段属性 ;设置段描述符,其中参数Limit和Base为32位存贮器操作数,Attr为16位常数(高字节的低4位必须为0) ;打开地址线A20 ;关闭地址线A20
INCLUDE GENERAL.ASM MOV AX,LOADER_SEG+DSEG ;补充1 ;设置代码段描述符 ;设置显存数据段描述符 ;设置堆栈段描述符 CLI ;关中断 ENABLEA20 ;打开地址线A20 ;CRO的PE位置1,进入保护模式 ;跳转 补充2 PROTECTED: MOV AH,0CH MOV ESI,OFFSET PMMessage ;将PMMessage显示出来 CALL DispReturn ;换行 MOV AX,SelectorString ;补充3 ;CRO的PE位置0,回到实模式 ;跳转 补充4 REAL: MOV AX,LOADER_SEG+DSEG DISABLEA20 ;关闭地址线A20 STI ;开中断 ;显示1个L,表示回到了实模式 JMP $ ;程序到这里结束 TextRead PROC TextWrite PROC DispAL PROC ;将AL中的数据用十六进制数(2个)显示 DispReturn PROC
;数据段GDT GdtLen = $ - GDT ;GDT长度 FGDTR GDTR_STRUC<GdtLen,0> ;伪描述符 SELECTOR_BASE DD ? ;GDT选择子 ;保存字符串数据段 DSEG_STACK SEGMENT USE16 END START 由于保护模式的详细说明内容太多,再加上很多资料都有介绍(比如《80》和《自》),所以这里就不再累述。 为了解释补充1先看一下MZ文件头的结构: 偏移 长度 描述 这里简化程序的运行过程,只说我们关心的部分。MSDOS从重定位表的偏移来确定重定位表在MZ头的位置,从重定位表项数来确定重定位表的长度,然后利用重定位表的内容给程序中每一个段值的引用加上一个修正值。 重定位表中每两个字节表示一个需要重定位的位置,也叫做重定位指针(不是需要加上的修正值)。比如MOV DS,DSEG这样的代码,重定位表就会有一个重定位指针,指向编译出来的EXE文件中该语句DSEG的值,MSDOS运行该程序的时候就会给它加上一个修正值。这样做的原因是,编译的时候,编译器无法确定程序运行的时候会被加载到什么位置,所以需要运行的时候由操作系统来修正。 补充1:本段代码被引导扇区程序(本篇后边给出)加载到LOADER_SEG:0处,引导扇区程序并没有MSDOS的重定位能力,所以在LOADER中每一个对段值的引用都需要加上LOADER_SEG。 补充2:这里用机器码完成了一个跳转,而且是强制作为一个段间跳转处理。这里需要一个段间跳转的原因是,当CR0的PE位被置1后,处理器就已经运行在保护模式下了,但这时CS中还是实模式下的段值,而不是保护模式下的选择子,所以需要这样一个将选择子放入CS的段间跳转。如果不做这样一个跳转,那么当CPU取指令的时候,CS中虽然是一个段值,但是CPU却会当成一个选择子来处理,如此一来,就无法正确取指令。又由于指令预取队列的存在,这个跳转指令在PE位被置1之前就已经在指令预取队列中了, JMP DWORD PTR [LQZT] MOV BX,$+6 ;这里MOV和JMP各占3字节,所以加6 这里LQZT是代码段下的标号,MASM编译的时候会自动加上段超越前缀,所以不用指定。看起来似乎是错的,因为读CS:[LQZT]内存数据的时候,处理器已经运行在保护模式下了,CS中的段值会被当作选择子来寻址。实际上保护模式下,CPU直接从段描述符高速缓冲寄存器获取基址,属性等信息,而此时段描述符高速缓冲寄存器中的基址是正确的,所以这里不会错。回过头来看刚才的这个结论“该跳转代码后的一定长度的指令都被预取了,所以就算不做跳转,被预取的代码也能正确执行”,想想段描述符高速缓冲寄存器可以知道,就算指令不被预取,也不会有问题,而且没有长度限制。 补充3:这里是给堆栈段加载合适的段属性,以使对应段描述符高速缓冲寄存器能有合适的段界限和段属性,这样才能正常的回到实模式。段描述符高速缓冲寄存器(也叫影子寄存器),它不仅仅在保护模式下,起作用,在实模式下仍然起作用。目前我并没有看到任何资料详细介绍了段描述符高速缓冲寄存器,就算在INTER手册上也只看到了简单的几句话,可能是没找到吧。在实模式下段描述符高速缓冲寄存器必须有合适的段界限和段属性,否则,一旦使用对应的段寄存器就会出错。我不知道CPU如何判断是否合适,我只知道下面的属性和界限是合适的(这里说一下BOCHS的dump_cpu能看到段描述符高速缓冲寄存器的值):界限0FFFFH(连0FFFEH都不行),属性93H(数据段,包括堆栈段,当然92H也可以)、9BH(代码段)。CS是不能直接修改的,所以保护模式回到实模式,必须从16位段跳转(下一篇详述)。通过上面的的结论可以知道,如果设置堆栈段描述符的界限的时候不是设置为TopOfStack而是0FFFFH,那么这一步就可以省略。其实对实模式来说,段描述符高速缓冲寄存器,没有什么用处,不知道为什么CPU要这样设计,BOCHS和VPC在模拟实模式下的CPU的时候可能就没有对段描述符高速缓冲寄存器做限制,于是在回实模式前,不更改段属性也不会错。 补充4:这里又用机器码完成了一个跳转,同样强制作为一个段间跳转。当CR0的PE位被置0后,处理器已经运行在实模式下了,但这时CS中还是保护模式下的选择子,所以需要这样一个将段值放入CS的段间跳转。同样这里的跳转可以去掉。再来看看段间直接转移指令和段间间接转移指令。分析在上一个跳转中,不能使用段间直接转移指令的原因,可以很容易得出结论,这里直接使用段间直接转移指令 JMP FAR PTR REAL在MSDOS下运行是可以的,但因为引导扇区程序没有重定位功能,所以这里不能这样写。那段间间接转移指令呢?先看一下这里的段间间接转移指令: JMP DWORD PTR [LQZT2] MOV BX,$+6 ;这里MOV和JMP各占3字节,所以加6 想一下,当CR0的PE位被置0后,处理器已经运行在实模式下了,如果这时候用段间间接跳转指令,那么读CS:[LQZT]内存数据的时候,处理器会将CS中的值作为段值来寻址,但实际上此时CS中却是一个选择子,于是,这里是不能用段间间接转移指令。再回想一下补充2中的内容,如果去掉那一个跳转,CS中将一直是段值,于是这里可以用段间跳转指令了!很有意思,不是吗。 补充5:这里的段界限必须是0FFFFH,不能是SegCodeLen,否则在最后回到实模式后不能调用中断。主要原因还是段描述符高速缓冲寄存器必须有正确的值。这里困扰了好久...... loader.asm到这里就结束了,要运行loader还需要一个引导扇区程序: CSEG SEGMENT ;清屏,并将光标移到左上角,因为BOCHS启动的时候输出了很多信息,所以清屏 MOV BH,0 ;软驱复位 ;读LOADER JMP load_sys load_sys_suc: ;这里跳转的LOADER_SEG:0 注意代码段必须放最前边 ORG 510 ;填充剩下的空间,使生成的二进制代码恰好为512字节 用命令ML bootLoad.asm 和 ML loader.asm 分别编译并链接,然后用写扇区工具将bootloade.exe写入第一个扇区,将loader.exe从第二个扇区开始写入。然后就可以用虚拟机运行了。 下一篇对loader.asm作一点修改,让保护模式处于32位段下。 操作系统实践3--16位段下实模式和32位段下保护模式相互跳转 本篇对上一篇的loader.asm做了一点改动,让保护模式在32位段下。 ----------------------------------- loader32.asm --------------------------------------------- INCLUDE GENERAL.ASM ;GENERAL.ASM没有变,在此不再给出 .386P ;16位代码段 CSEG16 SEGMENT USE16 ASSUME CS:CSEG16,DS:DSEG START: MOV AX,LOADER_SEG+DSEG MOV DS,AX MOV AX,LOADER_SEG+DSEG_STACK MOV SS,AX MOV SP,TopOfStack
;伪描述符基地址赋值,界限定义时已设置 XOR EAX,EAX ;EAX清0 MOV AX,DS SHL EAX,4 ADD EAX,OFFSET GDT ;EAX为GDT基地址 MOV FGDTR.BASE,EAX ;设置16位代码段描述符 XOR EAX,EAX ;EAX清0 MOV AX,CS SHL EAX,4 ;EAX为DES_CODE16基地址 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,0FFFFH ;这里必须为0FFFFH,否则回到实模式后CS影子寄存器界限不对 SetDescriptor <DES_CODE16>,SELECTOR_LEN,SELECTOR_BASE,ATCE ;设置32位代码段描述符 XOR EAX,EAX ;EAX清0 MOV AX,LOADER_SEG+CSEG32 SHL EAX,4 ;EAX为DES_CODE32基地址 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,SegCodeLen32 SetDescriptor <DES_CODE32>,SELECTOR_LEN,SELECTOR_BASE,ATCE+DA_32 ;设置显存数据段描述符 MOV SELECTOR_BASE,0B8000H MOV SELECTOR_LEN,0FFFFH SetDescriptor <DES_VIDEO>,SELECTOR_LEN,SELECTOR_BASE,ATDW ;设置要显示的字符串所在数据段描述符 XOR EAX,EAX MOV AX,LOADER_SEG+DSEG_STR SHL EAX,4 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,0FFFFH SetDescriptor <DES_STRING>,SELECTOR_LEN,SELECTOR_BASE,ATDW ;设置5MB处读写数据段描述符 MOV SELECTOR_BASE,500000H MOV SELECTOR_LEN,0FFFFH SetDescriptor <DES_OUT>,SELECTOR_LEN,SELECTOR_BASE,ATDW ;设置堆栈段描述符 XOR EAX,EAX MOV AX,SS SHL EAX,4 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,0FFFFH SetDescriptor<DES_STACK>,SELECTOR_LEN,SELECTOR_BASE,ATDW
LGDT FGDTR ;加载GDTR CLI ;关中断 ENABLEA20 ;打开地址线A20 ;CRO的PE位置1,进入保护模式 MOV EAX,CR0 OR EAX,1 MOV CR0,EAX ;进入32位段下的保护模式 补充1 DB 66H DB 0EAH DD OFFSET PROTECTED32 DW SelectorCode32 PROTECTED16: ;CRO的PE位置0,回到实模式 MOV EAX,CR0 AND AL,0FEH MOV CR0,EAX ;跳回实模式 DB 0EAH DW OFFSET REAL DW LOADER_SEG ;如果在MSDOS下运行,这里LOADER_SEG需要改为CSEG
REAL: MOV AX,LOADER_SEG+DSEG_STACK MOV SS,AX MOV AX,LOADER_SEG+DSEG MOV DS,AX MOV SP,TopOfStack
DISABLEA20 ;关闭地址线A20 STI ;开中断 ;调用中断输出5个L,以示回到了实模式 MOV BH,0H MOV AL,'L' MOV BL,47H MOV CX,5H MOV AH,9 INT 10H JMP $ ;程序到这里结束 SegCodeLen16 = $ - START CSEG16 ENDS ;32位代码段 CSEG32 SEGMENT ASSUME CS:CSEG32,DS:DSEG PROTECTED32: ;设置选择子 MOV AX,SelectorString MOV DS,AX
MOV AX,SelectorOut MOV ES,AX MOV AX,SelectorVideo MOV GS,AX MOV AX,SelectorStack MOV SS,AX MOV ESP,TopOfStack MOV AH,0CH XOR EDI,EDI MOV EDI,(80*10+0)*2 MOV ESI,OFFSET PMMessage ;将PMMessage显示出来 _AGAIN: MOV AL,[ESI] INC ESI TEST AL,AL JZ _NEXT MOV GS:[EDI],AX ADD EDI,2 JMP _AGAIN _NEXT: CALL DispReturn ;换行 CALL TextRead ;显示5MB处内存 CALL TextWrite ;写5MB处内存 CALL TextRead ;显示更新后的5MB处内存 ;这里ES影子寄存器都有正确的界限和属性,所以不用修改
;跳转到16位段的保护模式 补充2 DB 0EAH DW OFFSET PROTECTED16 DW 0 DW SelectorCode16 TextRead PROC XOR ESI,ESI MOV ECX,8 _LOOP: MOV AL,ES:[ESI] CALL DispAL INC ESI LOOPW _LOOP CALL DispReturn RET TextRead ENDP TextWrite PROC PUSH ESI PUSH EDI XOR ESI,ESI XOR EDI,EDI MOV ESI,OFFSET StrTest CLD NUM1: lodsb test AL,AL JZ NUM2 MOV ES:[EDI],AL INC EDI JMP NUM1 NUM2: POP EDI POP ESI RET TextWrite ENDP DispAL PROC ;将AL中的数据用十六进制数(2个)显示 PUSH ECX PUSH EDX MOV AH,0CH MOV DL,AL ;AL中是读出的字符 SHR AL,4 ;先处理高4位 MOV ECX,2 BEGIN: AND AL,01111B CMP AL,9 JA NUM1 ;AL中数据大于9转移 ADD AL,'0' ;AL小于等于9(ASCII 9后面是“:” 不是字母A) JMP NUM2 NUM1: SUB AL,0AH ;AL大于等于10就先减10 ADD AL,'A' NUM2: MOV GS:[EDI],AX ADD EDI,2 MOV AL,DL LOOP BEGIN ADD EDI,2 POP EDX POP ECX RET DispAL ENDP DispReturn PROC PUSH EAX PUSH EBX MOV EAX,EDI MOV BL,160 DIV BL AND EAX,0FFH INC EAX MOV BL,160 MUL BL MOV EDI,EAX POP EBX POP EAX RET DispReturn ENDP SegCodeLen32 = $ - 1 CSEG32 ENDS ;数据段GDT DSEG SEGMENT USE16 GDT Descriptor <> ;空描述符 DES_CODE16 Descriptor <> ;16位代码段描述符 DES_CODE32 Descriptor <> ;32位代码段描述符 DES_VIDEO Descriptor <> ;显存段描述符 DES_STRING Descriptor <> ;读取段描述符 DES_OUT Descriptor <> ;写入段描述符 DES_STACK Descriptor <> ;堆栈段描述符 GdtLen = $ - GDT ;GDT长度 FGDTR GDTR_STRUC<GdtLen,0> ;伪描述符 SELECTOR_BASE DD ? SELECTOR_LEN DD ? ;GDT选择子 SelectorCode16 = DES_CODE16 - GDT ;16位代码段选择子 SelectorCode32 = DES_CODE32 - GDT ;32位代码段选择子 SelectorVideo = DES_VIDEO - GDT ;显存段选择子 SelectorString = DES_STRING - GDT ;要显示的字符串所在数据段选择子 SelectorOut = DES_OUT - GDT ;5MB处读写数据段选择子 SelectorStack = DES_STACK - GDT ;堆栈段选择子 DSEG ENDS ;保存字符串数据段 DSEG_STR SEGMENT PMMessage DB "In Protect Mode now. @_@",0 StrTest DB "ABCDEFGHIJKLMNOPQRSTUVWXYZ",0 DSEG_STR ENDS DSEG_STACK SEGMENT TEMP DB 512 DUP(0) TopOfStack = $ - TEMP DSEG_STACK ENDS END START ----------------------------------- loader32.asm 完 ------------------------------------------- 先说下为什么回到实模式需要先回到16位段的保护模式下,再回到实模式。因为保护模式在32位段下,所以CS的段描述符高速缓冲寄存器中段属性的D位被置1,表示这是一个32位段。因为在实模式下无法修改段描述符高速缓冲寄存器中的段属性和界限,所以必须在回到实模式前修改,而修改的办法就是跳转到16位段。 补充1:这是一个跳转指令,在保护模式下从16位段跳转到32位段。因为是在保护模式下跳转,所以放入CS的是选择子SelectorCode32。因为PROTECTED32是32位段下的一个标号,32位段的段内偏移是32位,所以跳转中的偏移是DD OFFSET PROTECTED32。0EAH表示一个段间直接跳转,但是由于这是16位段下的跳转,它本应该是使用16位偏移的,而我们需要它使用32位偏移,所以在0EAH前一个字节加上操作数长度前缀66H。另外由于处理器在跳转到32位代码段的时候,如果偏移是16位,那么会在高16位补0,所以这里也可以改为: DB 0EAH DD OFFSET PROTECTED32 ORG $-2 ;因为这里OFFSET PROTECTED32没有超过0FFFFH,所以完全可以这样处理 DW SelectorCode32 补充2:这是32位代码段下的段间直接跳转,它需要32位偏移作操作数,而PROTECTED16是16位段下的标号,OFFSET PROTECTED16是16位,所以需要补16位的0。当然也可以使用操作数长度前缀: DB 66H DB 0EAH DW OFFSET PROTECTED16 DW SelectorCode16
操作系统实践4--建立并使用局部描述符表 本篇建立了局部描述符表,并让1个32位代码段描述符位于局部描述符表中: ----------------------------- loaderL.asm -------------------------------------------- INCLUDE GENERAL.ASM ;GENERAL.ASM加了几个符号,见补充1 .386P ;16位代码段 CSEG16 SEGMENT USE16 ASSUME CS:CSEG16,DS:DSEG,ES:DSEGLDT START: MOV AX,LOADER_SEG+DSEG MOV DS,AX MOV AX,LOADER_SEG+DSEG_STACK MOV SS,AX MOV SP,TopOfStack MOV AX,LOADER_SEG+DSEGLDT MOV ES,AX
;伪描述符基地址赋值,界限定义时已设置 XOR EAX,EAX ;EAX清0 MOV AX,DS SHL EAX,4 ADD EAX,OFFSET GDT ;EAX为GDT基地址 MOV FGDTR.BASE,EAX ;设置16位代码段描述符 XOR EAX,EAX ;EAX清0 MOV AX,CS SHL EAX,4 ;EAX为DES_CODE16基地址 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,0FFFFH ;这里必须为0FFFFH SetDescriptor <DES_CODE16>,SELECTOR_LEN,SELECTOR_BASE,ATCE ;设置32位代码段描述符 XOR EAX,EAX ;EAX清0 MOV AX,LOADER_SEG+CSEG32 SHL EAX,4 ;EAX为DES_CODE32基地址 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,SegCodeLen32 SetDescriptor <DES_CODE32>,SELECTOR_LEN,SELECTOR_BASE,ATCE+DA_32 ;设置显存数据段描述符 MOV SELECTOR_BASE,0B8000H MOV SELECTOR_LEN,0FFFFH SetDescriptor <DES_VIDEO>,SELECTOR_LEN,SELECTOR_BASE,ATDW ;设置堆栈段描述符 XOR EAX,EAX MOV AX,SS SHL EAX,4 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,0FFFFH SetDescriptor<DES_STACK>,SELECTOR_LEN,SELECTOR_BASE,ATDW ;设置LDT在GDT中的描述符 XOR EAX,EAX MOV AX,LOADER_SEG+DSEGLDT SHL EAX,4 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,SegLDTCodeLen SetDescriptor<DES_LDT>,SELECTOR_LEN,SELECTOR_BASE,DA_LDT
;设置LDT中的描述符 XOR EAX,EAX MOV AX,LOADER_SEG+CSEGLDT SHL EAX,4 MOV SELECTOR_BASE,EAX MOV SELECTOR_LEN,SegLDTCodeLen SetDescriptor<ES:LDT_CSEG>,SELECTOR_LEN,SELECTOR_BASE,ATCE+DA_32
LGDT FGDTR ;加载GDTR CLI ;关中断 ENABLEA20 ;打开地址线A20 ;CRO的PE位置1,进入保护模式 MOV EAX,CR0 OR EAX,1 MOV CR0,EAX ;进入32位段下的保护模式 DB 66H DB 0EAH DD OFFSET PROTECTED32 DW SelectorCode32 PROTECTED16: ;CRO的PE位置0,回到实模式 MOV EAX,CR0 AND AL,0FEH MOV CR0,EAX ;跳回实模式 DB 0EAH DW OFFSET REAL DW LOADER_SEG REAL: MOV AX,LOADER_SEG+DSEG_STACK MOV SS,AX MOV AX,LOADER_SEG+DSEG MOV DS,AX MOV SP,TopOfStack
DISABLEA20 ;关闭地址线A20 STI ;开中断 ;调用中断输出5个L,以示回到了实模式 MOV BH,0H MOV AL,'L' MOV BL,47H MOV CX,5H MOV AH,9 INT 10H JMP $ ;程序到这里结束 SegCodeLen16 = $ - 1 CSEG16 ENDS ;32位代码段 CSEG32 SEGMENT ASSUME CS:CSEG32,DS:DSEG PROTECTED32: ;设置选择子 MOV AX,SelectorString MOV DS,AX MOV AX,SelectorVideo MOV GS,AX MOV AX,SelectorStack MOV SS,AX MOV ESP,TopOfStack MOV AH,0CH XOR EDI,EDI MOV EDI,(80*10+0)*2 MOV ESI,OFFSET PMMessage ;将PMMessage显示出来 _AGAIN: MOV AL,[ESI] INC ESI TEST AL,AL JZ _NEXT MOV GS:[EDI],AX ADD EDI,2 JMP _AGAIN _NEXT: CALL DispReturn ;换行 ;在使用LDT前必须将正确的选择子装入LDTR 补充2 MOV AX,SelectorLDT LLDT AX
;跳转到LDT记录的32位代码段 DB 0EAH DD OFFSET LDTSTART DW SelectorCSEGLDT DispReturn PROC PUSH EAX PUSH EBX MOV EAX,EDI MOV BL,160 DIV BL AND EAX,0FFH INC EAX MOV BL,160 MUL BL MOV EDI,EAX POP EBX POP EAX RET DispReturn ENDP SegCodeLen32 = $ - 1 CSEG32 ENDS ;数据段GDT----------------------------------------------------------- DSEG SEGMENT USE16 GDT Descriptor <> ;空描述符 DES_CODE16 Descriptor <> ;16位代码段描述符 DES_CODE32 Descriptor <> ;32位代码段描述符 DES_VIDEO Descriptor <> ;显存段描述符 DES_STACK Descriptor <> ;堆栈段描述符 DES_LDT Descriptor<> ;LDT在GDT中的描述符 FGDTR GDTR_STRUC<GdtLen,0> ;伪描述符 SELECTOR_BASE DD ? SELECTOR_LEN DD ? ;GDT选择子 SelectorCode16 = DES_CODE16 - GDT ;16位代码段选择子 SelectorCode32 = DES_CODE32 - GDT ;32位代码段选择子 SelectorVideo = DES_VIDEO - GDT ;显存段选择子 SelectorStack = DES_STACK - GDT ;堆栈段选择子 SelectorLDT = DES_LDT - GDT ;记录LDT的选择子 GdtLen = $ - 1 ;GDT长度 DSEG ENDS ;保存字符串数据段------------------------------------------------ DSEG_STR SEGMENT PMMessage DB "In Protect Mode now. @_@",0 StrTest DB "ABCDEFGHIJKLMNOPQRSTUVWXYZ",0 DSEG_STR ENDS ;堆栈段---------------------------------------------------------- DSEG_STACK SEGMENT TEMP DB 512 DUP(0) TopOfStack = $ - 1 DSEG_STACK ENDS ;LDT段----------------------------------------------------------- DSEGLDT SEGMENT LDT LABEL BYTE LDT_CSEG Descriptor<> ;LDT记录的32位代码段描述符 SelectorCSEGLDT = LDT_CSEG - LDT + SA_TIL ;选择子TI位必须为1 LDTLen = $ - 1 DSEGLDT ENDS ;LDT记录的32位代码段----------------------------------------------- CSEGLDT SEGMENT LDTSTART: MOV AX,SelectorVideo MOV GS,AX ;输出1个Q MOV EDI,(80*12+0)*2 MOV AH,0CH MOV AL,'Q' MOV GS:[EDI],AX ;跳转到16位段的保护模式,准备跳回实模式 DB 0EAH DW OFFSET PROTECTED16 DW 0 DW SelectorCode16 SegLDTCodeLen = $ - 1 CSEGLDT ENDS END START ----------------------------- loaderL.asm 完------------------------------------------ 补充1:loaderL.asm中有两个符号DA_LDT和SA_TIL,它们在GENERAL.ASM中定义,需要新加: DA_LDT = 82h ;局部描述符表段类型值 SA_TIL = 4 ;LDT中的描述符 补充2:这里的操作是将GDT中的LDT选择子装入LDTR。LDTR和GDTR完全不同,GDTR是48位直接记录的是32位基地址和16位段界限,而LDTR是16位记录的是LDT在GDT中的选择子。LDTR更像一个普通的段寄存器,都是装入一个16位的选择子,都有一个高速缓冲寄存器,高速缓冲寄存器都是记录的基地址,界限和属性。 原文网址: http://hi.baidu.com/liuqizealot/blog/item/30a2708de93c311ab31bba91.html 操作系统实践5--段特权级 本篇只涉及段特权级的内容,关于各种门也只涉及调用门。
1. CPL表示的是程序或者说任务的当前特权级,它不属于某个段,当前的程序或任务不可能同时表现出2种特权级,那么CS和SS的第0位和第1位应该总是相同的。尝试将RPL异于CPL的数据段选择子装入SS会引起异常。另外后面在论述跳转的时候可以看到,它们总是相同的。 2.DPL表示的是某个段或门的特权级,当程序要访问段A的时候,会将CPL和段A的DPL作比较,以确定程序是否有权限访问段A。 3.RPL表示选择子是否有权限访问其所指向的段。选择子指向一个段描述符,段描述符指向一个段,段的特权级由段描述符中的DPL决定。当程序需要访问段A的时候,需要先通过段A的选择子加载段A的段描述符,然后才能访问段A,在这个过程中CPU会先将段A选择子中的RPL和段A描述符中的DPL作比较,确定选择子是否有权限访问其所指向的段,成功后才是CPL和段A的DPL的比较。 RPL、 DPL之间的比较规则与CPL、DPL之间的比较规则一致。举个例子解释前面这句话的意思:比如当前程序要访问一个数据段A,那么CPL不能大于段A的DPL,否则失败(后面会讲到这一点)。这是CPL和段A的DPL的比较规则,那么同样段A的RPL、DPL也遵循这样的规则,也就是段A的RPL不能大于段A的DPL。 RPL和CPL不会进行比较。 4.不管什么情况下,相同特权级之间的访问总是不会错的,所以后面的讨论中通常会忽略相同特权级之间的比较。 5.保护模式下代码段中可以存放数据,但数据段中不能存放代码(跳转不过去)。所以代码段既可以获取代码段中的数据(被读的代码段属性需要可读,就算是获取自身段内数据也需要可读),也可以获取数据段中的数据(数据段总是可读的),还可以跳转到其他代码段。而数据段除了被读取,什么都做不了。 6.代码段分为一致代码段和非一致代码段,这会对特权级比较产生影响,是否一致由段描述符的第42位决定。数据段和代码不同,它总是非一致的。代码段只在作为被访问一方(或者说目标代码段)时一致性才会对特权级比较产生影响,在作为访问一方时没有区别。是否一致代码段的区别是,在段间跳转过程中,如果目标代码段是一个特权级更高的一致代码段,那么跳转成功,并且CPL不会改变(CS和SS都不会变),于是CPL异于目标代码段的RPL(CPL数值更大,特权级更低);如果目标代码段是一个特权级更高的非一致代码段,那么跳转是会失败的,此时需要使用调用门。另外,如果目标代码段是一个特权级更低的代码段(不论是否一致),那么跳转总是会失败的,除非使用RETF跳转。 7.如果当前程序要访问一个数据段A,那么CPL不能大于段A的DPL,否则失败。也就是说当前程序不能访问特权级更高的数据段。CPL可以小于段A的DPL,也就是说当前程序可以访问特权级更低的数据段。当前程序对调用门和TSS的访问规则与此一致。 8.特权级的检查是在选择子被装入段寄存器的时候进行的。先看相对简单一点的数据段A选择子装入DS的情况(装入ES,FS,GS类似,这里以DS为例说明):段A的DPL必须大于RPL,同时还必须大于CPL(第7点),否则产生异常,段A的RPL不和CPL进行比较。 另外,当程序向低特权级跳转时,会检查CPL和DS指向的段的DPL,如果DPL小于CPL,那么DS会被加载空描述符的选择子。 9.数据段A选择子装入SS的情况(只有数据段选择子才能装入SS,代码段选择子不能装入SS,不管是否可读):段A的RPL和DPL都必须和CPL相等,否则失败。 10.代码段A选择子装入DS的情况(装入ES,FS,GS类似,这里以DS为例说明):如果段A不可读,装入失败。如果段A是可读的非一致代码段,那么CPL、段A的RPL都必须等于段A的DPL,否则装入就会失败。如果段A是可读的一致代码段,那么CPL、段A的RPL都可以大于段A的DPL,但不能小于段A的DPL。 11.最后是代码段的段间跳转。段间跳转可以用JMP、CALL、RETF和调用门。从高特权级到低特权级只能用RETF;从低特权级到高特权级非一致代码段,只能用调用门,而且还必须是用CALL指令来使用调用门;从低特权级到高特权级一致代码段JMP、CALL、调用门都可以。 general.asm添加、修改了一些内容: ================================= general.asm ====================================== LOADER_SEG EQU 5000H ;程序加载后,所在内存段值 ;--------------------------------------------------------------- ; 描述符类型值说明 ; 其中: ; DA_ : Descriptor Attribute ; D : 数据段 ; C : 代码段 ; S : 系统段 ; R : 只读 ; RW : 读写 ; A : 已访问 ; 其它 : 可按照字面意思理解 DA_32 = 4000H ;32位段 ;DPL DA_DPL0 = 00H DA_DPL1 = 20H DA_DPL2 = 40H DA_DPL3 = 60H ;--------------------------------------------------------------- ;--------------------------------------------------------------- ; 存储段描述符类型值说明 DA_DR = 90H ;存在的只读数据段类型值 DA_DRW = 92H ;存在的可读写数据段属性值 DA_DRWA = 93H ;存在的已访问可读写数据段类型值 DA_C = 98H ;存在的只执行代码段属性值 DA_CR = 9AH ;存在的可执行可读代码段属性值 DA_CCO = 9CH ;存在的只执行一致代码段属性值 DA_CCOR = 9EH ;存在的可执行可读一致代码段属性值 ;-------------------------------------------------------------- ;-------------------------------------------------------------- ; 系统段描述符类型值说明 DA_LDT = 82H ;局部描述符表段类型值 DA_TaskGate = 85H ;任务门类型值 DA_386TSS = 89H ;可用 386 任务状态段类型值 DA_386CGate = 8CH ;386 调用门类型值 DA_386IGate = 8EH ;386 中断门类型值 DA_386TGate = 8FH ;386 陷阱门类型值 ;-------------------------------------------------------------- ;-------------------------------------------------------------- ;选择子类型值说明 ; SA_ 表示Selector Attribute ;RPL SA_RPL0 = 0 SA_RPL1 = 1 SA_RPL2 = 2 SA_RPL3 = 3 ;TI SA_TIG = 0 ;GDT中的描述符 SA_TIL = 4 ;LDT中的描述符 ;----------------------------------------------------------------- ;GDTR用伪描述符结构 GDTR_STRUC STRUC LIMIT DW 0 ;低16位段界限 BASE DD 0 ;高32位基地址 GDTR_STRUC ENDS
;Descriptor 段描述符 8字节 ;Base 段基地址,32位,双字DD ;Limit 段界限,20位,双字DD低20位 ;Attr 段属性,12位,字DW,其中高字节的低4位始终为0(段界限的高4位) Descriptor STRUC LimitL DW 0 ;段界限1 (0~15) BaseL DW 0 ;段基地址1 (0~15) BaseM DB 0 ;段基地址2 (16~23) Attribute DW 0 ;属性1 + 段界限2 +属性2 BaseH DB 0 ;段基地址3 (23~31) Descriptor ENDS ;设置段描述符,该宏和以前不同,使用该宏之前,将32位基地址放入EAX,20位界限放入EDX ;Attr为常数(高字节的低4位始终为0),注意EAX,EDX的值会被改变 SetDescriptor MACRO DesName,Attr MOV &DesName.LimitL,DX ;LimitL赋值 MOV &DesName.BaseL,AX ;BaseL赋值 SHR EAX,16 MOV &DesName.BaseM,AL ;BaseM赋值 MOV &DesName.Attribute,Attr ;Attr赋值 MOV EDX,16 OR BYTE PTR &DesName.Attribute+1,DL ;Limit 16~19位赋值 MOV &DesName.BaseH,AH ;BaseH赋值 ENDM ;门描述符结构体,8字节 GateDes STRUC OffsetL DW 0 ;32位偏移的低16位 Selector DW 0 ;选择子 PCount DB 0 ;参数计数 GType DB 0 ;类型 OffsetH DW 0 ;32位偏移的高16位 GateDes ENDS ;设置门描述符,使用该宏之前,将32位偏移放入EAX,EAX会被改变 ;GATE_SELECTOR为选择子,GATE_COUNT为参数计数,GATE_TYPE为类型 SetGateDes MACRO GDesName,GATE_SELECTOR,GATE_COUNT,GATE_TYPE MOV GDesName.OffsetL,AX ;OffsetL赋值
MOV GDesName.Selector,GATE_SELECTOR ;Selector赋值 MOV GDesName.PCount,GATE_COUNT ;PCount赋值
MOV GDesName.GType,GATE_TYPE ;GType赋值
SHR EAX,16 MOV GDesName.OffsetH,AX ;OffsetH赋值 ENDM ENABLEA20 MACRO PUSH AX IN AL,92H OR AL,2 OUT 92H,AL POP AX ENDM DISABLEA20 MACRO PUSH AX IN AL,92H AND AL,0FDH OUT 92H,AL POP AX ENDM =================================== general.asm 完 ================================= PLevel.asm先通过RETF从高特权级到低特权级,然后通过调用门从低特权级到高特权级非一致代码段 =================================== PLevel.asm ===================================== INCLUDE GENERAL.ASM .386P ;16位代码段 CSEG16 SEGMENT USE16 ASSUME CS:CSEG16,DS:DSEG START: MOV AX,LOADER_SEG+DSEG MOV DS,AX MOV AX,LOADER_SEG+DSEG_STACK MOV SS,AX MOV SP,TopOfStack
;伪描述符基地址赋值,界限定义时已设置 XOR EAX,EAX ;EAX清0 MOV AX,DS SHL EAX,4 ADD EAX,OFFSET GDT MOV FGDTR.BASE,EAX ;设置32位代码段描述符 XOR EAX,EAX ;EAX清0 MOV AX,LOADER_SEG + CSEG32 SHL EAX,4 MOV EDX,SegCodeLen32 SetDescriptor <DES_CODE32>,DA_C + DA_32 ;设置显存数据段描述符,因为32位RING2代码段要用到该段,所以这里DPL设置为2 MOV EAX,0B8000H MOV EDX,0FFFFH SetDescriptor <DES_VIDEO>,DA_DRW + DA_DPL2 ;设置堆栈段描述符 XOR EAX,EAX MOV AX,SS SHL EAX,4 MOV EDX,0FFFFH SetDescriptor<DES_STACK>,DA_DRW + DA_32 ;设置调用门描述符 XOR EAX,EAX SetGateDes<DES_GATE>,SelectorGateCode,0,DA_386CGate + DA_DPL2 ;设置调用门代码段描述符,就是通过调用门跳转到的高特权级非一致代码段 XOR EAX,EAX MOV AX,LOADER_SEG + CGATE SHL EAX,4 MOV EDX,GateLen SetDescriptor<DES_GATE_CODE>,DA_C + DA_32 + DA_DPL1 ;设置TSS描述符 XOR EAX,EAX MOV AX,LOADER_SEG + DTSS SHL EAX,4 MOV EDX,TSSLen SetDescriptor<DES_TSS>,DA_386TSS ;设置32位RING2代码段描述符 XOR EAX,EAX MOV AX,LOADER_SEG+CSEGR2 SHL EAX,4 MOV EDX,SegCodeRING2Len SetDescriptor<DES_RING2>,DA_C + DA_DPL2 + DA_32 ;设置RING1堆栈段描述符 XOR EAX,EAX MOV AX,LOADER_SEG+STACK_R1 SHL EAX,4 MOV EDX,TopOfStackR1 SetDescriptor<DES_STACK_R1>,DA_DRWA + DA_DPL1 + DA_32 ;设置RING2堆栈段描述符 XOR EAX,EAX MOV AX,LOADER_SEG+STACK_R2 SHL EAX,4 MOV EDX,TopOfStackR2 SetDescriptor<DES_STACK_R2>,DA_DRWA + DA_DPL2 + DA_32
LGDT FGDTR ;加载GDTR CLI ;关中断 ENABLEA20 ;打开地址线A20 ;CRO的PE位置1,进入保护模式 MOV EAX,CR0 OR EAX,1 MOV CR0,EAX ;进入32位段下的保护模式 DB 66H DB 0EAH DD OFFSET PROTECTED32 DW SelectorCode32 SegCodeLen16 = $ - 1 CSEG16 ENDS ;32位代码段 CSEG32 SEGMENT ASSUME CS:CSEG32,DS:DSEG PROTECTED32: ;设置选择子 MOV AX,SelectorVideo MOV GS,AX MOV AX,SelectorStack MOV SS,AX MOV ESP,TopOfStack ;在使用调用门变换特权级前必须加载TSS选择子到TSS寄存器TR ;因为这里TSS的DPL被设置为0,所以必须在RING0段下执行LTR指令 MOV AX,SelectorTSS LTR AX ;将SS ESP CS EIP依次入栈 补充 PUSH SelectorStackR2 PUSH TopOfStackR2 PUSH SelectorCodeR2 PUSH 0 RETF ;跳转到32位RING2代码段
SegCodeLen32 = $ - 1 CSEG32 ENDS ;调用门目标32位代码段-------------------------------------------------- CGATE SEGMENT MOV AX,SelectorVideo MOV GS,AX MOV EDI,(80*1+0)*2 ;输出一个T MOV AH,0CH MOV AL,'T' MOV GS:[EDI],AX RETF ;返回 GateLen = $ - 1 CGATE ENDS ;32位RING2代码段----------------------------------------------------- CSEGR2 SEGMENT MOV AX,SelectorVideo MOV GS,AX
MOV EDI,(80*0+0)*2 ;输出一个Z MOV AH,0CH MOV AL,'Z' MOV GS:[EDI],AX ;调用门 DB 9AH DD 0 DW SelectorGate JMP $ ;调用门代码段返回后结束程序 SegCodeRING2Len = $ - 1 CSEGR2 ENDS ;描述符数据段----------------------------------------------------------- DSEG SEGMENT GDT Descriptor <> ;空描述符 DES_CODE32 Descriptor <> ;32位代码段描述符 DES_VIDEO Descriptor <> ;显存段描述符 DES_STACK Descriptor <> ;堆栈段描述符 DES_STACK_R1 Descriptor <> ;RING1堆栈段描述符 DES_STACK_R2 Descriptor <> ;RING2堆栈段描述符 DES_TSS Descriptor <> ;TSS描述符 DES_GATE_CODE Descriptor <> ;调用门目标段描述符 DES_RING2 Descriptor <> ;32位RING2代码段描述符 DES_GATE GateDes <> ;调用门描述符 FGDTR GDTR_STRUC<0FFFFH,0> ;伪描述符 ;选择子 SelectorCode32 EQU DES_CODE32 - GDT ;32位代码段选择子 SelectorVideo EQU DES_VIDEO - GDT ;显存段选择子 SelectorStack EQU DES_STACK - GDT ;堆栈段选择子 SelectorTSS EQU DES_TSS - GDT ;TSS选择子 SelectorGateCode EQU DES_GATE_CODE - GDT + SA_RPL1 ;调用门目标段选择子 SelectorGate EQU DES_GATE - GDT + SA_RPL2 ;调用门选择子 SelectorCodeR2 EQU DES_RING2 - GDT + SA_RPL2 ;32位RING2代码段选择子 SelectorStackR1 EQU DES_STACK_R1 - GDT + SA_RPL1 ;RING1堆栈段选择子 SelectorStackR2 EQU DES_STACK_R2 - GDT + SA_RPL2 ;RING2堆栈段选择子 GdtLen = $ - 1 ;GDT长度 DSEG ENDS ;堆栈段---------------------------------------------------------- DSEG_STACK SEGMENT TEMP DB 512 DUP(0) TopOfStack = $ - 1 DSEG_STACK ENDS ;RING1堆栈段----------------------------------------------------- STACK_R1 SEGMENT DB 512 DUP(0) TopOfStackR1 = $ - 1 STACK_R1 ENDS ;RING2堆栈段----------------------------------------------------- STACK_R2 SEGMENT DB 512 DUP(0) TopOfStackR2 = $ - 1 STACK_R2 ENDS ;TSS------------------------------------------------------------- DTSS SEGMENT DD 0 DD TopOfStack ;0级堆栈 DD SelectorStack DD TopOfStackR1 ;1级堆栈 DD SelectorStackR1 DD TopOfStackR2 ;2级堆栈 DD SelectorStackR2 DD 0 ;CR3 DD 0 ;EIP DD 0 ;EFLAGS DD 0 ;EAX DD 0 ;ECX DD 0 ;EDX DD 0 ;EBX DD 0 ;ESP DD 0 ;EBP DD 0 ;ESI DD 0 ;EDI DD 0 ;ES DD 0 ;CS DD 0 ;SS DD 0 ;DS DD 0 ;FS DD 0 ;GS DD 0 ;LDT DW 0 ;调试陷阱标志 DW 0 ;I/O位图基址 DB 0FFH ;I/O位图结束标志 TSSLen = $ - 1 DTSS ENDS END START =================================== PLevel.asm 完 ================================== 补充:32位段下CALL指令入栈CS和SS时是以32位进行的(高16位补0),RETF指令出栈CS和SS也是以32位进行的(高16位被丢弃),所以这里依次入栈了4个32位的操作数。 操作系统实践6--启用内存分页 general.asm中添加的符号 ---------------------------------------------------------------------------------------------------------------------------------- PageDirBase EQU 200000H ;页目录开始地址:2MB DA_G1 = 8000H ;段界限粒度4K PG_P EQU 1 ;页存在属性位 ----------------------------------------------------------------------------------------------------------------------------------- 下面是page.asm ------------------------------------------------ page.asm ----------------------------------------------------------------- INCLUDE GENERAL.ASM ;16位代码段=============================================================== MOV AX,LOADER_SEG + DSEG_STACK GetMemInfo: CMP ddType,1 ;Type为1,才是OS可用的内存 MOV EAX,ddBaseAddrLow JMP GetMemInfo GetMemInfoFail: ;伪描述符基地址赋值,界限定义时已设置----------------------------------- ;设置32位代码段描述符-------------------------------------------- ;设置显存数据段描述符----------------------------------------------- ;设置堆栈段描述符------------------------------------------------- CLI ;关中断 ENABLEA20 ;打开地址线A20 ;CRO的PE位置1,进入保护模式 ;进入32位段下的保护模式 SegCodeLen16 = $ - 1
MOV AX,SelectorStack MOV AX,SelectorDseg ;初始化页目录表和页表-------------------------------------------------- NO_REMAINDER: ;先初始化页目录 PDE_AGAIN: MOV EDI,1000H ;页表是从201000H开始,而页目录表不一定有1024个PDE MOV EAX,PageDirBase MOV EDI,(80*5+0)*2 ;输出1个L SegCodeLen32 = $ - 1
FGDTR GDTR_STRUC<0FFFFH,0> ;伪描述符 ;选择子 GdtLen = $ - 1 ;GDT长度
ddMemSize DD 0 ;内存大小 DSEG ENDS
END START -------------------------------------------------- page.asm 完 ----------------------------------------------- 原文网址: http://hi.baidu.com/liuqizealot/blog/item/8aa96d0625b72373020881a8.html 操作系统实践7--保护模式下的中断处理 这里以IRQ0(时钟中断)为例说一下保护模式下的中断响应过程,我们假定中断响应过程中不会有错误出现,于是忽略一些检测过程: 1.当产生一个IRQ0,首先由中断屏蔽寄存器IMR中的数据(由写入的OCW1决定)判断是否响应该中断,另外还会判断EOI,如果EOI为0,也不会响应中断; 2.如果步骤1中的判断都是响应,那么中断向量号经由INTR传到CPU(这里中断向量号为20H,可通过向主8259A,端口21H写入ICW2设置IRQ0的中断向量号); 3.CPU接收到中断请求后,根据IF位判断是否响应中断,如果响应,那么CPU会将EOI置0,将IF置0; 4.CPU从中断描述符表寄存器IDTR中获取中断描述表IDT基地址,并根据中断向量号取得响应的中断门描述符(中断向量号乘以8H就是要找的描述符在IDT中的偏移,这里是20H*8H),然后从中断门描述符中取得中断处理程序的选择子和偏移,转移到中断处理程序; 5.中断处理程序返回的时候,CPU自动将IF标志置1,EOI视情况而定,本例中需要手动将其置1; 6.中断处理结束。由上可知在中断响应过程中,CPU不会响应其他的中断。 下面是保护模式下中断处理示例代码interrup.asm ;16位代码段=============================================================== MOV AX,LOADER_SEG + DSEG_STACK ;伪描述符基地址赋值,界限定义时已设置----------------------------------- ;设置32位代码段描述符-------------------------------------------- ;设置显存数据段描述符----------------------------------------------- ;设置堆栈段描述符------------------------------------------------- CLI ;关中断 ;加载IDTR ENABLEA20 ;打开地址线A20 ;CRO的PE位置1,进入保护模式 ;进入32位段下的保护模式 SegCodeLen16 = $ - 1
MOV AX,SelectorStack ;设置可编程中断控制器8259A---------------------------------------------- INT 21H ;21H号中断,在屏幕左上角显示一个L
IRETD ;中断返回,保护模式下的中断返回是IRETD,不能是IRET IRETD ;中断返回
SegCodeLen32 = $ - 1
_IDT: ;中断描述符表,为简便放到了GDT数据段里面 FGDTR GDTR_STRUC<0FFFFH,0> ;伪描述符 ;选择子 GdtLen = $ - 1 ;GDT长度
END START ---------------------------- interrup.asm 完 ---------------------------------------- 操作系统实践8--elf文件格式 ELF文件由4部分组成:ELF头(ELF header)、程序头表(Program header table)、节(Sections)和节头表(Section header table) 一.ELF头(ELF header) 它位于EFL文件的最开始处(偏移为0),格式如下: #define EI_NIDENT 16 typedef struct{ unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; }Elf32_Ehdr; 其中的数据类型: 名称 大小 对其 用途 ======== ===== ===== ============== Elf32_Addr 4 4 无符号程序地址 Elf32_Half 2 2 无符号中等大小整数 Elf32_Off 4 4 无符号文件偏移 Elf32_Sword 4 4 有符号大整数 Elf32_Word 4 4 无符号大整数 unsigned char 1 1 无符号小整数 Elf32_Ehdr各项含义如下: e_ident 它的各位依次称为EI_MAG0、EI_MAG1、EI_MAG2、EI_MAG3、EI_CLASS、EI_DATA、 EI_VERSION、EI_PAD。 第0,1,2,3个字节是Magic Number(EI_MAG0、EI_MAG1、EI_MAG2、EI_MAG3),其取值必须依次是0x7f、'E'、'L'、'F',表明这是一个ELF格式的二进制文件。 第4个字节(EI_CLASS)用来确定文件的类型或者说是能力,可能的取值为 ELFCLASSNONE 0 Invalid class ELFCLASS32 1 32-bit objects ELFCLASS64 2 64-bit objects 只有当EI_CLASS为ELFCLASS32才使用上面的Elf32_Addr…这些数据类型。 第5个字节(EI_DATA)给出字节序特性,可能的取值为ELFDATA2LSB(1)或ELFDATA2MSB(2)。对 Intel x86 这里是1,也就是低地址存放最低有效字节。 第6个字节(EI_VERSION)表明ELF头的版本号,必须是EV_CURRENT(elf.h 中定义)。 第7个字节(EI_PAD)标明了在e_ident中无用字节的开始,也就是第7至第EI_NIDENT-1字节暂时没有意义。 e_type 表示该ELF文件的类型,其可能的取值为 ET_NONE 0 No file type ET_REL 1 Relocatable file ET_EXEC 2 Executable file ET_DYN 3 Shared object file ET_CORE 4 Core file ET_LOPROC 0xff00 Processor-specific ET_HIPROC 0xffff Processor-specific e_type = 1,表明它是重定位文件,可以从连接视图去解读它; e_type = 2,表明它是可执行文件,至少可以从装载运行视图去解读它; e_type = 3,表明它是共享动态库文件,同样可以至少从装载运行视图去解读它; e_type = 4,表明它是 Core dump 文件,可以从哪个视图去解读依赖于具体的实现。 其他取值保留。 从装载运行角度,关注的是程序头表,由程序头表的指引把 ELF 文件加载进内存运行它。从连接的角度,关注节头表,由节头表的指引把各个节连接组装起来。 e_machine 指出了运行该程序需要的体系结构,可能的部分取值如下: EM_NONE 0 No machine EM_M32 1 AT&T WE 32100 EM_SPARC 2 SPARC EM_386 3 Intel 80386 EM_68K 4 Motorola 68000 EM_88K 5 Motorola 88000 EM_860 7 Intel 80860 EM_MIPS 8 MIPS RS3000 e_version 表示该ELF文件的版本,通常是EV_CURRENT。 e_entry 表示程序的入口地址。如果没有入口点,为0。 e_phoff 表示程序头表(program header table)在文件中的偏移量,以字节为单位。没有程序头表,为0。 e_shoff 表示节头表(section header table)在文件中的偏移量,以字节为单位。没有节头表,为0。 e_flags 表示相关文件的特定处理器标志,对IA32,为0。 e_ehsize 表示ELF头大小,以字节为单位。 e_phentsize 程序头表(program header table)中每一个条目(program heade)的大小(以字节为单位)。所有的program header都是同样的大小。 e_phnum 程序头表(program header table)条目个数,没有程序头表e_phnum为0。 e_shentsize 节头表(section header table)中每一个条目(section header)的大小(以字节为单位)。所有的section header都是同样的大小。 e_shnum 节头表(section header table)条目个数,没有程序头表e_shnum为0。 e_shstrndx 表示包含节名称的字符串表所在节数(从0开始计数)。假如没有节名称字符表,e_shstrndx为SHN_UNDEF。 二.程序头(Program Header) 程序头表(Program header table)位于EFL文件偏移e_phoff处,程序头表包含e_phnum个大小为e_phentsize字节的程序头。 程序头结构如下: typedef struct { Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; } Elf32_Phdr; Elf32_Phdr各项含义如下: p_type 当前程序头描述的段的类型。类型值和含义如下: PT_NULL 0 PT_LOAD 1 PT_DYNAMIC 2 PT_INTERP 3 PT_NOTE 4 PT_SHLIB 5 PT_PHDR 6 PT_LOPROC 0x70000000 PT_HIPROC 0x7fffffff PT_NULL表示该程序头未使用,不表示任何段。 PT_LOA表示可载入段,由 p_filesz 和 p_memsz 描述。如果该段的内存大小( p_memsz )比文件大小( p_filesz )要大,则多出的字节为0。文件大小不会比内存大小大。 PT_DYNAMIC表示该段指定动态链接信息。 PT_INTERP表示一个null-terminated路径名的位置和大小(作为解释程序)。这种段类型仅仅对可执行文件有意义(尽管它可能发生在一个共享 object 上);它在一个文件中只能出现一次。如果它出现,它必须先于任何一个可载入段入口。 PT_NOTE表示该段指定辅助信息的位置和大小。 PT_SHLIB保留 PT_PHDR该段(如果出现),指定了程序头表本身的位置和大小(包括在文件中和在该程序的内存映像中)。更进一步来说,它仅仅在该程序头表是程序内存映像的一部分时才有效。如果它出现,它必须先于任何可载入段入口。 PT_LOPROC through PT_HIPROC保留 p_offset 表示段在ELF文件中的偏移。 p_vaddr 表示段首字节在内存中的虚拟地址。 p_paddr 表示段的物理地址,通常保留未用。 p_filesz 该段在ELF文件中的字节数,有可能是0。 p_memsz 该段在内存中的字节数,有可能是0。 p_flags 表示与段相关的标志。 p_align 决定段在文件及内存中的对齐方式。 暂时只用到这么多,以后按需要再补充。 参考资料:http://www.nsfocus.net/index.php?act=magazine&do=view&mid=890 |
上一篇:简单方法实现U盘启动到WinPE--菜鸟进阶篇(初步定制WinPE)
下一篇:带VLAN的交换机是三层交换机吗