当前位置: 首页 > news >正文

哈尔滨网站建设哪儿好薇sem竞价培训班

哈尔滨网站建设哪儿好薇,sem竞价培训班,seo网站建设流程,知识营销案例有哪些这篇文章写的很好是理解操作系统加载部分的基础 https://www.cnblogs.com/chuganghong/p/15415208.html loader的功能是#xff1a; 从软盘中把操作系统内核读取到内存中。 进入保护模式。 把内存中的操作系统内核重新放置到内存中。 执行操作系统内核。 如果理解不了上面的…这篇文章写的很好是理解操作系统加载部分的基础 https://www.cnblogs.com/chuganghong/p/15415208.html loader的功能是 从软盘中把操作系统内核读取到内存中。 进入保护模式。 把内存中的操作系统内核重新放置到内存中。 执行操作系统内核。 如果理解不了上面的部分语句先搁置后面会详细说明。 流程图 先看loader的流程图。 不必全部看懂。我觉得可能对读者理解后面的内容有帮助所以先给出这张图。 Kernel 代码 在加载kernel到内存中之前先要有一个内核。我们马上写一个。下面的代码在文件kernel.asm中。 [section .text]global _start _start:mov ah, 0Fhmov al, Cmov [gs:(80 * 20 40) * 2], axmov [gs:(80 * 21 40) * 2], axmov [gs:(80 * 22 80) * 2], axjmp $jmp $上面的代码是展示了一个汇编函数的模板但又不是典型的汇编函数模板。 [section .text]是伪指令不会被CPU执行仅仅只是告知程序员下面的代码是可执行代码提高代码的可读性。 global _start让函数_start成为全局函数。 func_name:; some coderet这才是汇编函数的标准模板。func_name是函数名;some code和ret都是函数体。ret可以理解为C语言中的return。 在kernel.asm中函数名是_start这固定的只能是这个名字由编译器或链接机制决定我也不是特别清楚。 mov [gs:(80 * 20 40) * 2], ax把字符C打印在屏幕的第20行、第40列。 jmp $可以理解成C代码while(1){}。 编译 把kernel.asm编译成elf格式的目标文件kernel.o。nasm -o kernel.o -f elf kernel.asm使用连接器ld把目标文件kernel.o连接成32位的可执行文件kernel.bin并且使用0x30400作为文本段的起始点。ld -s -Ttext 0x30400 -o kernel.bin kernel.o -m elf_i386 放入软盘 把boot.bin写入软盘a.imgdd ifboot.bin ofa.img bs512 count1 convnotrunc挂载软盘a.img以便在下面把loader.bin、kernel.bin写入软盘。sudo mount -o loop a.img /mnt/floppy/sudo cp loader.bin /mnt/floppy/ -vsudo cp kernel.bin /mnt/floppy/ -v卸载软盘sudo umount /mnt/floppy 经过上面的一系列步骤就得到了一个可执行的内核文件kernel.bin。虽然简单但我们写的这个操作系统无论变得多复杂都是在这个简单的内核文件上慢慢添加功能变成的。 加载内核 内核代码在kernel.bin文件中。kernel.bin是kernel.asm经过编译后的二进制文件。加载内核就是把kernel.bin从软盘中读取到内存中。 从软盘中读取数据的思路是 根据文件名在软盘的根目录中找到目标文件的第一个FAT项的编号同时也是在数据区的第一个扇区的编号。 由于文件的所有FAT项构成一个单链表变量这个单链表读取每个FAT项对应的扇区的数据复制到内存中。 和读取引导扇区到内存中的方法高度相似在代码上只有很小的差异。在后面不能理解加载内核的代码时请回头看看《开发引导扇区》。 CPU模式 es:bx 在boot.asm中使用int 13h会把从软盘中读取到的数据存储到es:bx指向的内存中。就从es:bx这条指令称呼它为指令不严谨我没见过在汇编代码中直接使用这样的指令开始讲述本节的内容。 es:bx的的值是多少这个值有什么意义 先回答第二个问题它的值表示一个内存地址物理内存地址不是在高级编程语言例如C语言编写的程序中出现的内存地址。后者是虚拟内存地址。 什么叫物理内存地址先解释什么是物理地址。拿软盘来说。在软盘的引导扇区的最后两个字节存储0x55AA那么在软盘中偏移量是510个字节的存储空间中能看到0x55AA。如果510是虚拟地址在软盘中偏移量是510个字节的存储空间中可能看不到0x55AA。 所以对“物理地址”和“虚拟地址”的理解是前者和存储空间一一对应如果0x55AA存储在软盘的引导扇区中地址为510的存储空间中就能在这个地址的存储空间中找到0x55AA中这两个字节的数据。后者和存储空间不是一一对应的0x55AA存储在虚拟地址510的存储空间中在软盘中地址为510的存储空间找不到0x55AA这两个字节的数据。 理解了“物理地址”再把软盘换成内存想必非常容易理解“物理内存地址”的含义。 用一句什么话过渡到CPU模式呢 那么es:bx的值究竟应该如何计算呢计算方法取决于CPU所处的模式。先介绍一下“实模式”。 实模式 实际上我们已经体验过“实模式了。引导扇区程序就运行在实模式下。 我认为对这种古老的历史知识不必深究。实模式对我而言有用的知识点是 在这种模式下物理内存地址的计算方法也就是计算es:bx的值的方法。 寄存器中的数据最大是16位的不能是32位的。 卖了不少关子其实计算方法只用一句话就能讲清楚 实模式下的物理内存地址 e s ∗ 16 b x 。 实模式下的物理内存地址 es * 16 bx。 实模式下的物理内存地址es∗16bx。 例如要把内核读取到物理内存地址是0x91000的存储空间中只需把es的值设置成0x9000把bx的值设置成0x1000。 为什么这样做就能表示物理内存地址0x91000呢这涉及到选择子、“地址总线”等知识。我认为不知道这些古老的概念暂时并不妨碍我们继续开发自己的操作系统内核。就算花点时间弄明白了过段时间又忘记了不忘记作用似乎也不大仅仅满足了自己的好奇心。不如先跳过这种细枝末节、不影响大局的知识点降低自己的学习难度。以后有时间再弄清楚这种细节。 实模式下最多能使用多大的内存 内存条可以随意增加空间能使用的最大内存就取决于计算机能寻址多大的内存地址。计算机的内存寻址又受制于寄存器能提供的内存地址值和地址总线能传递的内存地址值。 不可避免地要介绍一下地址总线。我也只是稍微了解一点够用的知识。 CPU和内存之间通过地址总线交流可以这么简单粗暴地理解。CPU把内存地址例如0x9000:0x1000通过地址总线告知内存要读取0x9000:0x1000处的数据。 首先0x9000、0x1000这两个数值能存储在16位寄存器es、ax中。如果超过16位寄存器的数值存储范围CPU就不能读取0x9000:0x1000处的数据。 地址总线接收到的内存地址是0x9000:0x1000计算出来的数值0x91000。如果0x91000超出地址总线的存储范围CPU也不能读取到目标数据。 实模式下地址总线是20位的能表示的最大数值是 2 20 − 1 2^{20}-1 220−1。 计算机寻址的最小单位是“字节”而不是“bit”。所以实模式下计算机能使用的内存最大是 2 20 − 1 1 2^{20} - 1 1 220−11字节即1M。 为什么是 220 − 1 1 2{20}-11 220−11因为内存地址的初始值是0。从0到最大值$2-$1总计有 2 20 − 1 1 2^{20}-11 220−11个字节。 各位的电脑内存是多大远远大于1M。实模式很快就不能满足需求所以就出现了“保护模式”。 保护模式 和实模式对比 保护模式下 CPU通过寄存器能提供的最大内存地址是 2 32 − 1 2^{32}-1 232−1。 地址总线是32位的能传输的最大内存地址也是 2 32 − 1 2^{32}-1 232−1。 因此计算机能使用的最大内存是4GB。 在实模式下任何指令能使用任何合法的内存地址指向的内存空间。想象一下A地址存储小明的代码XMB地址存储小王的代码XW。小明和小王约定好执行完XM后跳转到XW执行XW后跳转到XM。可是小王和小明吵了一架偷偷地在XW中把A地址处的代码全部擦除执行完XW后小明的代码就再也不会执行了。 再举个例子你一边用编辑器写代码一边用某个音乐播放器听音乐音乐播放器偷偷修改了正在运行中的编辑器所占用的内存中的数据导致你运行自己的代码时总是出错。当然你肯定没有遇到过这种奇怪的事情。因为你的电脑的CPU不是运行在实模式下。 通过两个例子应该能够知道实模式的弊端。保护模式中的“保护”二字集中体现在对指令能使用的内存空间做了限制不能像在实模式下那样能随心所欲地使用任何合法的内存地址指向的内存空间。 用最简单的语言描述保护模式 计算机能使用的最大内存是4GB。 指令只能使用特定的内存空间。 内存寻址方式不同。 保护模式下仍然用es:bx这种格式的数据表示内存地址可计算es:bx的值的方法和实模式下大为不同。 先了解几组概念。 GDT GDT的全称是Global Description Table对应的中文术语是“全局描述符表。在后面还会遇到LDT。 全局描述符表顾名思义这个表中的每个项都是一个全局描述符。 全局描述符是一段8个字节的数据。这8个字节包含三项内容段界限、段基址、段属性。 全是陌生概念。不要慌。让我们一个一个来弄清楚。 在前面我讲过保护模式下指令不能访问任意内存空间的指令能访问的内存空间是被限制的。怎么实现这种限制呢 划定一段内存空间规定这段内存空间只有具有某种属性的指令才能访问。怎么划定一段内存空间非常自然地想到在指定一个初始值再指定一个界限值就能确定一段内存空间。然后再用“段属性”规定访问这段内存空间需要具备的条件。 “初始值”就是“段基址”“界限值”就是“段界限”。段属性是什么有点难说清楚后面再说。 在逻辑上全局描述符的结构很清晰可它的实际结构却非常不规则。先看看全局描述符的结构示意图。 怎么解读这张图 图中的数据使用“小端法”存储。 全局描述符占用8个字节即64个bit。 示意图中把描述符分为“代码段”描述符和“数据段”描述符。是哪种描述符由段属性决定。 段属性依然很复杂。咱们再搁置一会儿。 介绍描述符结构的终极目的是用代码表示描述符而且是用汇编代码表示。 描述符的本质是64个bit要把一段内存空间的信息初始地址–段基址、这段内存空间的长度减去1–段界限、这段内存空间的属性–段属性按描述符的结构存储到这64个bit中。 所谓“结构”可以理解为按某种规则解读一段数据。例如桌子上按顺序放着10支铅笔每支红色铅笔表示1年2支红色铅笔表示2年3支红色铅笔表示3年按照这个规定10支红色铅笔表示10年。再规定前四支铅笔表示小明的年龄后六支铅笔表示小王的年龄。给你10支持按顺序排列的铅笔你能从中查询到小明和小王的年龄吗这一定是一件非常容易的事。分别看看前4支、后6支铅笔中红色铅笔的数量即可。 不知道这个例子是否恰当。我想表达的意思是结构是一种规定好的规则例如前4支铅笔表示小明的年龄读数据要按这种规则去读存数据也要按这种规则去存。小明的年龄只能从前4支铅笔读取也只能用前4支铅笔存储。 先给出用C语言表示的描述符结构。 typedef struct{// 对应描述符结构图中的BYTE0、BYTE1。unsigned short seg_limit_below;// 对应描述符结构图中的BYTE2、BYTE3。unsigned short seg_base_below;// 对应描述符结构图中的BYTE4。unsigned char seg_base_middle;// 对应描述符结构图中的BYTE5。unsigned char seg_attr1;// 对应描述符结构图中的BYTE6。unsigned char seg_limit_high_and_attr2;// 对应描述符结构图中的BYTE7。unsigned char seg_base_high; }Descriptor;我当初怎么都理解不了汇编代码表示的描述符结构直到看到用C语言表示的描述符结构后才豁然开朗所以先给出C代码希望跟我有同样困惑的人能和我一样顿悟。 因为我只是普通的人当初理解“结构”和代码的对应关系着实花了不少时间所以在这里写得比较啰嗦实际上是把我当时的理解过程全部写了出来。聪明人请快速跳过这段。 用汇编语言怎么表示描述符结构我决定先不照搬之前已经写好的代码而是和读者朋友一起现场徒手再写一次描述符结构的汇编代码A。请对照前面的描述符结构图写代码。过程如下 使用汇编代码中的宏来实现描述符结构。这个宏有三个参数分别是段界限、段基址、段属性命名为seg_limit、seg_base、seg_attr。每个参数的长度都是4个字节。段界限1是seg_limit的低16位代码是seg_limit 0xFFFF。段基址1是seg_base的低24位代码是seg_base 0xFFFFFF。属性是seg_attr的低8位 seg_limit的高4位 seg_attr的高4位代码是(seg_attr 0xFF) |(( (seg_attr 8) 12) | (seg_limit 16)8)。段基值2是seg_base的高8位代码是seg_base24。 和我以前写的描述符结构的汇编代码B比较一下。 ; 三个参数分别是段基址、段界限、段属性 ; 分别用 %1、%2、%3表示上面的三个参数分别等价于A代码中的seg_base、seg_limit、seg_attr。 %macro Descriptor 3dw %2 0ffffhdw %1 0ffffhdb (%1 16) 0ffhdb %3 0ffhdb ((%2 16) 0fh) | (((%3 8) 0fh) 4)db (%1 24) 0ffh %endmacro二者的差异在对段基址1、属性的处理。 汇编代码B中对段基址1的处理是 dw %1 0ffffh db (%1 16) 0ffh汇编代码A中对段基值1的处理是seg_base 0xFFFFFF。把A中的6个字节拆分成4个字节和2个字节原因是在nasm汇编中只存在dw、db这样的伪指令而不存在能表示6个字节的伪指令。二者都能把段基址1存储到正确的内存空间只是受限于汇编语法把6个字节拆分成两部分来处理。 dw、db都是伪指令。伪指令是指不被处理器直接支持的指令最终会被翻译成机器指令被处理器处理。dw一个字两个字节。db一个字节。再比较二者对属性的处理。 ; A代码 (seg_attr 0xFF) |(( (seg_attr 8) 12) | (seg_limit 16)8) ; B代码 db seg_attr 0ffh db ((seg_limit 16) 0fh) | (((seg_attr 8) 0fh) 4)A代码和B代码对属性的处理结果是一致的都符合nasm汇编语法。B代码很容易理解。A代码有点难懂展开分析一下。 在描述符结构图中BYTE5、BYTE6中存储的数据混合了段界限的高4位和全部属性。 这个混合体的低8位是属性的低8位用seg_attr 0xFF来获取这不应该有疑问。 混合体的高4位是属性的高4位。属性的高4位是seg_attr 8。混合体总计12位高4位的前面是低12位所以必须把获取到的属性的高4位左移12位因此最终结果是(seg_attr 8) 12。 混合体的高4位、低8位都已经填充数据剩余中间4位用来存储段界限的高4位。段界限的高4位是seg_limit 16。要把这4位存储在混合体的中间4位需要跳过混合体的低8位使用(seg_limit 16)8实现。 最后把混合体的三部分用|运算符拼接起来就是这样的(seg_attr 0xFF) |(( (seg_attr 8) 12) | (seg_limit 16)8)。 gdtptr GDT存储在内存的一段空间内。CPU要想正确读取GDT中的描述符需要先把这段空间的初始地址和空间界限存储在专门的寄存器gdtptr中。 gdtptr的结构图如下。 在前面介绍保护模式时我提到过要确定一段内存空间至少需要两个值这段内存空间的初始值和这段内存空间的长度。 GDT也存储在一段内存空间中只需提供空间初始值和空间长度就能找出存储GDT的这段内存空间。gdtptr的设计正是如此低16位存储界限高32位存储基地址。16位界限 空间长度L) - 1因为内存地址的初始值是0。内存的第1个字节内存地址是0x0内存的第2个字节内存地址是0x1。GDT的第1个字节内存地址是基地址0GDT的第2个字节内存地址是基地址1GDT的第3个字节内存地址是基地址2GDT的第L个字节内存地址是基地址L-1。这就是“16位界限 空间长度L) - 1”的由来。 那么要填充到gdtptr中的值怎么用代码表示呢请看下面。 ; 三个参数分别是段基址、段界限、段属性 ; 分别用 %1、%2、%3表示上面的三个参数 %macro Descriptor 3dw %2 0ffffhdw %1 0ffffhdb (%1 16) 0ffhdb %3 0ffhdb ((%2 16) 0fh) | (((%3 8) 0fh) 4)db (%1 24) 0ffh %endmacroLABEL_GDT: Descriptor 0, 0, 0LABLE_GDT_FLAT_X: Descriptor 0, 0ffffffh, 0c9ahLABLE_GDT_FLAT_X_16: Descriptor 0, 0ffffffh, 98hLABLE_GDT_FLAT_X_162: Descriptor 0, 0ffffffh, 98h;LABLE_GDT_FLAT_X: Descriptor 0, 0FFFFFh, 0c9ah;LABLE_GDT_FLAT_WR:Descriptor 0, 0fffffh, 293hLABLE_GDT_FLAT_WR_TEST:Descriptor 5242880, 0fffffh, 0c92hLABLE_GDT_FLAT_WR_16:Descriptor 0, 0fffffh, 0892hLABLE_GDT_FLAT_WR:Descriptor 0, 0fffffh, 0c92hLABLE_GDT_VIDEO: Descriptor 0b8000h, 0ffffh, 0f2hGdtLen equ $ - LABEL_GDTGdtPtr dw GdtLen - 1dd 0这段代码中的GdtPtr存储的值就是要填充到寄存器gdtptr中的值。 GdtLen equ $ - LABEL_GDT中的$表示当前位置即GdtLen之前的位置。LABEL_GDT表示从存储当前二进制文件当前源文件编译之后得到的二进制文件的内存的初始地址到LABEL_GDT这个位置的字节偏移量。汇编代码中的变量名称都能理解成这个变量相对于存储当前二进制文件的内存空间的初始位置的字节偏移量。从LABEL_GDT到GdtLen是GDT表的长度。 GdtPtr dw GdtLen - 1dd 0GdtPtr的前16位存储GDT的长度减去1就是GDT的界限后32位存储0就是GDT的基地址。这和gdtptr需要的数据结构正好一致。事实上往gdtptr中存储的值就是GdtPtr中的数据。完成这个操作的指令是 [GdtPtr]表示内存地址GdtPtr指向的内存空间中存储的数据而不是指内存地址这个值本身。可以把[GdtPtr]理解成指针。 lgdt [GdtPtr] 关于GDT的理论知识只剩下全局描述符的指针和选择子还没有讲解。让我们看看全局描述符的代码顺便理解一下全局描述符中的段属性。 LABEL_GDT: Descriptor 0, 0, 0 LABLE_GDT_FLAT_X: Descriptor 0, 0ffffffh, 0c9ah LABLE_GDT_FLAT_X_16: Descriptor 0, 0ffffffh, 98h LABLE_GDT_FLAT_WR_16:Descriptor 0, 0fffffh, 0892h LABLE_GDT_FLAT_WR:Descriptor 0, 0fffffh, 0c92h LABLE_GDT_VIDEO: Descriptor 0b8000h, 0ffffh, 0f2h现在开始解读这段代码。 Descriptor是创建描述符的宏。汇编函数中的宏和C语言中的宏的作用相同。 注意我没有说Descriptor是创建“全局”描述符的宏。因为用这个宏既能创建全局描述符又能创建局部描述符。 LABEL_GDT。使用宏Descriptor三个参数都是0。意思是这个描述符的描述的内存空间的段基址、段界限、段属性都是0。这是一个空描述符是GDT的初始位置。把GDT中的第一个描述符设计成空描述符是为了定位其他描述符时有一个参照系。 LABLE_GDT_FLAT_WR_16。段基址是0段界限是0fffffh段属性是0892h。 段界限是0fffffh字节还是0fffffhG这由段属性中的一个属性决定。下面详细介绍段属性。 描述符LABLE_GDT_FLAT_WR_16的属性值是0892h。这个段属性是怎么确定的又应该怎么解读? 首先属性值0892h的二进制形式是1000 1001 0010。这个二进制值的存储方式是“小端法”。把它填入下面的表格中。怎么填写? 从1000 1001 0010的最右边开始依次把值填入两个表格的第0列、第1列、第2列直至填充到第11列。 表格一 0 1 2 3 4 5 6 7 8 9 10 11 A R C X S DPL DPL P AVL L D/B G 0 1 0 0 1 0 0 1 0 0 0 1表格二 A W E X S DPL DPL P AVL L D/B G 0 1 0 0 1 0 0 1 0 0 0 1这两个表格是段属性的12个bit拼接在一起形成的仅仅只有表格的第1列、第2列所存储的数据的含义不同。 第1列、第2列是R、C的那个表格是代码段的段属性表格R、C分别表示是否可读、是否依从。最关键的是X列表示是否可执行。表格一中的X位上的值是0这表示这个描述符所描述的内存空间中存储的是“数据”所以这种描述符叫“代码段”描述符。 两个表格的第0列到第3列对应描述符结构图中的TYPE。每个位的含义请看下面的两张图。 代码段一定是不可写的所以代码段属性中没有“写”属性。 数据段一定是可读的所以数据段属性中不必设计一个“读”属性。 上面是对描述符结构图中TYPE的介绍。下面介绍除TYPE外的其他属性。 S。S是0时描述符是系统段/门描述符。S是1时描述符是代码段/数据段描述符。DPL。描述符的特权级值可以是0或1或2。数字越小特权级越高。P。P是0时描述符指向的段在内存中不存在。P是1时描述符指向的段在内存中存在。AVL。保留位。我没用到过。暂时不用关注。D/B。比较复杂。太琐碎了。只需知道它决定段使用的操作数、内存地址的位数或者决定堆栈扩展的方向。用到这一位时查资料即可。毕竟记住这种细节用处不大成本不小。G。在前面我提过段界限除了具体数值还有一个数值单位。这个单位由G决定。G是granularity意思是“粒度”。当G的值是0时段界限粒度是字节当G的粒度是1时段界限粒度是4kB。 描述符LABLE_GDT_FLAT_WR_16的段属性的G位是1段界限是0fffffh * 4kb 4GB。 再分析一个描述符LABLE_GDT_FLAT_WR。它的段属性是0c92h。我想把这个描述符设置成段界限粒度为4kB数据段可读写32位。 把属性0c92h换算成二进制形式110010010010然后填入数据段属性的表格。 A W E X S DPL DPL P AVL L D/B G 0 1 0 0 1 0 0 1 0 0 1 1W位是1表示描述符指向的段是可读可写的。S是1表示描述符是代码段或数据段描述符。D/B是1表示描述符指向的段是32位的。G是1表示描述符指向的段的段界限的粒度是4kB。和我前面的设想吻合。 对GDT的介绍终于可以结束了。 选择子 现在可以进一步解释es:bx了。 在保护模式下es:bx中的es中存储的数据叫做“选择子”。GDT中包含多个描述符选择子的作用是从GDT中指定目标选择子。 可以近似地把GDT理解成C语言中的数组数组的元素的数据类型是描述符选择子是数组的索引。像这样简化地理解选择子只是为了帮助我们体会选择子的作用。它的作用是什么从GDT或LDT中找出目标描述符。 在前面我提过描述符分为全局描述符和局部描述符。由全局描述符组成的描述符表叫GDT由局部描述符表组成的描述符表叫LDT。 给出一个选择子是从GDT还是LDT中挑选目标描述符呢?这是由选择子中的某些数据决定的。来了解一下选择子的结构。 选择子的前2个bit存储RPL对应的中文术语是“请求特权级”。RPL和DPL一样有三个取值00、01、11。 选择子的第2个bi存储TI。TI是0时是GDT的选择子。TI是1时是LDT的选择子。 选择子的剩余13位存储描述符索引。这才是目标描述符在描述符表中的索引。描述符索引本质是目标描述符在描述符表中的字节偏移量。每个描述符的长度是8个字节。描述符表能容纳的描述符的最大数量是 2 13 2^{13} 213即8192。 gdtptr的低16位存储GDT的段界限也就是说GDT的最大长度是 216 2{16} 216字节。GDT中描述符的最大数量是 2 2 2每个描述符的长度是8个字节那么GDT中所有描述符的长度之和也就是GDT的长度是 2 13 ∗ 8 2 16 2^{13} * 8 2^{16} 213∗8216个字节。二者是吻合的。 看一看选择子的汇编代码。 SelectFlatX equ LABLE_GDT_FLAT_X - LABEL_GDT SelectFlatX_16 equ LABLE_GDT_FLAT_X_16 - LABEL_GDT SelectFlatX_162 equ LABLE_GDT_FLAT_X_162 - LABEL_GDT SelectFlatWR equ LABLE_GDT_FLAT_WR - LABEL_GDT SelectFlatWR_TEST equ LABLE_GDT_FLAT_WR_TEST - LABEL_GDT SelectFlatWR_16 equ LABLE_GDT_FLAT_WR_16 - LABEL_GDT SelectVideo equ LABLE_GDT_VIDEO - LABEL_GDT 3代码中的第0列初始值规定为0是选择子例如SelectFlatX。表面上看选择子是对应描述符的内存地址相对于空描述符的内存地址的偏移量。这个偏移量一定是8个字节的整数倍。 选择子的前三位分别是CPL和TI后13位是描述符在描述符表中的索引。代码中的选择子是描述符之间的偏移量理解偏移量和选择子结构如何吻合是一个难点。 描述符相对于描述符表的初始地址及空描述符表的地址的偏移量必定是8个字节的整数倍。若觉得不能透彻理解可以举例子看看。 丢弃偏移量的低3位相当于把偏移量左移3位等价于除以8结果是描述符的数量。也就是说偏移量的高13位是选择子对应的描述符相对于描述符数组的初始位置的偏移量但这个偏移量的单位不再是字节而是描述符。简单说偏移量的高13位是描述符在描述符表中的描述符偏移量。描述符偏移量不就是描述符索引么 偏移量的高13位是描述符在描述符表中的索引理由在上面说了。偏移量的低3位是0正好对应选择子的低3位。把选择子的低3位即CPL、TI设置成0从结构和数据的对应看当然没问题。可是选择子的低3位不会总是0需要存储具有特定意义的数据。怎么办实现这个目的对描述符内存地址的偏移量进行修改就能实现这个目的。 正如上面的代码中的SelectVideo。它的选择子的计算方式是LABLE_GDT_VIDEO - LABEL_GDT 3。 SelectVideo的TI是0CPL是3。这表示SelectVideo是GDT的选择子当前特权级是3。 代码中的其他选择子都是描述符内存地址相对于GDT初始地址的原始偏移量没有修改低3位存储的TI和CPL。 寻址方式 保护模式下内存地址的寻址方式如下图所示。 es:bx这种形式的内存地址叫“逻辑地址”。es中的值是描述符表的选择子。根据选择子在描述符表中选择它对应的描述符。从描述符中获取这个描述符指向的内存空间根据描述符中包含的段基址和段界限就能划定一段内存空间。bx就是在这段内存空间中的地址偏移量。而es就是上图中的SEG。 进入保护模式 进入保护模式的代码如下。 ; 把GdtPtr加载到寄存器gdtptr中。 mov dword [GdtPtr 2], BaseOfLoaderPhyAddr LABEL_GDT lgdt [GdtPtr]; 关闭中断。 ; 关闭中断的目的是让进入保护模式的所有操作具有原子性。 ; 假如打开了A20地址线却还处在实模式下此时会错误处理超过了2的20次方的地址。 cli; 打开A20地址线。 in al, 92h or al, 10b out 92h, al; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式。这句把cs设置为SelectFlatX jmp dword SelectFlatX:(BaseOfLoaderPhyAddr LABEL_PM_START)从实模式进入保护模式使用这几行代码就可以了。要理解每行代码的含义又涉及到一些古老的细节知识。我以为这些东西作用不大在后续功能开发中不会用到第二次。所以对进入保护模式的方法掌握到这个程度就足够了。 重新放置内核 内核文件kernel.bin已经从软盘中读入内存了为什么还要重新放置内核呢因为这个内核文件是elf格式的。这种格式的可执行文件不仅包含可执行的指令还包括一些额外数据。 下面一起来了解ELF文件。 这种起“过渡”作用的废话真的需要吗不写这句话直接介绍elf又显得太突兀。 ELF elf的全称是excutable load format。linux系统上的目标文件、可执行文件都是elf文件。elf文件包含elf头、section头表、segment、program header table。 一图胜千言。让我们看看ELF文件的结构示意图。 图2-4 ELF的知识远远不止上面这么一点但了解上面的知识足以让我们理解重新放置内核这个操作。好奇心强烈的读者朋友可以看看下面的补充知识。不看也不会妨碍我们继续开发自己的操作系统。 复制段 流程 重新放置内核处理的是kernel.bin。它是ELF格式的可执行文件。所谓重新放置就是把segment0、segment1、segment2等复制到内存中的某些位置。具体流程如下 从ELF Header中查询出program header table在文件中的偏移量e_phoff和program header table中的program header的数量e_phnum。 每个program header对应一个segment。program header中记录着segment在文件中的偏移量p_offset、应该在内存中的地址p_vaddr和它的大小p_filesz。 遍历program header table把对应的segment复制到p_vaddr 内存地址处。 伪代码 重新放置内核的伪代码如下。 // kernel.bin在内存中的位置。 address_of_kernel; for(int i 0; i ELF_Header.e_phnum; i ){// 每个program_header的大小是32字节。program_header program_header_table i * 32;// 复制program_header对应的段。Memcpy(program_header.p_vaddr, program_header.p_offset address_of_kernel, program_header.p_filesz); }汇编代码 汇编代码如下。 BaseOfKernelPhyAddr equ 80000h ; Kernel.BIN 被加载到的位置 ; 重新放置内核 InitKernel:push eaxpush ecxpush esi;程序段的个数e_phnummov cx, [BaseOfKernelPhyAddr 2CH]movzx ecx, cx;程序头表的内存地址xor esi, esi; 对应伪代码中的program_header_table也是ELF头的e_phoff。mov esi, [BaseOfKernelPhyAddr 1CH]add esi, BaseOfKernelPhyAddr .Begin:; program_header的p_filesz。mov eax, [esi 10H]push eaxmov eax, BaseOfKernelPhyAddr; [esi 4H] 是program_header的 p_offseteax的最终结果是段的内存地址。add eax, [esi 4H]push eax; [esi 8H] 是program_header的 p_vaddreax的最终结果是段应该被重新放置到的内存地址。mov eax, [esi 8H]push eax; 调用复制函数。call Memcpy; 三个参数每个占用32位4个字节2个字占用6个字,12个字节add esp, 12; ecx是e_phnum。重新复制一个段后ecx的值应该减去1。dec ecx; 当ecx的值是0时所有的段已经处理完毕。cmp ecx, 0jz .NoAction; 一个program_header的大小是20H处理完一个program_header后; 应该处理下一个program_header。add esi, 20Hjmp .Begin .NoAction:pop esipop ecxpop eaxret验证 重新放置内核后怎么知道有没有把内核中的代码段放置到了正确的内存位置呢 有人说执行一次呗。如果能看到内核运行的效果就说明是正确的。 我试过这样不能验证内核被正确地重新放置了。内核也就是kernel.bin是一个ELF文件除了包含代码段还包含很多CPU不能识别的数据。直接运行kernel.binCPU遇到不能识别的数据就不处理遇到了代码段就执行所以即使没有正确地放置内核也能看到内核运行效果。也许是因为我们目前的内核太简单。 验证内核有没有被正确放置到内存中的方法是使用bochs查看内存中的数据。让我们一起来验证一下。 实例分析ELF文件 用xxd查看kernel.bin的数据data。 [rootlocalhost v4]# xxd -u -a -g 1 -c 16 kernel.bin 00000000: 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............ 00000010: 02 00 03 00 01 00 00 00 00 04 03 00 34 00 00 00 ............4... 00000020: 30 04 00 00 00 00 00 00 34 00 20 00 01 00 28 00 0.......4. ...(. 00000030: 03 00 02 00 01 00 00 00 00 00 00 00 00 00 03 00 ................ 00000040: 00 00 03 00 1D 04 00 00 1D 04 00 00 05 00 00 00 ................ 00000050: 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ * 00000400: B4 0F B0 43 65 66 A3 D0 0C 00 00 65 66 A3 70 0D ...Cef.....ef.p. 00000410: 00 00 65 66 A3 60 0E 00 00 EB FE EB FE 00 2E 73 ..ef..........s 00000420: 68 73 74 72 74 61 62 00 2E 74 65 78 74 00 00 00 hstrtab..text... 00000430: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000440: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000450: 00 00 00 00 00 00 00 00 0B 00 00 00 01 00 00 00 ................ 00000460: 06 00 00 00 00 04 03 00 00 04 00 00 1D 00 00 00 ................ 00000470: 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 ................ 00000480: 01 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 ................ 00000490: 1D 04 00 00 11 00 00 00 00 00 00 00 00 00 00 00 ................ 000004a0: 01 00 00 00 00 00 00 00 ........从上面的数据中找出program header table在文件中的偏移量e_phoff、第一个代码段在段中的偏移量p_offset、第一个代码段被重新放置到内存中后的初始地址p_vaddr、程序在内存中的入口地址e_entry。 e_phoff。在文件中的偏移量是1CH长度是4个字节。 program header table在文件中的地址就是e_phoff。 在data中找到e_phoff是34 00 00 00。这是十六进制数。 34 00 00 00是十六进制数左边是低内存地址右边是高内存地址。 把它写成我们平时的读写顺序这个数字应该是0x34。 e_entry。在文件中的偏移量是18H长度是4个字节值是00 04 03 00换算成0x030400。 p_offset。在program header中的偏移量是4H在文件中的偏移量是0x340x4长度是4个字节。 代码段在文件中的地址就是p_offset。 p_offset是00 00 00 00和前面转换34 00 00 00的方法一样00 00 00 00是0x0。 p_vaddr。在段中的偏移量是8H在文件中的偏移量是0x340x8长度是4个字节。 值是00 00 03 00换算成0x030000。 程序的入口在文件中的偏移量是什么 ELF文件没有直接提供这个数据但我们可以计算出来。 先计算程序的入口在段中的偏移量。 程序的入口在内存中的地址是e_entry段在内存中的地址是p_vaddr。 程序的入口是段中的一条指令。这一句非常关键。 e_entry - p_vaddr就是程序的入口在段中的偏移量offset。 段在文件中的偏移量 程序的入口在段中的偏移量就是程序的入口在段中的偏移量。 这个值是0 0x030400 - 0x030000 0x400。 还需要知道代码段的长度p_filesz。它在program header中的偏移量是10H在文件中的偏移量是0x34 0x10 0x44因而值是1D 04 00 00换算成0x041D。 在上面写了这么多就是为了得出下列结论 第一个代码段在文件中的偏移量是0x0应该被重新放置到内存地址p_vaddr即0x30000。 程序的入口的内存地址e_entry即0x30400在文件内的偏移量是0x400。 第一个代码段的长度是p_filesz即0x041D。 我们继续进行分析ELF文件。 代码段的数据是 00000000: 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............ 00000010: 02 00 03 00 01 00 00 00 00 04 03 00 34 00 00 00 ............4... 00000020: 30 04 00 00 00 00 00 00 34 00 20 00 01 00 28 00 0.......4. ...(. 00000030: 03 00 02 00 01 00 00 00 00 00 00 00 00 00 03 00 ................ 00000040: 00 00 03 00 1D 04 00 00 1D 04 00 00 05 00 00 00 ................ 00000050: 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ * 00000400: B4 0F B0 43 65 66 A3 D0 0C 00 00 65 66 A3 70 0D ...Cef.....ef.p. 00000410: 00 00 65 66 A3 60 0E 00 00 EB FE EB FE 00 2E 73 ..ef..........s这是第一个代码段中的数据没有搞错吗怎么这个代码段包含了ELF Header和program header table 分析ELF文件后我发现第一个代码段的确包含了ELF Header和program header table。可我在《一个操作系统的实现》、《程序员的自我修改—链接、装载与库》、维基百科中看到的ELF文件结构示意图都把segment和ELF画成互不包含的两个部分。怎么理解两类矛盾的现象是这些书写错了吗 它们没有错。正确的理解应该是这些资料中的图是第二个、第三个代码段反正不是第一个代码段和ELF Header、program header table的结构示意图。 回到“验证内核中的指令是否正确’这个主题上来。 在data中从0x400到0x41C都是代码段中的指令就是下面这块数据。 00000400: B4 0F B0 43 65 66 A3 D0 0C 00 00 65 66 A3 70 0D ...Cef.....ef.p. 00000410: 00 00 65 66 A3 60 0E 00 00 EB FE EB FE -- -- -- ..ef..........s程序在内存中的初始地址是0x30400。我们只需对比内存0x30400到0x3041C中的数据是否和上面文件中0x400到0x41C数据一一相等就能判断内核是否被正确地放置到了内存中。 请看下面的表格。 表格中的地址偏移量是指文件地址偏移量和内存地址偏移量。两个偏移量相等只不过地址的基址不同。 文件地址的基址是0x0内存地址的基址是0x30000。 启动bochs查看内存地址0x30400、0x30401、0x30402中的数据并填入下面的表格。 (0) Magic breakpoint Next at t14812083 (0) [0x000000090312] 0008:0000000000090312 (unk. ctxt): jmpf 0x0008:00030400 ; ea000403000800 bochs:2 xp /1wx 0x30400 [bochs]: 0x0000000000030400 bogus 0: 0x43b00fb4 bochs:3 xp /1bx 0x30400 [bochs]: 0x0000000000030400 bogus 0: 0xb4 bochs:4 xp /1bx 0x30401 [bochs]: 0x0000000000030401 bogus 0: 0x0f bochs:5 xp /1bx 0x30402 [bochs]: 0x0000000000030402 bogus 0: 0xb0 bochs:6 xp /1bx 0x3041D [bochs]: 0x000000000003041d bogus 0: 0x00 bochs:7 XP /bx 0x3041C :7: syntax error at XP bochs:8 xp /1bx 0x3041C [bochs]: 0x000000000003041c bogus 0: 0xfe地址偏移量 0x400 0x401 0x402 0x41C 文件中的数据 B4 0F B0 FE 内存中的数据 b4 0f b0 fe表格中的数据都是十六进制数据无论是否有没有0x前缀。十六进制数据中的字母大小写不敏感。比较下面几个地址处的数据是否相等。一眼就能看出文件中的数据和内存中对应位置的数据相等。我只对比了几个地址的数据。读者朋友如果不相信这个结果可以继续看看其他地址处的数据是否相同。 如果你经过对比后发现内存中的第一个段的数据和文件中的段的数据完全相同就可以做出判断了内核被正确地重新放置到了内存中。 留一个疑问。我们对比的是内存中的第一个段的数据和文件中的段的数据然而重新放置内核并不是直接从软盘中的文件中读取数据再放置而是从内存的一个位置把内核数据重新放置到另一个位置。我的问题是对比内存中的数据和软盘中的文件中的数据效果等同于对比内存中的数据和另一段内存中的数据。为什么二者的效果是等同的 补充知识 .text是代码段.bss 段保存未初始化的变量.rodata段保存字符串或只读变量.data段保存已经初始化了的全局或局部静态变量。它们都是section或segment。 在可执行文件中它们是segment。在目标文件等非执行文件中它们是section。 在”kernel这个小节编译kernel.asm产生的kernel.o是目标文件kernel.bin是可执行文件。使用file命令查看这两个文件。 [rootlocalhost v4]# file kernel.o kernel.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped [rootlocalhost v4]# file kernel.bin kernel.bin: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped kernel.o 是ELF 32-bit LSB relocatablekernel.bin是 ELF 32-bit LSB executable。二者都是ELF文件但前者是relocatable后者是executable。 只有ELF Header的位置在ELF文件中是固定的在开头。其他元素例如program header table的位置不固定在ELF Header中记录了它们的位置、大小等信息。 想了解更多关于ELF的知识请去看《一个操作系统的实现》的5.3节和《程序员的自我修改》的第3章。 《一个操作系统的实现》中的elf文件的结构和《程序员的自我修改》中的elf文件的结构不同。另外像.rodata、.bss、.text这些在《一个操作系统的实现》中没有出现。我不清楚像.rodata、.bss、.text这些和section头表、segment头表、program header table的关系。 section头等都挨在一起组成一个表是这样吗 program header描述一个segment 还是一个segment header二者是一一对应的关系吗 segment 只存在于可执行文件中吗 参考资料 《一个操作系统的实现》 《程序员的自我修养—链接、装载与库》 代码注释 我不知道怎么用更好的方式写剩下的内容就直接送上添加了详细注释的loader.asm吧。 ; 暂时不必理会这句的意思。 org 0100hjmp LABEL_STARTnop; 下面是 FAT12 磁盘的头BS_OEMName DB YOUR--OS ; OEM String, 必须 8 个字节BPB_BytsPerSec DW 512 ; 每扇区字节数BPB_SecPerClus DB 1 ; 每簇多少扇区BPB_RsvdSecCnt DW 1 ; Boot 记录占用多少扇区BPB_NumFATs DB 2 ; 共有多少 FAT 表BPB_RootEntCnt DW 224 ; 根目录文件数最大值BPB_TotSec16 DW 2880 ; 逻辑扇区总数BPB_Media DB 0xF0 ; 媒体描述符BPB_FATSz16 DW 9 ; 每FAT扇区数BPB_SecPerTrk DW 18 ; 每磁道扇区数BPB_NumHeads DW 2 ; 磁头数(面数)BPB_HiddSec DD 0 ; 隐藏扇区数BPB_TotSec32 DD 0 ; wTotalSectorCount为0时这个值记录扇区数BS_DrvNum DB 0 ; 中断 13 的驱动器号BS_Reserved1 DB 0 ; 未使用BS_BootSig DB 29h ; 扩展引导标记 (29h)BS_VolID DD 0 ; 卷序列号BS_VolLab DB YOUR--OS.02; 卷标, 必须 11 个字节BS_FileSysType DB FAT12 ; 文件系统类型, 必须 8个字节; 三个参数分别是段基址、段界限、段属性 ; 分别用 %1、%2、%3表示上面的三个参数 %macro Descriptor 3dw %2 0ffffhdw %1 0ffffhdb (%1 16) 0ffhdb %3 0ffhdb ((%2 16) 0fh) | (((%3 8) 0fh) 4)db (%1 24) 0ffh %endmacro; 描述符和选择子的详细讲解请看本问的”GDT”、选择子小节。; 空描述符也是GDT的地址。LABEL_GDT: Descriptor 0, 0, 0; 代码段描述符LABLE_GDT_FLAT_X: Descriptor 0, 0ffffffh, 0c9ah; 数据段描述符LABLE_GDT_FLAT_WR:Descriptor 0, 0fffffh, 0c92h; 显存段描述符LABLE_GDT_VIDEO: Descriptor 0b8000h, 0ffffh, 0f2h; GDT地址在下面加载到gdtptr寄存器。; GDT的长度。GdtLen equ $ - LABEL_GDTGdtPtr dw GdtLen - 1dd BaseOfLoaderPhyAddr LABEL_GDT; 代码段的选择子SelectFlatX equ LABLE_GDT_FLAT_X - LABEL_GDT; 数据段的选择子SelectFlatWR equ LABLE_GDT_FLAT_WR - LABEL_GDT; 显存段的选择子SelectVideo equ LABLE_GDT_VIDEO - LABEL_GDT 3LABEL_START:; 打印Xmov ax, 0B800hmov gs, axmov ah, 0Chmov al, Xmov [gs:(80 * 16 20)*2], ax; 下面的代码把kernel.bin从软盘中加载到内存中和从软盘中读取loader.bin相似; 请看上一篇文章《开发引导扇区》中的代码注释。mov ax, BaseOfKernelmov es, axmov ax, 0x9000mov ds, ax; 复位软驱mov ah, 00hmov dl, 0int 13hmov ax, FirstSectorOfRootDirectorymov cl, 1mov bx, OffSetOfLoadercall ReadSectormov cx, 4mov bx, (80 * 18 40) * 2mov di, OffSetOfLoader SEARCH_FILE_IN_ROOT_DIRECTORY:cmp cx, 0jz FILE_NOT_FOUNDpush cxmov si, LoaderBinFileNamemov cx, LoaderBinFileNameLengthmov dx, 0 COMPARE_FILENAME:;cmp [es:si], [ds:di];cmp [si], [di]lodsbcmp al, byte [es:di]jnz FILENAME_DIFFIERENTdec cxinc diinc dxcmp dx, LoaderBinFileNameLengthjz FILE_FOUNDjmp COMPARE_FILENAME FILENAME_DIFFIERENT:mov al, Emov ah, 0Chmov [gs:bx], axadd bx, 160pop cx ; 在循环中cx会自动减少吗cmp cx, 0dec cxjz FILE_NOT_FOUND;;;;;xchg bx, bxand di, 0xFFE0 ; 低5位设置为0其余位数保持原状。回到正在遍历的根目录项的初始位置add di, 32 ; 增加一个根目录项的大小jmp SEARCH_FILE_IN_ROOT_DIRECTORY FILE_FOUND:mov al, Smov ah, 0Ahmov [gs:(80 * 23 35) *2], ax;;;;xchg bx, bx; 修改段地址和偏移量后获取的第一个簇号错了 ; 获取文件的第一个簇的簇号and di, 0xFFE0 ; 低5位设置为0其余位数保持原状。回到正在遍历的根目录项的初始位置; 获取文件的第一个簇的簇号add di, 0x1Amov si, dimov ax, BaseOfKernelpush dsmov ds, ax;;;;xchg bx, bxlodswpop ds push ax;;;;xchg bx, bx ; call GetFATEntrymov bx, OffSetOfLoader; 获取到文件的第一个簇号后开始读取文件 READ_FILE:;;;;xchg bx, bxpush bx; push ax; 簇号就是FAT项的编号把FAT项的编号换算成字节数;;push bx;mov dx, 0;mov bx, 3;mul bx;mov bx, 2;div bx ; 商在ax中余数在dx中;mov [FATEntryIsInt], dx;;; 用字节数计算出FAT项在软盘中的扇区号;mov dx, 0;mov bx, 512;div bx ; 商在ax中余数在dx中。商是扇区偏移量余数是在扇区内的字节偏移量; 簇号就是FAT项的编号同时也是文件块在数据区的扇区号。; 用簇号计算出目标扇区在软盘中的的扇区号。add ax, 19add ax, 14sub ax, 2; 读取一个扇区的数据 start; add ax, SectorNumberOfFAT1mov cl, 1pop bx call ReadSector;;;;xchg bx, bxadd bx, 512; 读取一个扇区的数据 end;jmp READ_FILE_OVERpop axpush bxcall GetFATEntrypop bxpush axcmp ax, 0xFF8; 注意了ax 0xFF8 时跳转使用jc 而不是jz。昨天一定是在这里弄错了导致浪费几个小时调试。;jz READ_FILE_OVER ;jc READ_FILE_OVER jnb READ_FILE_OVER ;mov al, A;inc al;mov ah, 0Ah;mov [gs:(80 * 23 36) *2], ax ;;;;;xchg bx, bx jmp READ_FILEFILE_NOT_FOUND:mov al, Nmov ah, 0Ahmov [gs:(80 * 23 36) *2], axjmp OVER READ_FILE_OVER:;xchg bx, bx;mov al, O;mov ah, 0Dh;mov [gs:(80 * 23 33) * 2], ax; 开启保护模式 start;cli;mov dx, BaseOfLoaderPhyAddr LABEL_PM_START ;;xchg bx, bx lgdt [GdtPtr] cliin al, 92hor al, 10bout 92h, almov eax, cr0or eax, 1mov cr0, eax;xchg bx, bx; 真正进入保护模式。这句把cs设置为SelectFlatX ;jmp dword SelectFlatX:(BaseOfLoaderPhyAddr 100h LABEL_PM_START);jmp dword SelectFlatX:dxjmp dword SelectFlatX:(BaseOfLoaderPhyAddr LABEL_PM_START); 开启保护模式 end; 在内存中重新放置内核;call InitKernel;;;xchg bx, bx;jmp BaseOfKernel:73h;jmp BaseOfKernel:61h;jmp BaseOfKernel2:400h;jmp BaseOfKernel:60h;jmp BaseOfKernel:0;jmp BaseOfKernel:OffSetOfLoader ;jmp BaseOfKernel2:0x30400;jmp BaseOfKernel:OffSetOfLoader;jmp BaseOfKernel:40h;jmp OVER OVER:jmp $BootMessage: db Hello,World OS! ;BootMessageLength: db $ - BootMessage ; 长度需要使用 equ BootMessageLength equ $ - BootMessageFirstSectorOfRootDirectory equ 19 SectorNumberOfTrack equ 18 SectorNumberOfFAT1 equ 1;LoaderBinFileName: db KERNEL BIN LoaderBinFileName: db KERNEL BIN LoaderBinFileNameLength equ $ - LoaderBinFileName ; 中间两个空格FATEntryIsInt equ 0 ; FAT项的字节偏移量是不是整数个字节0不是1是。 BytesOfSector equ 512 ; 每个扇区包含的字节数量 ; 根据FAT项的编号获取这个FAT项的值 GetFATEntry:; 用FAT项的编号计算出这个FAT项的字节偏移量 start; mov cx, 3; mul cx; mov cx, 2;div cx ; 商在al中余数在ah中 ; push axMOV ah, 00hmov dl, 0int 13hpop ax mov dx, 0mov bx, 3mul bxmov bx, 2div bx; 用FAT项的编号计算出这个FAT项的字节偏移量 endmov [FATEntryIsInt], dx; 用字节偏移量计算出扇区偏移量 startmov dx, 0; and ax, 0000000011111111b ; 不知道这句的意图是啥忘记得太快了; mov dword ax, al ; 错误用法; mov cx, [BytesOfSector]mov cx, 512div cx; push dxadd ax, SectorNumberOfFAT1 ; ax 是在FAT1区域的偏移。要把它转化为在软盘中的扇区号需加上FAT1对软盘的偏移量。; mov ah, 00h; mov dl, 0; int 13h; 用字节偏移量计算出扇区偏移量 end; mov dword ax, al; add ax,1mov cl, 2 mov bx, 0push espush dxpush axmov ax, BaseOfFATEntrymov es, axpop ax; 用扇区偏移量计算出在某柱面某磁道的扇区偏移量可以直接调用ReadSectorcall ReadSector;pop es;;;;;;xchg bx, bx;pop ax;mov ax, [es:bx]pop dxadd bx, dxmov ax, [es:bx]pop es; 根据FAT项偏移量是否占用整数个字节来计算FAT项的值cmp byte [FATEntryIsInt], 0jz FATEntry_Is_Intshr ax, 4 FATEntry_Is_Int:and ax, 0x0FFFret; 读取扇区 ReadSector:push axpush bppush bxmov bp, spsub sp, 2mov byte [bp-2], cl; push al ; error: invalid combination of opcode and operands;push cx; mov bx, SectorNumberOfTrack; ax 存储在软盘中的扇区号mov bl, SectorNumberOfTrack ; 一个磁道包含的扇区数div bl ; 商在al中余数在ah中mov ch, alshr ch, 1 ; ch 是柱面号mov dh, aland dh, 1 ; dh 是磁头号mov dl, 0 ; 驱动器号0表示A盘inc ahmov cl, ah;add cl, 1 ; cl 是起始扇区号; pop al ; al 是要读的扇区数量mov al, [bp-2]add sp, 2mov ah, 02h ; 读软盘pop bx;mov bx, BaseOfKernel ; 让es:bx指向BaseOfKernel;mov ax, cs;mov es, ax;;;;;;xchg bx, bxint 13h;pop cx;;;;;;xchg bx, bx; pop bxpop bppop axret ; ; mov ch, 0 ; mov cl, 1 ; mov dh, 0 ; mov dl, 0 ; mov al, 1 ; 要读的扇区数量 ; mov ah, 02h ; 读软盘 ; mov bx, BaseOfKernel ; 让es:bx指向BaseOfKernel ; int 13h ; int 13h 中断 ; ret; 读取扇区 ReadSector2:mov ch, 0mov cl, 2mov dh, 1mov dl, 0mov al, 1 ; 要读的扇区数量mov ah, 02h ; 读软盘mov bx, BaseOfKernel ; 让es:bx指向BaseOfKernelint 13h ; int 13h 中断retBaseOfKernel equ 0x8000 BaseOfKernel2 equ 0x6000 BaseOfKernel3 equ 0x0 OffSetOfLoader equ 0x0 BaseOfFATEntry equ 0x1000 BaseOfLoader equ 0x9000BaseOfLoaderPhyAddr equ BaseOfLoader * 10h ; LOADER.BIN 被加载到的位置 ---- 物理地址 ( BaseOfLoader * 10h)[SECTION .s32]ALIGN 32[BITS 32] LABEL_PM_START:; 初始化一些寄存器mov ax, SelectFlatWRmov ds, axmov es, axmov fs, axmov ss, axmov ax, SelectVideomov gs, axmov gs, axmov al, Kmov ah, 0Ahmov [gs:(80 * 19 25) * 2], ax; 重新放置内核call InitKernel;xchg bx, bx ;mov gs, axmov al, Gmov ah, 0Ahmov [gs:(80 * 19 20) * 2], axxchg bx, bx;jmp 0x30400; 开始执行内核代码。jmp SelectFlatX:0x30400; 等同于while(1){}jmp $jmp $jmp $jmp $; 重新放置内核 InitKernel:push eaxpush ecxpush esi;xchg bx, bx;程序段的个数;mov cx, word ptr ds:0x802cmov cx, [BaseOfKernelPhyAddr 2CH]movzx ecx, cx;程序头表的内存地址xor esi, esimov esi, [BaseOfKernelPhyAddr 1CH]add esi, BaseOfKernelPhyAddr;xchg bx, bx .Begin:mov eax, [esi 10H]push eaxmov eax, BaseOfKernelPhyAddradd eax, [esi 4H]push eaxmov eax, [esi 8H]push eaxcall Memcpy;xchg bx, bx; 三个参数每个占用32位4个字节2个字占用6个字,12个字节add esp, 12dec ecxcmp ecx, 0jz .NoActionadd esi, 20Hjmp .Begin .NoAction:;xchg bx, bxpop esipop ecxpop eaxret; Memcpy(p_vaddr, p_off, p_size) Memcpy:push ebpmov ebp, esppush eaxpush ecxpush esipush edi;mov bp, sp;mov di, [bp 4] ; p_vaddr即 dst;mov si, [bp 8] ; p_off即 src;mov cx, [bp 12] ; 程序头的个数即p_size;mov di, [bp 8] ; p_vaddr即 dst;mov si, [bp 12] ; p_off即 src;mov cx, [bp 16] ; 程序头的个数即p_sizemov edi, [ebp 8] ; p_vaddr即 dstmov esi, [ebp 12] ; p_off即 srcmov ecx, [ebp 16] ; 程序头的个数即p_sizepush es; 在32位模式下这两步操作不需要。而且我没有找到把大操作数赋值给小存储单元的指令。; mov es, edi; mov edi, 0 .1:mov byte al, [ds:esi]mov [es:edi], alinc esiinc edidec ecxcmp ecx, 0jz .2jmp .1 .2:pop esmov eax, [ebp 8]pop edipop esipop ecxpop eaxpop ebpretBaseOfKernelPhyAddr equ 80000h ; Kernel.BIN 被加载到的位置 ---- 物理地址 中的段基址部分求道之人不问寒暑。
http://www.hkea.cn/news/14262863/

