《操作系统真象还原》第4章

说实话,这大概已经是我第四遍看实模式、保护模式的相关内容了,xv6确实是个好东西,那其中的代码与书中示例的思想和结构非常相似,所以我理解书中的这些代码也并没有感到很吃力。但常读常新,还是颇有收获的:

1.

保护模式在 Intel 80286 CPU 中首次出现。

实模式缺点:

①实模式下操作系统和用户程序属于同一特权级,这哥俩平起平坐,没有区别对待。

②用户程序所引用的地址都是指向真实的物理地址,也就是说逻辑地址等于物理地址,实实在在地指哪打哪。

③用户程序可以自由修改段基址,可以不亦乐乎地访问所有内存,没人拦得住。

以上原因属于安全缺陷,没有安全可言的 CPU 注定是不可依赖的,这从基因上决定了用户程序乃至操作系统的数据都可以被随意地删改,一旦出事往往都是灾难性的,而且不容易排查。

④访问超过64kb的内存区域时要切换段基址,转来转去容易晕乎。

⑤一次只能运行一个程序,无法充分利用计算机资源。

⑥共 20 条地址线,最大可用内存为 1MB,这即使在 20 年前也不够用。

 2.

3.

每个属性的含义就不细说了,不然太过啰嗦,随时翻阅书本即可。

“但现在即使内存不足时,也没有将整个段都换出去的,现在基本都是平坦模型,一般情况下,段都要4GB大小,换到硬盘不也是很占空间吗?而且这些平坦的段都是公用的,换出去就麻烦啦。所以这些是未开启分页时的解决方案,保护模式下有分页功能,可以按页(4KB)的单位来将内存换入换出。”

4.

5.

6.流水线

“由于在实模式下时,指令按照16位指令格式来译码,第78~82行既有16位指令,又有32位指令,所以流水线把32位指令按照16位译码就会出错。解决这问题的方法就是无条件跳转指令清空流水线。”

“仔细想想看,其实这个流水线没用了,因为CPU早已经跳到别处去执行了,第二、三条指令用不上了,所以CPU在遇到无条件跳转指令jmp时,会清空流水线。”

在学习xv6时,我也一直没想明白那句ljmp指令到底怎么清空的流水线,现在答案就很明了了。

7.分支预测

以前只是听说过“分支预测”这个概念,并没有深入去了解过,因此详细摘录一下:

“Intel的分支预测部件用了分支目标预测器(BTB)。”

“BTB中记录着分支指令地址,CPU遇到分支指令时,先用分支指令的地址在BTB中查找,若找到相同地址的指令,根据跳转统计信息判断是否把相应的预测分支地址上的指令送上流水线。在真正执行时,根据实际分支流向,更新BTB中跳转统计信息。

如果BTB中没有相同记录该怎么办呢?这时候可以使用Static Predictor,静态预测器。为什么称为静态呢?这是因为存储在里面的预测策略是固定写死的,它是由人们经过大量统计之后,根据某些特征总结出来的。”

“如果分支预测错了,也就是说,当前指令执行结果与预测的结果不同,这也没关系,只要将流水线清空就好了。因为处于执行阶段的是当前指令,即分支跳转指令。处于‘译码’‘取指’的是尚未执行的指令,即错误分支上的指令。只要错误分支上的指令还没到执行阶段就可以挽回,所以,直接清空流水线就是把流水线上错误分支上的指令清掉,再把正确分支上的指令加入到流水线,只是清空流水线代价比较大。”

8.保护模式之内存段的保护

为了避免出现非法引用内存段的情况,在这时候,处理器会在以下几方面做出检查:

①根据段选择子的值检验段描述符是否超越界限。

②检查段类型(type位)。主要是检查段寄存器的用途和段类型是否匹配。

③检查完type后,还会再检查段是否存在(P位)。


 步骤:

1.进入保护模式


1.进入保护模式

由于loader.bin超过了512字节,所以我们要把mbr.S中加载loader.bin的读入扇区数增大,由1扇区直接改为4扇区:

然后修改include/boot.inc

注意,书中有误:

1.第13行(书中13行)的DESC_LIMIT_VIDEO2后面应该是16个0,少了1个0

2.第42行(书中27行)的DESC_VIDEO_HIGH4后面应该是0x0B,而不是0x00

复制代码
 1 ;----------------- loader 和 kernel -----------------
 2 LOADER_BASE_ADDR equ 0x900
 3 LOADER_START_SECTOR equ 0x2
 4 
 5 ;------------------ gdt描述符属性 -------------------
 6 DESC_G_4K equ 1_00000000000000000000000b ;第23位G 表示4K或者1MB位 段界限的单位值 此时为1则为4k
 7 DESC_D_32 equ 1_0000000000000000000000b  ;第22位D/B位 表示地址值用32位EIP寄存器 操作数与指令码32位
 8 DESC_L    equ 0_000000000000000000000b   ;第21位 设置成0表示不设置成64位代码段 忽略
 9 DESC_AVL  equ 0_00000000000000000000b    ;第20位 是软件可用的 操作系统额外提供的 可不设置
