0%

操作系统真相还原Ch4-2

进入保护模式

保护模式是在loader.bin中进入的,除了要更新loader.S之外,还应该更新两个文件:mbr.Sinclude/boot.inc

由于loader.bin超过了512字节,所以要把mbr.S中加载loader.binloader.binmbr.bin中的函数rd_disk_m_16读入)的读入扇区增大,由1扇区增加到4扇区,待读入的扇区数(cx)应该大于loader.bin的大小

1
2
3
;在mbr.S中修改
mov cx,4 ;待读入的扇区数
call rd_disk_m_16 ;函数调用,以下读取程序的起始部分

mbr.S完整程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
;主引导程序 
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax

; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h

; 输出字符串:MBR
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁,4表示前景色为红色

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4

mov eax,LOADER_START_SECTOR ; 起始扇区lba地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,4 ; 待读入的扇区数,上面提到要修改的地方
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)

jmp LOADER_BASE_ADDR

;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数

mov eax,esi ;恢复ax

;第2步:将LBA地址存入0x1f3 ~ 0x1f6

;LBA地址7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al

;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al

;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f ;lba第24~27位
or al,0xe0 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al

;第3步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al

;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等。

;第5步:从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
; 共需di*512/2次,所以di*256
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret

times 510-($-$$) db 0
db 0x55,0xaa

include/boot.inc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
;-------------	 loader和kernel   ----------

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;-------------- gdt描述符属性 -------------
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暂置为0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b
DESC_P equ 1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_CODE equ 1_000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b
DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.

DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b

;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b

equ是nasm的伪指令,即equal。符号名采用DESC_字段名_字段相关信息的形式,DESC_G_4K是4K粒度

loader.S是关键

6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
   %include "boot.inc"
section loader vstart=LOADER_BASE_ADDR ;0x900
LOADER_STACK_TOP equ LOADER_BASE_ADDR ;LOADER_STACK_TOP是保护模式下的栈
jmp loader_start ; 此处的物理地址是:
;使用dd指令deine double-word定义双字变量
;程序编译后的地址从上到下越来越高,下面用dd定义的数据地址(段描述符的高4字节)要高于上面的(段描述符的低4字节)
;即先定义低4字节,再定义高四字节
;构建gdt及其内部的描述符--begin
GDT_BASE: dd 0x00000000 ;GDT的起始地址
dd 0x00000000

CODE_DESC: dd 0x0000FFFF ;代码段描述符,0-15位是段界限FFFF,16-31是段基址0000
dd DESC_CODE_HIGH4 ;高四字节定义麻烦,所以在boot.inc提前定义好,直接拿来用

DATA_STACK_DESC: dd 0x0000FFFF ;数据段和栈段描述符
dd DESC_DATA_HIGH4 ;为简单,直接用普通的数据段作栈段

VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7 ;显存段描述符低4字节
dd DESC_VIDEO_HIGH4 ; 此时dpl已改为0
;实模式下文本模式显示适配器的内存地址0xb8000-0xbffff, 0xc0000是显示适配器BIOS所在区域
;为方便,把段基址设为文本模式的起始地址0xb8000段大小0x7fff=0xbffff-0xb8000,段粒度4K,所以段界限=7
;--end.构建全局描述符表,并在里面填充段描述符
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
;通过地址差获得GDT大小,使用GDT大小-1获得段界限,为加载GDT作准备
times 60 dq 0 ; 此处预留60个描述符的slot,为将来往GDT添加其他描述符,提前保留空间
;dq是define quad-word,定义4 word即8字节,times是nasm的伪指令,循环
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址

gdt_ptr dw GDT_LIMIT
dd GDT_BASE ;定义全局描述符表GDT的指针,此指针是lgdt加载GDT到gdtr寄存器时用的,48位
loadermsg db '2 loader in real.' ;定义字符串,显示一下要进入保护模式了

loader_start:

;------------------------------------------------------------
;INT 0x10 功能号:0x13 功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址
;AL=显示输出方式
; 0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
; 1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
; 2——字符串中含显示字符和显示属性。显示后,光标位置不变
; 3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg ; ES:BP = 字符串地址
mov cx, 17 ; CX = 字符串长度
mov ax, 0x1301 ; AH = 13, AL = 01h
mov bx, 0x001f ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
mov dx, 0x1800 ;
int 0x10 ; 10h 号中断

;---------------------------------------- 准备进入保护模式 ------------------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1,表示为保护模式;0位实模式


