当前位置: 代码迷 >> 综合 >> 从零开始写操作系统-2---之自己动手写操作系统“保护模式”代码分析
  详细解决方案

从零开始写操作系统-2---之自己动手写操作系统“保护模式”代码分析

热度:51   发布时间:2023-12-15 04:11:47.0

距离自己写“从零开始写操作系统-1”已经有5天了,其中3天是本人去自学了汇编语言(因为本人非科班出身,之前只接触过51的汇编和ARM的少量汇编,并未接触过X86的汇编,所以找了本PDF(王爽的汇编语言第三版)从头到尾看了一遍,代码只少量实现了一遍,因为本人只希望短期内了解这些操作码,不过看了这本书,还是觉得直接与硬件打交道的感觉挺奇妙,遂以后找时间认真再学一遍,还有两天忙着去完成自己专业的期末设计,哎,虽我心向于操作系统,无奈烦事众多啊)
好,闲话说到这里,因为本人花了一个晚上去看“orange s 一个操作系统的实现”(自己动手写操作系统的第二版)的第三章保护模式,看的我一头雾水,而在第三章开头的那一大串代码(各位大神别喷,因为本人刚接触汇编,觉得这一串已经很长了)也是读的一脸懵逼,遂慢慢看,并且在网上搜索有关材料,终于把这串代码初步看懂了,也写下此文来帮助那些和我一样的小白学徒,让大家在分析这段代码得时候少走弯路。
在此贴上这一大串代码(保护模式的概念大家还是可以去看书“orange s 一个操作系统的实现”,相信作者比我这个小白说的要明白多了)

; ==========================================
; pmtest1.asm
; 编译方法:nasm pmtest1.asm -o pmtest1.com
; ==========================================%include    "pm.inc"    ; 常量, 宏, 以及一些说明org 0100hjmp LABEL_BEGIN[SECTION .gdt]
; GDT
; ---------------------------------- 段基址,----- 段界限,---------- 属性---------
LABEL_GDT:        Descriptor   0,          0,                0         ; 空描述
LABEL_DESC_CODE32: Descriptor  0,  SegCode32Len - 1 ,   DA_C + DA_32   ; 非一致代码段
LABEL_DESC_VIDEO:  Descriptor  0B8000h,     0ffffh,        DA_DRW      ; 显存首地址
; GDT 结束GdtLen      equ $ - LABEL_GDT  ; GDT长度
GdtPtr      dw  GdtLen - 1  ; GDT界限dd  0       ; GDT基地址; GDT 选择子
SelectorCode32      equ LABEL_DESC_CODE32   - LABEL_GDT
SelectorVideo       equ LABEL_DESC_VIDEO    - LABEL_GDT
; END of [SECTION .gdt][SECTION .s16]
[BITS   16]
LABEL_BEGIN:mov ax, csmov ds, axmov es, axmov ss, axmov sp, 0100h; 初始化 32 位代码段描述符xor eax, eaxmov ax, csshl eax, 4add eax, LABEL_SEG_CODE32mov word [LABEL_DESC_CODE32 + 2], axshr eax, 16mov byte [LABEL_DESC_CODE32 + 4], almov byte [LABEL_DESC_CODE32 + 7], ah; 为加载 GDTR 作准备xor eax, eaxmov ax, dsshl eax, 4add eax, LABEL_GDT      ; eax <- gdt 基地址mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址; 加载 GDTRlgdt    [GdtPtr]; 关中断cli; 打开地址线A20in  al, 92hor  al, 00000010bout 92h, al; 准备切换到保护模式mov eax, cr0or  eax, 1mov cr0, eax; 真正进入保护模式jmp dword SelectorCode32:0  ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
; END of [SECTION .s16][SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS   32]LABEL_SEG_CODE32:mov ax, SelectorVideomov gs, ax          ; 视频段选择子(目的)mov edi, (80 * 10 + 0) * 2  ; 屏幕第 10 行, 第 0 列。mov ah, 0Ch         ; 0000: 黑底 1100: 红字mov al, 'P'mov [gs:edi], ax; 到此停止jmp $SegCode32Len    equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]