10 
11 DESC_LIMIT_CODE2  equ  1111_0000000000000000b   ;第16-19位 段界限的最后四位 全部初始化为1 因为最大段界限*粒度必须等于0xffffffff
12 DESC_LIMIT_DATA2  equ  DESC_LIMIT_CODE2         ;相同的值  数据段与代码段段界限相同
13 DESC_LIMIT_VIDEO2 equ  0000_0000000000000000b   ;第16-19位 显存区描述符VIDEO2 书上后面的0少打了一位 这里的全是0为高位 低位即可表示段基址
14 
15 DESC_P            equ   1_000000000000000b      ;第15位  P Present判断段是否存在于内存
16 DESC_DPL_0        equ  00_0000000000000b        ;第13-14位 DPL Descriptor Privilege Level 0-3
17 DESC_DPL_1        equ  01_0000000000000b        ;0为操作系统,权力最高;3为用户段,用于保护
18 DESC_DPL_2        equ  10_0000000000000b
19 DESC_DPL_3        equ  11_0000000000000b
20 
21 DESC_S_sys        equ  0_000000000000b           ;第12位为0 则表示系统段 为1则表示数据段
22 DESC_S_CODE       equ  1_000000000000b           ;第12位与type字段结合 判断是否为系统段还是数据段
23 DESC_S_DATA       equ  DESC_S_CODE
24 
25 DESC_TYPE_CODE    equ  1000_00000000b            ;第9-11位表示该段状态 1000 可执行 不允许可读 已访问位0
26 ;x=1 e=0 w=0 a=0
27 DESC_TYPE_DATA    equ  0010_00000000b            ;第9-11位type段 0010 可写
28 ;x=0 e=0 w=1 a=0
29 
30 ;代码段描述符高位4字节初始化 (0x00共8位 <<24 共32位初始化0)
31 ;4KB为单位 Data段32位操作数 初始化的部分段界限 最高权限操作系统代码段 P存在表示 状态
32 DESC_CODE_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
33 DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + \
34 DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0X00
35 
36 ;数据段描述符高位4字节初始化
37 DESC_DATA_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
38 DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + \
39 DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X00
40 
41 ;显存段描述符高位4字节初始化
42 DESC_VIDEO_HIGH4   equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \
43 DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + \
44 DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X0B
45 
46 ;-------------------- 选择子属性 --------------------------------
47 ;第0-1位 RPL 特权级比较是否允许访问;第2位 TI 0表示GDT 1表示LDT;第3-15位索引值
48 RPL0    equ 00b
49 RPL1    equ 01b
50 RPL2    equ 10b
51 RPL3    equ 11b
52 TI_GDT  equ 000b
53 TI_LDT  equ 100b
复制代码

再修改loader.S:

复制代码
 1 %include "boot.inc"
 2 SECTION LOADER vstart=LOADER_BASE_ADDR     ;同书上,设置为0x900
 3 LOADER_STACK_TOP equ LOADER_BASE_ADDR      ;初始化栈顶,0x900向下为栈空间
 4 jmp loader_start
 5 
 6 ;构建GDT及其内部的描述符
 7    GDT_BASE:   dd   0x00000000             ;没用的第0个段描述符
 8                dd   0x00000000
 9    CODE_DESC:  dd   0x0000FFFF
