0%

操作系统真相还原阅读笔记

操作系统真相还原阅读笔记

学的慢,忘的快,做个记录防止发生学了后面学了忘了前面的事情。

BIOS

基本输入输出模块,储存在rom中,rom也被包括在内存中,BIOS天生存在在内存中,所以不需要被装载,而且地址是固定,一般在0xf0000~0xfffff中,所以在机器通电的一瞬间cpu的cs:ip就指向了BIOS的某个地址,来运行BIOS,此时处于实模式。

BIOS主要检测硬件,做各种初始化,建立中断向量表。

MBR

主引导记录,固定于0盘0道1扇区,这个扇区称为MBR引导扇区,mbr存在在硬盘中,当BIOS执行完自己的逻辑,然后会把mbr加载到内存中,地址固定为0x7c00,最后BIOS执行jmp 0:7c00跳到MBR。

简单的mbr代码

代码并不复杂,主要是使用了BIOS中断例程0x10来向屏幕输出字符串hello mbr,其中比较有意思的点就是SECTION MBR vstart=0x7c00,让起始地址为0x7c00,这么做的原因是BIOS只负责把MBR加载到0x7c00指向的内存中,并不会给他重定位,所以代码本身就得从0x7c00开始,代码才会正常运行。

虽然不复杂,但给我一种错觉,好像编写底层代码并不是一件非常困难的事情,搞懂了代码的运行环境就比较容易了,比如下面这段代码,他的起始地址必须是0x7c00,运行在实模式中,有BIOS提供的一些中断API,知道了这些就可以编写简单的代码了(逃。

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
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00

mov ax,0x600
mov bx,0x700
mov cx,0
mov dx,0x184f
int 0x10

mov ah,3
mov bh,0
int 0x10

mov ax, msg
mov bp,ax
mov cx,9
mov ax,0x1301
mov bx,0x2
int 0x10

jmp $

msg db "hello mbr"
times 510-($-$$) db 0
db 0x55,0xaa

image-20230207213759574

上面是通过调用BIOS的api来完成的清屏和输出,其实可以直接操作显卡的显存来完成这一件事。主要就是把显存的第一页全置为0,然后把想输入的字符串输入进去。

比较麻烦的是实模式不支持很多的汇编语句,比如mov cl, byte [ax],赶紧进入保护模式吧。

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
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
mov bx,0
clear:
mov byte [gs:bx] ,0
add bx,1
cmp bx,4000
jnz clear

mov bx,0
mov di, msg
write:
mov cl,byte [di]
mov byte [gs:bx], cl
add bx,1
mov byte [gs:bx], 0xa4
add bx,1
add di,1
mov ax,di
cmp ax,msg+9
jnz write

jmp $

msg db "hello mbr"
times 510-($-$$) db 0
db 0x55,0xaa

image-20230209165230171

mbr只是启动的其中一个流程,主要的功能就是加载内核加载器,然后跳到内核加载器执行,内核加载器默认在第二个扇面,书中指定加载器加载到内存的0x900处,所以mbr需要做的工作就是把内核加载器loader从硬盘中读出来,读到内存0x900处,然后jmp 0x900就好了

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
%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
mov bx,0
clear:
mov byte [gs:bx] ,0
add bx,1
cmp bx,4000
jnz clear


mov bx,0
mov di, msg
write:
mov cl,byte [di]
mov byte [gs:bx], cl
add bx,1
mov byte [gs:bx], 0xa4
add bx,1
add di,1
mov ax,di
cmp ax,msg+9
jnz write


mov eax,LOADER_START_SELCTOR
mov bx,LOADER_BASE_ADDR
mov cx,1

call ld_disk_loader
jmp LOADER_BASE_ADDR

ld_disk_loader:
mov esi,eax ;備份eax
mov di,cx ;備份cx

;設置要讀取的扇區數
mov dx,0x1f2
mov al,cl
out dx,al
mov eax,esi

;把loader的地址存入0x1f3到0x1f6
mov dx,0x1f3
out dx,al

mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al

shr eax,cl
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al

mov dx,0x1f7
mov al,0x20
out dx,al

.not_ready:
nop
in al ,dx
and al,0x88
cmp al,0x08
jnz .not_ready

mov ax, di
mov dx,256
mul dx
mov cx,ax
mov dx,0x1f0

.read_loader:
in ax,dx
mov [bx],ax
add bx,2
loop .read_loader

ret

msg db "hello mbr"
times 510-($-$$) db 0
db 0x55,0xaa

写汇编还挺有意思的(逃。

保护模式

mbr把内核加载器加载到内存中然后跳到内核加载器执行代码,内核加载器第一步就应该切换模式,把实模式转换成保护模式,不然太束手束脚了。

感觉书上的关于显存段描述的代码错了,最后base=0x8000了,但其实得等于0xb8000才行,最后的截图也错了。

下面的汇编就是准备好gdt然后打开a20,加载gdt表到gdtr,然后打开cr0的pe,就正式进入了,最后再通过jmp SELECTOR_CODE:P_mode_start,既修改了cs又刷新了流水线,此时cpu真正进入了保护模式。

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
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR

jmp loader_start

GDT_BASE:
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000ffff
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000ffff
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4

GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0

SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

gdt_ptr:
dw GDT_LIMIT
dd GDT_BASE

msg:
db "loader in real."
dd 0x00000000



loader_start:
mov sp,LOADER_BASE_ADDR
mov bp, msg
mov cx, 17
mov ax,0x1301
mov bx,0x001f
mov dx,0x1800
int 0x10

in al,0x92
or al,0x02
out 0x92,al

lgdt [gdt_ptr]

mov eax, cr0
or eax, 0x1
mov cr0 ,eax
jmp dword SELECTOR_CODE:p_mode_start


[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_BASE_ADDR
mov ax,SELECTOR_VIDEO
mov gs,ax

mov byte [gs:160] ,'P'
mov byte [gs:161] ,0xa4
jmp $

其中比较疑惑的一点是jmp dword SELECTOR_CODE:p_mode_start,按道理来说执行完mov cr0 ,eax就已经打开保护模式了,此时cs=0,然后根据cs:ip取指肯定就取不到jmp dword SELECTOR_CODE:p_mode_start这条指令了啊,为什么还会执行它呢。

我的想法是还是和流水线有关系,当执行mov cr0 ,eax的时候,cpu还处于实模式,所以jmp dword SELECTOR_CODE:p_mode_start指令可以成功完成取值,而且此时处于译码,当mov cr0,eax执行完之后,jmp dword SELECTOR_CODE:p_mode_start也译码完成,但是译码成实模式的16位指令了。

在实模式下才可以使用bios中断,BIOS有一个中断可以获取内存情况,所以现在实模式下调用bios中断获取内存情况然后再转换成保护模式,内存情况是操作系统所需要的。

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
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR

GDT_BASE:
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000ffff
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000ffff
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4

GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0

SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

total_mem_bytes dd 0

gdt_ptr dw GDT_LIMIT
dd GDT_BASE

ARDS_buf times 244 db 0

ARDS_nr dw 0


loader_start:
xor ebx, ebx
mov edx, 0x534d4150
mov di,ARDS_buf

.e820_mem_get_findmax_loop:
mov eax, 0xe820
mov ecx,20
int 0x15
mov ebp, ARDS_buf
mov eax, [ebp]
add eax, [ebp+8]
cmp esi, eax
jge .e820_cmp_ebx
mov esi, eax
.e820_cmp_ebx:
inc word [ARDS_nr]
cmp ebx,0
jnz .e820_mem_get_findmax_loop

.mem_get_ok:
mov [total_mem_bytes] ,esi

.change_mode:
in al,0x92
or al,0x02
out 0x92,al

lgdt [gdt_ptr]

mov eax, cr0
or eax, 0x1
mov cr0 ,eax

jmp dword SELECTOR_CODE:p_mode_start


[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_BASE_ADDR
mov ax,SELECTOR_VIDEO
mov gs,ax

mov byte [gs:160] ,'P'
mov byte [gs:161] ,0xa4
jmp $

下面就要开启虚拟地址了,32位系统采用二级页表,采用一级页表也行,但是使用的内存会比较大,最后页表的大小=(0x100000000//0x1000)*4=4MB,并且要一次性准备好,但是如果采用二级页表就不需要提前准备好,只有需要使用的时候再分配,那为什么不采用三级页表呢,因为没必要。64位系统采用四级页表。

页表映射关系也不是很复杂,目前就映射了前1MB的地址,而且有两种关系,采用直接映射主要是为了开启映射之后能正常执行代码。

![](C:\Users\张鹏\Pictures\Camera Roll\QQ图片20230225222943.jpg)

在页目录中填充了769~1022之间的页目录项,作用相当于占位符,代表前1GB的页表是固定的,这样所有进程的前1GB的页表也是一样的,可以共享内核所有空间。

在第1023处填了页表项自己的地址是比较巧妙的,这样内核就可以通过这个页表项管理所有的页表内容和页表项的内容了。

最后的映射如下图

image-20230225223107346

认真分析页表还是能看的明白的。

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR

GDT_BASE:
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000ffff
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000ffff
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4

GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0

SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

total_mem_bytes dd 0

gdt_ptr dw GDT_LIMIT
dd GDT_BASE

ARDS_buf times 244 db 0

ARDS_nr dw 0


loader_start:
xor ebx, ebx
mov edx, 0x534d4150
mov di,ARDS_buf

.e820_mem_get_findmax_loop:
mov eax, 0xe820
mov ecx,20
int 0x15
mov ebp, ARDS_buf
mov eax, [ebp]
add eax, [ebp+8]
cmp esi, eax
jge .e820_cmp_ebx
mov esi, eax
.e820_cmp_ebx:
inc word [ARDS_nr]
cmp ebx,0
jnz .e820_mem_get_findmax_loop

.mem_get_ok:
mov [total_mem_bytes] ,esi

.change_mode:
in al,0x92
or al,0x02
out 0x92,al

lgdt [gdt_ptr]

mov eax, cr0
or eax, 0x1
mov cr0 ,eax

jmp dword SELECTOR_CODE:p_mode_start


[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_BASE_ADDR
mov ax,SELECTOR_VIDEO
mov gs,ax

call setup_page

sgdt [gdt_ptr]
add dword [gdt_ptr+2] ,0xc0000000
add esp,0xc0000000
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

mov eax, cr0
or eax, 0x80000000
mov cr0, eax

lgdt [gdt_ptr]

mov byte [gs:160], 'V'
mov byte [gs:161], 0x1f

jmp $


;--------------------创建页目录以及页表----------------------------------
setup_page:
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS+esi],0
mov byte [PAGE_DIR_TABLE_POS+esi+0x1000],0
inc esi
loop .clear_page_dir
.create_pde:
mov eax,PAGE_DIR_TABLE_POS
add eax, 0x1000
mov ebx, eax

or eax, PG_US_U|PG_RW_W|PG_P
mov [PAGE_DIR_TABLE_POS],eax
mov [PAGE_DIR_TABLE_POS+0xc00],eax

sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS+4092] ,eax

mov ecx,256
mov esi,0
mov edx, PG_US_U|PG_RW_W|PG_P
.create_pte:
mov [ebx+esi*4],edx
inc esi
add edx,0x1000
loop .create_pte
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000
or eax, PG_US_U|PG_RW_W|PG_P
mov ebx,PAGE_DIR_TABLE_POS
mov ecx,254
mov esi,769
.create_kernel_pde:
mov [ebx+esi*4],eax
inc esi
add eax,0x1000
loop .create_kernel_pde

ret