此代码分析,从另一方面说,是提供给那些书上的代码分析看不懂的小白,所以具体的代码的原理以及详细分析可以参照书本[在看本文之前应该先去看看书本,书本上面详细叙述了的,本文即略去]
注:本文对于标识符,即一串字符并加个: 如代码的第15行-18行前面的一串LABEL字符串都是标识符(也可以称为标号),本文将标识符理解为程序运行到此处相对于段首的偏移地址,当然新手可直接理解为标号所在的位置处距离本段段首[SECTION]处的代码长度

[6]行是定义了一个文件,这个文件即描述了descriptor的详细内容,此处可参照代码与书上图3.4处即可分析出来(此处也可以不去深入研究,因为descriptor是被定义好的,完全不需要修改)

[8]行指明正式代码将被加载到0100h内存中,然后jmp LABEL_BEGIN ,即代码从LABEL_BEGIN处开始运行。

[12]gdt文件开始,[SECTION .gdt]意味着gdt这个部分从此处开始

[15-19]是定义GDT中的内容,其中descriptor是表示GDT中的一个项,可以把GDT看做一个结构体数组,则一个descriptor就是一个数组元素,通俗点说,[15-18]中每一行对应着这个数组的一个元素,这个元素也是一个结构体,这个结构体中有三个成员,分别是段基址,段界限和属性,其中段基址代表着这个段的物理地址(此处要注意),段界限是段的最大偏移量,而属性,就不必多说了,书本已详细介绍,此处我们可以看到LABEL_DESC_CODE32的段界限为SegCode32Len - 1,因为段偏移是从0开始的,比如我们要执行两句指令,分别是指令1和指令2,指令1的物理地址是CS+0,而指令2的物理地址是CS+1,所以这里的1即为最大偏移量,就是此处的段界限,而2是指令的总长度,所以段界限 = 指令的总长度-1。至于为何SegCode32Len是-LABEL_SEG_CODE32的总长度呢,我们看第97行,其中SegCode32Len equ $ - LABEL_SEG_CODE32 ,意味着SegCode32Len =(equ 即为equal “=”)$(程序运行到当前行的偏移地址)(即97行的运行偏移地址)-LABEL_SEG_CODE32(程序运行到LABEL_SEG_CODE32的偏移地址)(即85行的运行偏移地址),所以97行的运行偏移地址-85行的运行偏移地址,即中间20行的指令总长度,也就是LABEL_SEG_CODE32的总长度,即为SegCode32Len,所以减去1就是LABEL_DSEG_CODE32中的段界限。

[23]行中GDT的长度为$(运行到本行的偏移地址)-LABEL_GDT(程序运行到15行的偏移地址),所以结果就是整个GDT段的总长度,而界限就是总长度减1,(dw,即定义一个word,16位来存储界限),而[25]行中dd为define double word 定义一个双字(32)位来存储基地址,0为暂时用0来填充,稍后再去更改

[27-30]行定义GDT的选择子,简而言之此处就是定义GDT中的各个元素相对于段首的偏移量,(此处也可以理解为就是选择子的特性,我们可以根据偏移量直接定位到选择子中的所有信息,然后根据基地址和段界限定位到一个段),SelectorCode32 =LABEL_DESC_CODE32(16行,LABEL_DESC_CODE32这行的偏移地址)- LABEL_GDT(15行,LABEL_GDT这行的偏移地址),我们可以通过SelectorCode32直接使用LABEL_DESC_CODE32(16行)中的信息。Selectorvideo同理分析。

[32]行标志着工作在16位模式下的代码段开始,[BITS 16]即让CPU工作在16位模式。
LABEL_BEGIN为标号,表示文件运行从此处开始。

[35-40]行,将本段的段地址CS赋给ax,然后将ax赋值给ds,es,ss寄存器,即cs=ds=ss=es,所以栈地址从ss:sp即cs:0100h处开始,其余几个寄存器稍后再用。