10                dd   DESC_CODE_HIGH4
11    DATA_STACK_DESE:   dd   0x0000FFFF
12                       dd   DESC_DATA_HIGH4
13    VIDEO_DESC: dd   0x80000007                ;limit=(0xbffff-0xb8000)/4k=0x7
14                dd   DESC_VIDEO_HIGH4          ;此时DPL为0
15    GDT_SIZE    equ  $-GDT_BASE                ;地址差作尺寸:当前行地址-GDT_BASE地址
16    GDT_LIMIT   equ  GDT_SIZE-1
17    times 60 dq 0                              ;此处预留60个描述符空位;dq 定义4字/8字节
18    SELECTOR_CODE   equ   (0x0001<<3)+TI_GDT+RPL0
19       ;相当于(CODE_DESC-GDT_BASE)/8+TI_GDT+RPL0
20    SELECTOR_DATA   equ   (0x0002<<3)+TI_GDT+RPL0
21    SELECTOR_VIDEO  equ   (0x0003<<3)+TI_GDT+RPL0
22 
23    ;以下是GDT的指针GDTR,6B/48bit,前2字节是GDT界限,后4字节是GDT起始地址
24    gdt_ptr     dw  GDT_LIMIT
25                dd  GDT_BASE
26    loadermsg   db  'Hello to Loader.'
27 
28 loader_start:
29 
30 ;---------------------------------------------------------------------
31 ;INT 0x10    功能号:0x13     功能描述:打印字符串
32 ;---------------------------------------------------------------------
33 ;输入:
34 ;AH 子功能号=13H
35 ;BH=页码
36 ;BL=属性
37 ;CX=字符串属性
38 ;(DH、DL)=坐标(行、列)
39 ;ES:BP=字符串地址
40 ;AL=显示输出方式
41 ; 0——字符串中只含显示字符,其显示属性在BL中
42      ;显示后,光标位置不变
43 ; 1——字符串中只含显示字符,其显示属性在BL中
44      ;显示后,光标位置改变
45 ; 2——字符串中含显示字符和显示属性。显示后,光标位置不变
46 ; 3——字符串中含显示字符和显示属性。显示后,光标位置改变
47 ;无返回值
48    mov sp,LOADER_BASE_ADDR
49    mov bp,loadermsg            ;ES:BP=字符串地址
50    mov cx,16                   ;CX=字符串长度
51    mov ax,0x1301               ;AX=13,AL=01H
52    mov bx,0x001F               ;页号为0(BH=0)蓝底粉红字(BL=1FH)
53    mov dx,0x1800
54    int 0x10                    ;10H号中断
55 
56 ;--------------------- 准备进入保护模式 ------------------------
57 ;1 打开A20
58 ;2 加载GDT
59 ;3 将cr0的PE位置1
60 
61 ;--------------------------- 打开A20 ---------------------------
62    in al,0x92
63    or al,0000_0010B            ;简单说,将端口0x92的第1位置1即可
64    out 0x92,al
65 
66 ;--------------------------- 加载GDT ---------------------------
67    lgdt [gdt_ptr]              ;load GDT [addr]
68 
69 ;-------------------------- cr0第0位置1 ------------------------
70    mov eax,cr0
71    or eax,0x00000001
72    mov cr0,eax
73 
74    jmp dword SELECTOR_CODE:p_mode_start     ;刷新流水线。因为要远转移,cs更新,所>以流水线上的其它指令都没用了,就会刷新
75 
76 [bits 32]     ;开启32位指令
77 p_mode_start:
78    mov ax,SELECTOR_DATA
79    mov ds,ax
80    mov es,ax
81    mov ss,ax
82    mov esp,LOADER_STACK_TOP
83    mov ax,SELECTOR_VIDEO
84    mov gs,ax
85 
86    mov byte [gs:160],'P'       ;第2行首字符打印P
87 
88    jmp $
复制代码

整个程序再没有初始化es,因为在mbr.S中已经初始化为0x0了。

然后编译指令:

nasm -I include/ -o loader.bin loader.S
dd if=/home/zbb/bochs/loader.bin of=/home/zbb/bochs/hd60M.img bs=512 count=2 seek=2 conv=notrunc

 一定要注意:第二条指令中count=2,而不再是count=1了!因为编译时,loader.bin写入硬盘大于512字节,因此参数count至少为2。

我真的是没注意到这个问题啊,模拟时一直不正确,用bochs调试时发现没进loader_start函数中,因为自己bochs调试用得也不是很熟练,昨晚盯了两个小时也没看出怎么回事,心想不可能啊,怎么能跳不到loader_start中呢。睡觉时还隐约看到了自己面对着调试界面的样子,真的是像汇编语言老师说的那样,“每晚睡觉都要拿着开发板,一个月后终于搞明白了”。今早上课偷偷看了很多博客才发现这个问题,原来是访问到了.img的野空间,太细节了,哎。

解决上面那个问题后,我发现还是不对:细细调试,然后比对代码发现有不少错误。所以还是墙裂推荐各位自己去亲手敲代码,不要直接copy,至少跟着书本写下来,而且在敲的过程中还会逼你思考这段代码为什么这样写。这样写的程序几乎必然会写错、写漏等等,那就多用bochs调试,看看reg、地址、反汇编语句,不经历这些坑你都不知道你写的代码有多愚蠢。

最后展示一下血泪:

是不是有些不同——能直接在WSL中运行bochs了,这个界面感觉比之前的要舒服多了:)

在本章临近结束之时,容我大概猜一下后面还要怎么折磨mbr.S和loader.S:关中断cli,加载kernel的ELF_header ...


参考博客:

posted @   Hell0er  阅读(322)  评论(0编辑  收藏  举报
编辑推荐:
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示