赞
踩
我有一个梦想,那就是自己写一个操作系统……
嗯……《操作系统真象还原》是本好书,我的计划就是跟着这本书从0造一个操作系统……
前几章的内容,我们完成了从BIOS到MBR,从实模式到保护模式,这一章叫做《保护模式进阶,向内核迈进》,正如笔者说的,前面的大量笔墨花在了理论,从这一章开始,我们才算开始了真正的操作系统学习之旅。
那我们就开始吧!
操作系统无时无刻不在和内存打交道,为了做好内存的管理,我们得先知道自己到底有多少物理内存才行。
Linux获取内存容量的本质是调用BIOS中断0x15实现的,它有三个子功能:
接下来作者事无巨细地介绍了三大功能的具体细节,像这种东西,我一般都是草草略过,因为我知道看一遍也记不下来……
嗯,用的时候再去查就好了……
感觉学计算机就是需要这样……
下面以功能最强大的0xE820为例,进行loader.S的修改:
%include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR jmp loader_start ; construct gdt and its inner-descriptor 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 ; save memory capacity ; gdt's pointer gdt_ptr dw GDT_LIMIT dd GDT_BASE ards_buf times 244 db 0 ; buffer_size ards_nr dw 0 ; buffer_num loader_start: xor ebx, ebx mov edx, 0x534d4150 mov di, ards_buf .e820_mem_get_loop: mov eax, 0x0000e820 mov ecx, 20 int 0x15 add di, cx inc word, [ards_nr] cmp ebx, 0 jnz .e820_mem_get_loop mov cx, [ards_nr] mov ebx, ards_buf xor edx, edx .find_max_mem_area: mov eax, [ebx] mov eax, [ebx + 8] add ebx, 20 cmp edx, eax jge .next_ards mov edx, eax .next_ards: loop .find_max_mem_area .mem_get_ok: mov [total_mem_bytes], edx ; print string mov sp, LOADER_BASE_ADDR mov bp, loadermsg mov cx, 15 mov ax, 0x1301 mov bx, 0x001f mov dx, 0x1800 int 0x10 ; ========= ready to enter protection mode ? ========= ; ========= 1. open A20 ========= ; ========= 2. load gdt ========= ; ========= 3. set pe = 1 ========= ;================ 1. open A20 =============== in al, 0x92 or al, 0000_0010b out 0x92, al ;================ 2. load GDT =============== lgdt [gdt_ptr] ; =============== 3. set pe = 1 ============= mov eax, cr0 or eax, 0x00000001 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_STACK_TOP mov ax, SELECTOR_VIDEO mov gs, ax mov byte [gs:160], 'P' jmp $
将新的loader安装到硬盘中,调试结果非常奇怪:
可以看到,显示的结果为0x1ef0000,并不是理想的0x02000000,这让我百思不得其解。。
通过检查,发现add eax [ebx + 8]写成了mov,也就是说0x1ef0000其实是内存的长度,而作者所谓的内存容量包含了内存基址+内存长度,也就是说,作者计算出来的内存容量,其实是内存的上界限……
这是为什么呢?经过一番思考,我觉得内存的分布结构应该是如下这样:
也就是说,通过e820探测到不同内存块之后,我们需要考虑整个内存的容量,也就是找到内存的上界限值,即最大内存的基址再加上最大内存本身长度值,等于原先定义好的32MB,验证成功。
;正确的代码 %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR jmp loader_start ; construct gdt and its inner-descriptor 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 59 dq 0 times 5 db 0 total_mem_bytes dd 0 ; save memory capacity ; gdt's pointer gdt_ptr dw GDT_LIMIT dd GDT_BASE ards_buf times 244 db 0 ; buffer_size ards_nr dw 0 ; buffer_num SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0 SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0 SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0 loader_start: xor ebx, ebx mov di, ards_buf .e820_mem_get_loop: mov eax, 0x0000e820 mov edx, 0x534d4150 mov ecx, 20 int 0x15 add di, cx inc word [ards_nr] cmp ebx, 0 jne .e820_mem_get_loop mov cx, [ards_nr] mov ebx, ards_buf xor edx, edx .find_max_mem_area: mov eax, [ebx] add eax, [ebx+8] add ebx, 20 cmp edx, eax ; if ebx >= eax: continue, else ebx = eax jge .next_ards mov edx, eax .next_ards: loop .find_max_mem_area jmp .mem_get_ok .mem_get_ok: mov [total_mem_bytes], edx ; ========= ready to enter protection mode ? ========= ; ========= 1. open A20 ========= ; ========= 2. load gdt ========= ; ========= 3. set pe = 1 ========= ;================ 1. open A20 =============== in al, 0x92 or al, 0000_0010b out 0x92, al ;================ 2. load GDT =============== lgdt [gdt_ptr] ; =============== 3. set pe = 1 ============= mov eax, cr0 or eax, 0x00000001 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_STACK_TOP mov ax, SELECTOR_VIDEO mov gs, ax mov byte [gs:160], 'P' jmp $
主要还是解决分段中存在的碎片等问题。
一句话概括:将地址拆成高20位和低12位,高20位通过硬件进行查表获得实际的物理地址,然后再加上低12位获得真正的物理地址。
在这一段中作者再次提及了平坦模型,所谓平坦模型,就是相对于多段模型的概念,在32位CPU中,我们可以访问的内存大小为4GB,那么对于平坦模型的概念就是整个4GB看成一个段,它的基地址就是从0开始,也就是说,一个偏移量就可以对应一个确定的地址。
因为偏移量共有12位,所以一个标准页的大小就是
2
12
2^{12}
212,也就是4KB。
二级页表主要是为了解决动态创建页表的问题。
一级页表是每个进程使用一个页表,二级页表是每个进程使用一个页目录表,每个页目录表项又对应一张页表。
接下来作者介绍了页目录项和页表项,除了物理地址之外,还有很多的控制位,存在位、访问位、读写位……这些到时候需要再回来看。
启动分页机制,就是完成以下三件事情:
1. 准备好页目录表和页表
2. 将页表地址写入专门存放页目录项的基址寄存器cr3
3. 寄存器cr0的PG位置1,也就是控制操作系统进入内存分页机制
一言概括,学习Linux的做法,0-3GB的虚拟地址空间分给用户进程,3-4GB的虚拟地址空间分给操作系统。
首先修改下boot.inc里面的配置如下:
PAGE_DIR_TABLE_POS equ 0x100000 ; 页目录表的物理地址
; ------------- page table property -------------
PG_P equ 1b ; 操作系统在处理完缺页中断之后将P为置1,被虚存管理置换进外存时置0,可以理解为有效位
PG_RW_R equ 00b ; 该内存只可读
PG_RW_W equ 10b ; 该内存可写
PG_US_S equ 000b ; 该内存不能被特权级为3的任务访问
PG_US_U equ 100b ; 该内存可以被任何特权级的任务访问
下面是分页机制的实现:
;-------------- 创建页目录及页表 --------------- setup_page: ; 先将页目录占用的空间逐字节清零 mov ecx, 4096 mov esi, 0 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 ; PAGE_DIR_TABLE是页表指针,这个宏定义在include.inc中 inc esi loop .clear_page_dir ; 创建页目录项(PDE) .create_pde: mov eax, PAGE_DIR_TABLE_POS add eax, 0x1000 ; 此时的eax对应第一个页表的位置 mov ebx, eax ; ebx是第一个页表的位置 or eax, PG_US_U | PG_RW_W | PG_P ; 这是一个用户属性的页表 mov [PAGE_DIR_TABLE_POS], eax ; 第一个目录项 mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 0xc00表示第768个页表占用的目录项 ; 该项划分出3G和1G的虚存空间大小,该项往上属于内核,该项往下属于用户进程 sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS +4092], eax ; 将最后一个目录项指向页目录表自己的地址,应该是为了循环遍历 ; 接下来创建页表项 mov ecx, 256 ; 1M低端内存 / 4K页大小 = 256 mov esi, 0 mov edx, PG_US_U | PG_RW_W | PG_P .create_pte: mov [ebx + esi * 4], edx add edx, 4096 inc esi loop .create_pte ; 循环256次建立,最开始的1MB内存的虚拟地址等于物理地址 ; 创建内核其他页表的PDE 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 ; 从769-1022 .create_kernel_pde: mov [ebx + esi * 4], eax inc esi add eax, 0x1000 loop .create_kernel_pde ret ; 这一段和前面大同小异
为什么低端1M内存,也就是我们操作系统的内核,既要放在表头的用户区,又要放在内核空间呢,我的理解是为了实现内存空间的共享。
接下来就是正式启用分页的三部曲:
; 创建页目录和页表并初始化页内存位图 call setup_page ; 刚才编写的初始化函数 ; 将描述符表地址及偏移量写入gdt_ptr备用 sgdt [gdt_ptr] ; 将gdt中显存段描述符中的段基址 + 0xc0000000 mov ebx, [gdt_ptr + 2] ; gdt的结构是前2位是偏移量,后四位是基址,现在先把基址取出来 or dword [ebx + 0x18 + 4], 0xc0000000 ; 0x18是因为显存段是第三段,每段8字节,所以加24 ; 4指的是写入段基址的最高1字节 ; 将gdt的基址加上0xc0000000使其成为内核所在的高地址 add dword [gdt_ptr + 2], 0xc0000000 add esp, 0xc0000000 ; 将栈指针同样映射到内核地址 ; 第二步,将页目录地址赋给cr3 mov eax, PAGE_DIR_TABLE_POS mov cr3, eax ; 第三步:打开cr0的pg位(第31位),开启分页机制 mov eax, cr0 or eax, 0x80000000 mov cr0, eax ; 开启分页后,用gdt新的地址重新加载 lgdt [gdt_ptr] mov byte [gs:160] 'v' jmp $
接下来就是验证环节:
可以看到gdt的基址被修改到了0xc的内核区,至于比900多了3,这是因为loader.S最开始的跳转需要三个字节所导致,然后,第三个描述符——视频段,也处在内核区了,一切如预期一样良好……
首先作者是详尽地介绍了elf,所谓elf,全称是Executable and Linkable Format,可执行链接格式,应该是和Windows中的PE(Portable Executable)是对等的,关于具体介绍,这里不再详细阐述,一言概之,elf文件是编译文件和进程的中间环节,它类似于C的风格,包含了文件头和文件体。
接下来写了一个最简单的内核C代码(以后代码主要是写C了,终于不用苦逼地写汇编了555):
int main(void) {
while(1);
return 0;
}
将它转换成二进制文件写入内核之中。
接下来loader.S完成两件事儿:加载内核并初始化。(刚说完不用写汇编就开始写汇编了)
; ------------- 加载kernel ---------------------
mov eax, KERNEL_START_SECTOR
mov ebx, KERNEL_BIN_BASE_ADDR
mov ecx, 200 ; 读入的扇区数
call rd_disk_m_32
; 置于创建页表之前
加载内核的代码如上,主要是给寄存器赋上关于内核的信息,然后进入加载函数,接下来是初始化内核:
; -------- 将kernel.bin中的segment拷贝到编译的地址 -------- kernel_init: xor eax, eax xor ebx, ebx xor ecx, ecx xor edx, edx mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; dx读取程序头大小 mov ebx, [KERNEL_BIN_BASE_ADDR + 42] ; ebx读取程序头偏移量 add ebx, KERNEL_BIN_BASE_ADDR mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; cx读取程序头的个数 .each_segment: cmp byte [ebx +0], PT_NULL je .PTNULL ; 说明该程序头未被使用 push dword [ebx + 16] ; 压入文件大小 mov eax, [ebx + 4] add eax, KERNEL_BIN_BASE_ADDR ; 压入段物理地址 push eax push dword [ebx + 8] ; 压入p_vaddr,目的地址 call mem_cpy add esp, 12 ; 清空栈 .PTNULL: add ebx, edx loop .each_segment ret ; ------------- 逐字节拷贝 mem_cpy(dst, src, size) -------------- mem_cpy: cld push ebp mov ebp, esp push ecx mov edi, [ebp + 8] mov esi, [ebp + 12] mov ecx, [ebp + 16] rep movsb ; 逐字节拷贝 ; 恢复环境 pop ecx pop ebp ret
写完这些,5.3就接近尾声了,总结起来,就是用loader引导内核完成初始化,包括内核映像的建立,内核栈位置的变化等……但是这一part没有验证部分,难免有些不安,算了,如果出问题的话再回来看吧。
特权级这一部分主要就是概念了,特权级结构呈环状,简单地说,等级数值上越小,权力越大。
一言概之,TSS是记录任务的数据结构,所谓任务,就是脱离了操作系统的进程,在没有操作系统的情况下,任务可以独立执行。TSS中记录了0,1,2三个级别的目标栈选择子和偏移量,除调用返回外,只能从低特权级转向高特权级,这就是为什么TSS无需记录3特权级栈的原因。
特权级通过CPL(Current Privilege Level),它任意时刻都存储在代码段寄存器的CS的RPL部分中。
书中的解释非常棒,门就是蹦床,如果你够到了蹦床(门的DPL),那么你的优先级就可以提高。
这一部分我就草草略过了,内容很多,全是密密麻麻的字……
时隔一个月左右,终于把第五章看完了……中间经历了期末复习,期末考试,课设和竞赛,没办法,这些事都是比自主学习的特权级更高的,明天开始进入第六章的学习,继续努力吧!
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。