[41-50]行是实现初始化 32 位代码段描述符的作用,其中 xor eax,eax eax为ax寄存器的extend扩展寄存器,在ax的基础上增加了16位,所以eax寄存器是一个32位寄存器,低16位就是ax寄存器,这两个寄存器是主体与部分的区别。(此处后面还会详细叙述),xor命令是异或,即让eax自己与自己异或,就是清零,只不过这个指令清零更彻底。
mov ax, cs 就是将cs赋值到ax中,即就是赋值到eax的低16位上。然后shl eax, 4 让eax左移4位,这里左移就是相当于段地址*16,因为cs是一个段地址,我们如今希望通过人工算出真实的物理地址(段地址*16+偏移地址,共20位),平时都是给出段地址和偏移量,由地址生成器来完成计算真实的物理地址的操作,此处需要我们进行人工计算,因为16位模式下所有的寄存器都是16位,所以我们只有借助eax寄存器(虽然此时CPU工作在16位模式下,但32位寄存器依旧可以使用),因此我们将eax左移4位(相当于乘以16)再加上LABEL_SEG_CODE32(第85行的运行偏移地址),所以得到了32位代码段的真实物理地址(就是LABEL_SEG_CODE32处的真实物理地址),将其存入内存[LABEL_DESC_CODE32 + 2]中(为何存在这里,看图分析),然后shr eax, 16 右移16位将已经存储了的低16位移出eax,继续将剩下的高16位存入描述符中
这里写图片描述

从图中可以看到,段基地址(物理地址)分散在byte2-4以及byte7,所以我们如今需要将eax中物理地址放入这4个字节中,其中mov word [LABEL_DESC_CODE32 + 2], ax 是将ax,也就是eax的低16位放入byte2和byte3,(因为这里是mov word 即移动一个字,16位),然后shr eax, 16 将已经存储的16位移除,将高16位移到ax中(因为我们已经看到图中表示的,我们还未存储的段基址是两个字节,即byte4和byte7,所以我们只能一次操作一个字节)所以我们只有使用al和ah这两个寄存器才能一次操作8个字节。然后
mov byte [LABEL_DESC_CODE32 + 4], al 将ax中的低8位[原eax中的bit17~bit24]8个位存储在byte4,mov byte [LABEL_DESC_CODE32 + 7], ah 将ax中的高8位[原eax中的bit25~bit32]存储在byte7,所以至此,初始化32位代码段描述符即看懂了。
[]中的内容为一个标识符加上一个数字,标识符代表此标识符出现时的运行相对地址,读者可简单理解为此标识符在代码中出现的位置,而加上的数字代表字节数,+2即为标识符在代码中出现时的位置再往后移2个字节,从此处开始存储(本人这样叙述只是为了让人看懂,当然叙述的不严谨,如有错误望各位看官提醒)[此处如还不明白,建议去看看汇编语言中的叙述]

[52-57]段中先xor将eax清零,在将ds(=cs)赋值给eax的低16位,再左移4位后加上LABEL_GDT即为LABEL_GDT处的真实物理地址[此处也是人工计算物理地址的操作,如看不懂请仔细看我上面[41-50]行的代码分析],并将真实的物理地址放在25行中Gdtptr的基地址处(
因为GDT界限是dw定义的,定义了一个字,16位,所以基地址从GdtPtr加上2开始存储)

[60]行即为加载GDTR,将GdtPtr的内容加载到相应的寄存器中,cli为关中断

[65-68]行为打开地址线A20(从实模式跳入保护模式必须将A20地址线打开),此处较为复杂,使用时可照葫芦画瓢

[70-74]行为使CPU运行到保护模式.Cr0寄存器的第0位PE位为1则预示CPU运行于保护模式,所以[70-74]行就是使cr0的第0位置为1(和1相与,即和000…001b相与,将第0位置为1,最右边一位是第0位),使CPU运行于保护模式

[76]行jmp正式跳入保护模式,jmp dword SelectorCode32:0,即将SelectorCode32选择子所对应的LABEL_DESC_CODE32中定义的段装入cs,此步即表示进行跳转入32位代码处。(32位中的cs不是严格的段地址了,而就表示一个真实的物理地址(或者可以称为一个段)。SelectorCode32选择子(28行)对应的LABEL_DESC_CODE32(16行)中的基地址以及段界限联合定义了一个段,而ip中的数则表示一个偏移量,也就是在上面定义的段的基础上的一个偏移量。简单来说,32位中的cs:ip不是cs*16+ip,而是选择子定义了一个段,我们此处将这个段装入cs中,然后ip在这个段中进行选择相应的位置(偏移地址),所以在32位中我们需要的都是物理地址,不能通过地址生成器来进行自动生成,因此必须通过选择子来定义段这种方法)

