目录

  • 前言
  • 5.1 获取物理内存容量
    • 5.1.1 学习Linux获取内存的方法
    • 5.1.2 实战内存容量检测
  • 5.2 内存分页
    • 为什么需要分页?
    • 一级页表
    • 二级页表
    • 如何设计一个页表
    • 分页机制的代码实现
  • 5.3 加载内核
  • 5.4 特权级
    • TSS是什么
    • 特权级究竟是什么
    • 什么是门
  • 总结

前言

我有一个梦想,那就是自己写一个操作系统……
嗯……《操作系统真象还原》是本好书,我的计划就是跟着这本书从0造一个操作系统……
前几章的内容,我们完成了从BIOS到MBR,从实模式到保护模式,这一章叫做《保护模式进阶,向内核迈进》,正如笔者说的,前面的大量笔墨花在了理论,从这一章开始,我们才算开始了真正的操作系统学习之旅。
那我们就开始吧!

5.1 获取物理内存容量

操作系统无时无刻不在和内存打交道,为了做好内存的管理,我们得先知道自己到底有多少物理内存才行。

5.1.1 学习Linux获取内存的方法

Linux获取内存容量的本质是调用BIOS中断0x15实现的,它有三个子功能:

  • EAX = 0xE820:遍历主机上全部内存(看上去是最厉害的……)
  • AX = 0xE201:分别检测低15MB和16MB~4GB的内存,最大支持4GB
  • AH = 0x88:最多检测64MB内存,实际内存超过也按照64MB返回(看上去是最弱的……)

接下来作者事无巨细地介绍了三大功能的具体细节,像这种东西,我一般都是草草略过,因为我知道看一遍也记不下来……
嗯,用的时候再去查就好了……
感觉学计算机就是需要这样……

5.1.2 实战内存容量检测

下面以功能最强大的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 0x00000000dd 0x00000000CODE_DESC: dd 0x0000FFFFdd DESC_CODE_HIGH4DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC: dd 0x80000007dd DESC_VIDEO_HIGH4GDT_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 + RPL0total_mem_bytes dd 0    ; save memory capacity; gdt's pointer
gdt_ptr dw GDT_LIMITdd GDT_BASEards_buf times 244 db 0 ; buffer_size
ards_nr dw 0 ; buffer_numloader_start:xor ebx, ebx
mov edx, 0x534d4150
mov di, ards_buf
.e820_mem_get_loop:mov eax, 0x0000e820mov ecx, 20int 0x15add di, cxinc word, [ards_nr]cmp ebx, 0jnz .e820_mem_get_loopmov cx, [ards_nr]mov ebx, ards_bufxor edx, edx
.find_max_mem_area:mov eax, [ebx]mov eax, [ebx + 8]add ebx, 20cmp edx, eaxjge .next_ardsmov edx, eax
.next_ards:loop .find_max_mem_area.mem_get_ok:mov [total_mem_bytes], edx; print stringmov sp, LOADER_BASE_ADDRmov bp, loadermsgmov cx, 15mov ax, 0x1301mov bx, 0x001fmov dx, 0x1800int 0x10; ========= ready to enter protection mode ? =========
; =========           1. open A20            =========
; =========           2. load gdt            =========
; =========           3. set pe = 1         =========;================ 1. open A20 ===============in al, 0x92or al, 0000_0010bout 0x92, al;================ 2. load GDT ===============lgdt [gdt_ptr]; =============== 3. set pe = 1 =============mov eax, cr0or eax, 0x00000001mov cr0, eaxjmp dword SELECTOR_CODE:p_mode_start[bits 32]
p_mode_start:mov ax, SELECTOR_DATAmov ds, axmov es, axmov ss, axmov esp, LOADER_STACK_TOPmov ax, SELECTOR_VIDEOmov gs, axmov 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 0x00000000dd 0x00000000CODE_DESC: dd 0x0000FFFFdd DESC_CODE_HIGH4DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC: dd 0x80000007dd DESC_VIDEO_HIGH4GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 59 dq 0
times 5 db 0total_mem_bytes dd 0    ; save memory capacity; gdt's pointer
gdt_ptr dw GDT_LIMITdd GDT_BASEards_buf times 244 db 0 ; buffer_size
ards_nr dw 0 ; buffer_numSELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0loader_start:xor ebx, ebx
mov di, ards_buf
.e820_mem_get_loop:mov eax, 0x0000e820mov edx, 0x534d4150mov ecx, 20int 0x15add di, cxinc word [ards_nr]cmp ebx, 0jne .e820_mem_get_loopmov cx, [ards_nr]mov ebx, ards_bufxor edx, edx
.find_max_mem_area:mov eax, [ebx]add eax, [ebx+8]add ebx, 20cmp edx, eax   ; if ebx >= eax: continue, else ebx = eaxjge .next_ardsmov edx, eax
.next_ards:loop .find_max_mem_areajmp .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, 0x92or al, 0000_0010bout 0x92, al;================ 2. load GDT ===============lgdt [gdt_ptr]; =============== 3. set pe = 1 =============mov eax, cr0or eax, 0x00000001mov cr0, eaxjmp dword SELECTOR_CODE:p_mode_start[bits 32]
p_mode_start:mov ax, SELECTOR_DATAmov ds, axmov es, axmov ss, axmov esp, LOADER_STACK_TOPmov ax, SELECTOR_VIDEOmov gs, axmov byte [gs:160], 'P'   jmp $