相关文章:

  • 甘肃省建设监理协会网站陕西省建设招投标网站
  • 围场网站建设长春快速建站公司
  • 网站建设php教程微信公众平台注册平台
  • 哈尔滨建设网官方网站模板网站
  • 看房自己的网站建设多少钱网站三大标签优化
  • 建站平台工具佛山seo代理
  • 上海龙象建设集团公司网站做IT的会做网站吗
  • 第一推是谁做的网站湖南建设监理工程网站
  • 仿网站 涉及侵权吗最新电子产品网站模板
  • 苏州建设项目备案网站竞价单页制作教程
  • 黄石建设网站小白wordpress
  • 一个网站可以有几个域名网页设计去除下划线代码
  • 台州网站制作服务淘宝友情链接怎么设置
  • 网站建设与管理需要哪些证书合肥建设网站
  • 赣州律师网站建设信息门户网站是什么
  • 重庆网站建设哪家便宜制作公司网站的费用
  • 珠海建站模板源码推广费
  • 怎么知道网站有没有被收录北京建站系统模板
  • 如何建设一个查询系统网站网站设计方案定制
  • wordpress 电影下载站源码网站开发笔记本要什么配置
  • 义乌本地网站开发android开发技术有哪些
  • 做性的网站有哪些内容wordpress网站静态页面
  • 商务网站规划与建设salutation wordpress
  • mvc5网站开发实战详解logo是个网站
  • 做国际黄金看什么网站广西金利建设有限公司网站
  • 网站建设的商业目的四川建设厅网站 蒋
  • 北京电商购物网站开发网络公司app制作
  • 网站开发人员兼职深圳买门的网站建设
  • 想让网站被谷歌收录怎么做计算机前端和后端
  • 怎么做公司网站需要什么科目北京企业网站建设方案