《操作系统真象还原》第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、地址、反汇编语句,不经历这些坑你都不知道你写的代码有多愚蠢。
最后展示一下血泪:
在本章临近结束之时,容我大概猜一下后面还要怎么折磨mbr.S和loader.S:关中断cli,加载kernel的ELF_header ...
参考博客:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库