[82~83]为32位代码段开始,即刚刚从实模式jmp到的地方,[BITS 32]标志CPU开始运行于32位模式下

[86~92]为将SelectorVideo选择子(LABEL_DESC_VIDEO中的基地址,和段界限联合定义的段)赋值给gs(现在gs中的内容就是显存这个段),然后对edi进行赋值(edi表示我们即将把ax赋值给显存这个段中的第edi个位置),即显示到某个具体的位置,然后再将即将显示的字符赋值给ax,将其写入gs:edi即显存中,然后jmp $ ,进行停等待

[97]行在前面的代码中已经分析过了

[98]行32位代码段结束






注:
32位代码段与数据段描述符的详细情况此图片来源于互联网,如涉及侵权,望通知本人,本人立即删除

说明:
;
; (1) P: 存在(Present)位。
; P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中;
; P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常。
;
; (2) DPL: 表示描述符特权级(Descriptor Privilege level),共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段能否访问。
;
; (3) S: 说明描述符的类型。
; 对于存储段描述符而言,S=1,以区别与系统段描述符和门描述符(S=0)。
;
; (4) TYPE: 说明存储段描述符所描述的存储段的具体属性。
;
;
; 数据段类型 类型值 说明
; ———————————-
; 0 只读
; 1 只读、已访问
; 2 读/写
; 3 读/写、已访问
; 4 只读、向下扩展
; 5 只读、向下扩展、已访问
; 6 读/写、向下扩展
; 7 读/写、向下扩展、已访问
;
;
; 类型值 说明
; 代码段类型 ———————————-
; 8 只执行
; 9 只执行、已访问
; A 执行/读
; B 执行/读、已访问
; C 只执行、一致码段
; D 只执行、一致码段、已访问
; E 执行/读、一致码段
; F 执行/读、一致码段、已访问
;
;
; 系统段类型 类型编码 说明
; ———————————-
; 0 <未定义>
; 1 可用286TSS
; 2 LDT
; 3 忙的286TSS
; 4 286调用门
; 5 任务门
; 6 286中断门
; 7 286陷阱门
; 8 未定义
; 9 可用386TSS
; A <未定义>
; B 忙的386TSS
; C 386调用门
; D <未定义>
; E 386中断门
; F 386陷阱门
;
; (5) G: 段界限粒度(Granularity)位。
; G=0 表示界限粒度为字节;
; G=1 表示界限粒度为4K 字节。
; 注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。
;
; (6) D: D位是一个很特殊的位,在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常是堆栈段)的三种描述符中的意义各不相同。
; ⑴ 在描述可执行段的描述符中,D位决定了指令使用的地址及操作数所默认的大小。
; ① D=1表示默认情况下指令使用32位地址及32位或8位操作数,这样的代码段也称为32位代码段;
; ② D=0 表示默认情况下,使用16位地址及16位或8位操作数,这样的代码段也称为16位代码段,它与80286兼容。可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大小。
; ⑵ 在向下扩展数据段的描述符中,D位决定段的上部边界。
; ① D=1表示段的上部界限为4G;
; ② D=0表示段的上部界限为64K,这是为了与80286兼容。
; ⑶ 在描述由SS寄存器寻址的段描述符中,D位决定隐式的堆栈访问指令(如PUSH和POP指令)使用何种堆栈指针寄存器。
; ① D=1表示使用32位堆栈指针寄存器ESP;
; ② D=0表示使用16位堆栈指针寄存器SP,这与80286兼容。
;
; (7) AVL: 软件可利用位。80386对该位的使用未左规定,Intel公司也保证今后开发生产的处理器只要与80386兼容,就不会对该位的使用做任何定义或规定。
;

  相关解决方案