;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------------- 加载GDT ----------------
lgdt [gdt_ptr] ;gdt_ptr是GDT地址指针变量,前16位是GDT界限值,后32位是GDT起始地址,
;gdt_ptr本身是个地址,使用[]表示在该地址处取值

;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

;jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,无条件跳转指令
jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax ;用选择子初始化成各段寄存器

mov byte [gs:160], 'P' ;传入P字

jmp $

在bochs中,控制寄存器和状态寄存器的相应位为1,bochs会用大写,0对应小写

文件相关:

  • 创建文件: touch a.txt

  • 创建文件夹: mkdir NewFolder

  • 删除文件: rm a.txt

  • 删除文件夹: rmdir NewFolder

  • 删除带有文件的文件夹: rm -r NewFolder

编译:

nasm -I include/ -o mbr.bin mbr.S

nasm -I include/ -o loader.bin loader.S

写入硬盘

dd if=/root/bochs-2.7/mbr.bin of=/root/bochs-2.7/hd60M.img bs=512 count=1 conv=notrunc

dd if=/root/bochs-2.7/loader.bin of=/root/bochs-2.7/hd60M.img bs=512 count=4 seek=2 conv=notrunc

!!! count=4 不是 1

感谢博主:操作系统真象还原 学习笔记04–保护模式入门

在书中会显示一些细节信息:creginfo gdt等,而bochs一直在循环过程中,要想结束循环,键盘Ctrl+c可中止程序进行查看详细信息。

处理器微架构

流水线

指令执行单元EU是执行指令的唯一部件,一次只能执行一个指令,在单核CPU的情况下,只有一个指令处于执行中。

CPU的指令执行过程分为取指令、译码、执行三个步骤,每个步骤都是独立的,CPU是按照程序中指令顺序来填充流水线的,也就是按照PC寄存器里的值来装载流水线,如果有jmp指令,后面的就没有用了,所以CPU在遇到无条件的转移指令jmp时会清空流水线。

乱序执行

指CPU运行的指令并不按照代码中的顺序执行,而是按照一定的策略打乱,也许后面的指令先执行,得保证指令之间不具备相关性。

X86最初使用CISC指令集(complex instruction set computer)复杂指令集计算机,与之相对的是RISC(reduced…)精简指令集计算机

缓存

CPU中的L1、L2、L3级缓存,都被称为SRAM,静态随机访问存储器。

为程序的局部性采取缓存原理:时间局部性(最近访问过的指令和数据)、空间局部性(靠近当前访问内存空间的内存地址)。

对于无条件跳转,没有什么,所谓的预测是针对有条件的跳转,最简单的统计是根据上一次跳转的结果来预测本次;最简单的方法是2位预测法,用2位bit的计数器来记录跳转状态,每跳转一次加1,直到3就不加了,如果未跳转减1,减到0停止,如果计数器大于1则跳转,小于等于1不跳。

静态预测器策略:向上跳转则转移会发生,向下跳转则转移不发生。

如果分支预测错误,只要清空流水线就行,只是代价较大

使用远跳转指令清空流水线

上述代码中的无条件跳转指令

jmp dword SELECTOR_CODE:p_mode_start

为什么要用jmp远转移
  1. 段描述符缓冲寄存器未更新,之前还是实模式下的值,进入保护模式后要填入正确的信息
  2. 流水线中指令译码错误

内存段的保护

段描述符的属性字段中,每个字段都不是多余的,这些属性是用来描述一块内存的性质,给CPU参考,当有实际动作在这片内存上发生时,CPU用这些属性检查动作的合法性

向段寄存器加载选择子时的保护
  • 首先验证段描述符是否超过界限:描述符表基地址+选择子的索引值*8+7 <= 描述符表基地址+描述符表界限值
  • 检查段寄存器的用途与段类型是否匹配
  • 检查内存段是否存在,P位为1则存在,若为0说明转储到硬盘上了,这时会抛出异常,等加载到内存中,P变为1

7

8

代码段和数据段的保护

CPU每访问一个地址,都要确认该地址不能超过内存段范围,实际的段界限值:(描述符中段界限+1)*(段界限粒度:4K或1)-1

对于代码段,段中的数据是各种指令,使用CS:EIP访问指令的起始地址,满足:EIP中的偏移地址+指令长度-1 <= 实际段界限大小

栈段的保护

将段界限地址+1视为栈可以访问的下限

9