操作系统真相还原阅读笔记
学的慢,忘的快,做个记录防止发生学了后面学了忘了前面的事情。
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
|

上面是通过调用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
|

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的地址,而且有两种关系,采用直接映射主要是为了开启映射之后能正常执行代码。

在页目录中填充了769~1022之间的页目录项,作用相当于占位符,代表前1GB的页表是固定的,这样所有进程的前1GB的页表也是一样的,可以共享内核所有空间。
在第1023处填了页表项自己的地址是比较巧妙的,这样内核就可以通过这个页表项管理所有的页表内容和页表项的内容了。
最后的映射如下图

认真分析页表还是能看的明白的。
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
|