5.2 内存分页

为什么需要分页?

主要还是解决分段中存在的碎片等问题。

一级页表

一句话概括:将地址拆成高20位和低12位,高20位通过硬件进行查表获得实际的物理地址,然后再加上低12位获得真正的物理地址。

在这一段中作者再次提及了平坦模型,所谓平坦模型,就是相对于多段模型的概念,在32位CPU中,我们可以访问的内存大小为4GB,那么对于平坦模型的概念就是整个4GB看成一个段,它的基地址就是从0开始,也就是说,一个偏移量就可以对应一个确定的地址。
因为偏移量共有12位,所以一个标准页的大小就是2122^{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, 4096mov esi, 0
.clear_page_dir:mov byte [PAGE_DIR_TABLE_POS + esi], 0 ; PAGE_DIR_TABLE是页表指针,这个宏定义在include.inc中inc esiloop .clear_page_dir
; 创建页目录项(PDE)
.create_pde:mov eax, PAGE_DIR_TABLE_POSadd 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, 0x1000mov [PAGE_DIR_TABLE_POS +4092], eax  ; 将最后一个目录项指向页目录表自己的地址,应该是为了循环遍历; 接下来创建页表项mov ecx, 256    ; 1M低端内存 / 4K页大小 = 256mov esi, 0mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:mov [ebx + esi * 4], edxadd edx, 4096inc esiloop .create_pte   ; 循环256次建立,最开始的1MB内存的虚拟地址等于物理地址; 创建内核其他页表的PDEmov eax, PAGE_DIR_TABLE_POSadd eax, 0x2000or eax, PG_US_U | PG_RW_W | PG_Pmov ebx, PAGE_DIR_TABLE_POSmov ecx, 254   ; 最开始的一张表在前面已创建好,最后一张循环指向表头mov esi, 769  ; 从769-1022
.create_kernel_pde:mov [ebx + esi * 4], eaxinc esiadd eax, 0x1000loop .create_kernel_pderet    ; 这一段和前面大同小异

为什么低端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最开始的跳转需要三个字节所导致,然后,第三个描述符——视频段,也处在内核区了,一切如预期一样良好……

5.3 加载内核

首先作者是详尽地介绍了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, eaxxor ebx, ebxxor ecx, ecxxor edx, edxmov dx, [KERNEL_BIN_BASE_ADDR + 42]    ; dx读取程序头大小mov ebx, [KERNEL_BIN_BASE_ADDR + 42]    ; ebx读取程序头偏移量add ebx, KERNEL_BIN_BASE_ADDRmov cx, [KERNEL_BIN_BASE_ADDR + 44]  ; cx读取程序头的个数.each_segment:cmp byte [ebx +0], PT_NULLje .PTNULL ; 说明该程序头未被使用push dword [ebx + 16]  ; 压入文件大小mov eax, [ebx + 4]add eax, KERNEL_BIN_BASE_ADDR    ; 压入段物理地址push eaxpush dword [ebx + 8]  ; 压入p_vaddr,目的地址call mem_cpyadd esp, 12  ; 清空栈
.PTNULL:add ebx, edxloop .each_segmentret; ------------- 逐字节拷贝 mem_cpy(dst, src, size) --------------
mem_cpy:cldpush ebpmov ebp, esppush ecxmov edi, [ebp + 8]mov esi, [ebp + 12]mov ecx, [ebp + 16]rep movsb ; 逐字节拷贝; 恢复环境pop ecxpop ebpret

写完这些,5.3就接近尾声了,总结起来,就是用loader引导内核完成初始化,包括内核映像的建立,内核栈位置的变化等……但是这一part没有验证部分,难免有些不安,算了,如果出问题的话再回来看吧。

5.4 特权级

特权级这一部分主要就是概念了,特权级结构呈环状,简单地说,等级数值上越小,权力越大。

TSS是什么

一言概之,TSS是记录任务的数据结构,所谓任务,就是脱离了操作系统的进程,在没有操作系统的情况下,任务可以独立执行。TSS中记录了0,1,2三个级别的目标栈选择子和偏移量,除调用返回外,只能从低特权级转向高特权级,这就是为什么TSS无需记录3特权级栈的原因。

特权级究竟是什么

特权级通过CPL(Current Privilege Level),它任意时刻都存储在代码段寄存器的CS的RPL部分中。

什么是门

书中的解释非常棒,门就是蹦床,如果你够到了蹦床(门的DPL),那么你的优先级就可以提高。
这一部分我就草草略过了,内容很多,全是密密麻麻的字……


总结

时隔一个月左右,终于把第五章看完了……中间经历了期末复习,期末考试,课设和竞赛,没办法,这些事都是比自主学习的特权级更高的,明天开始进入第六章的学习,继续努力吧!

操作系统真象还原——第5章 从保护模式到内核相关推荐

  1. 操作系统真象还原第5章:保护模式进阶,向内核进阶

    前言 由于涉及到马上要搞实习的事情,搞得我十分的浮躁,自己也是频繁失眠,想来还是自己太过懒了,没控制住自己,自己也在这一个多月没搞好,尤其是本来想花几天时间来写一个高性能服务器,也把游双大佬的linu ...

  2. 操作系统真象还原第4章:保护模式入门

    前言 妈的还是没控制住自己,玩了几天,我自己就有一个很臭的毛病,如果玩的话就会一直玩下去,既然如此我直接不玩了,妈的我发誓绝逼不玩了 还好没出现什么BUG,也算是完成了. 关于这一章,我觉得比较重要的 ...

  3. [书]操作系统真象还原 -- 第5章 开启保护模式、开启分页机制

    mbr:加载loader,跳转 loader: 1)调用BIOS中断获取内存大小; 2)构建GDT.开启保护模式;  3)构建页目录表和页表.开启分页机制; FILE:loader.asm ; FIL ...

  4. 《操作系统真象还原》第二章

    <操作系统真象还原>第二章 编写MBR主引导记录 载入内存 过程: (1)程序被加载器(软件或硬件)加载到内存某个区域. (2)CPU的cs:ip寄存器被指向这个程序的起始地址. 从按下主 ...

  5. 《操作系统真象还原》第九章

    <操作系统真象还原>第九章 本篇对应书籍第九章的内容 本篇内容介绍了线程是什么,多线程的原理,多线程用到的核心数据结构,以及通过代码实现了内核多线程的调度 线程是什么? 执行流 过去,计算 ...

  6. 《操作系统真象还原》1-3章 学习记录

    文章目录 前言 一.开始实验前的一些基本问题解答? section的含义? vstart的含义? $ 和 $$区别? 实模式的特点? CPU如何和硬盘进行交互? CPU和IO设备交互方式? 程序载入内 ...

  7. 《操作系统真象还原》第九章 ---- 终进入线程动斧开刀 豁然开朗拨云见日 还需解决同步机制才能长舒气

    文章目录 专栏博客链接 相关查阅博客链接 本书中错误勘误 进程 线程的自我小理解 线程 进程的状态 内核级线程 & 用户级线程 初步实现内核级线程 浪费两三个小时调试的辛酸史 编写thread ...

  8. 操作系统真象还原第2章:编写MBR主引导记录

    前言 这章的内容挺少的,也很简单,如果环境没配置错的话是没啥问题的.但是这章也很精彩,把引导的过程给说了出来,我也是看了几遍把这个过程给大致看懂了. 首先计算机一开机这个时cpu会自动把cs:ip指针 ...

  9. 《操作系统真象还原》第二章 ---- 编写MBR主引导记录 初尝编写的快乐 雏形已显!

    文章目录 专栏博客链接 前引 相关术语 理清操作系统启动程序运行流程(部分) 编写MBR引导内容 编译并检验mbr.bin Linux dd 磁盘操作指令与参数 模拟操作试一试 结束语 专栏博客链接 ...

最新文章

  1. python如何输出两行_python-如何串联在两行上指定的标识符?
  2. 8086 汇编指令手册查询(转)
  3. 经典的Java面试题及回答集锦
  4. 4位无符号比较器设计
  5. CocosCreator downlevelIteration 允许迭代器进行迭代
  6. Silvaco TCAD LTPS双栅器件仿真收敛不了,有没有好的解决办法
  7. 车架号 生成 java_车辆识别码VIN校验位计算方法及实现  VIN号检验、车架号检验 java、 C++...
  8. php识别名片,名片识别接口
  9. 招投标知识分享:影响投标报价编制的8大重要因素
  10. 新浪博客(sina blog)批量删除代码
  11. android gradle 多渠道打包小结
  12. 可视化1300个故事 揭秘6种情节套路
  13. 好心替同事接电话?小心接出事情来!
  14. 安卓USB接口插网线上网,USB接口直连手机上网,免流量,游戏零延迟,网线插手机上网 直播免流量
  15. 2步开启Safari审查元素
  16. 聚多巴胺修饰介孔二氧化硅SBA-15|氨基功能化介孔氧化硅SBA-15|乙烯醋酸乙烯酯橡胶/介孔二氧化硅SBA-15复合物 CMS
  17. 美容院管理系统;美容管理软件
  18. 基于人工智能的智慧校园助手(springboot+springcloud+redis+vue+vant ui+element ui+mysql+Elasticsearch+RabbitMQ项目)
  19. Ubuntu使用超级用户权限
  20. Android性能优化系列:Bitmap

热门文章

  1. Vue中的v-slot如何使用
  2. 前端逐帧动画性能探究和比较
  3. 嵌入式开发环境的搭建之威睿工作站
  4. 【转载】儒林外史人物——严贡生和严监生(一)
  5. Go语言中定时任务库Cron使用详解
  6. 单片机p0口接8个LED c51语言,51 单片机:在 P0 口接上 8 个 LED,实现每次亮两个灯的流水灯...
  7. LEADTOOLS V20,医学影像浏览器中3D体积渲染控件
  8. 原创:oracle PL/SQL编程基础 下lt;十二gt;
  9. ps把下一幅画透到上一幅去_一千字值得一幅画
  10. 干涉仪测向matlab程序,一种相关干涉仪测向算法的快速实现