1.第一节:最小的操作系统(引导扇区)
1.1环境准备
1.vmware虚拟机:
建立一个新的空白的虚拟机,命名为Tinix.添加软盘,使用自制的映像文件
虚拟机设置:
2.nasm编译器
3.notepad++
1.2制作映像文件
1.boot.asm文件
org 07c00h mov ax,cs mov ds,ax mov es,ax call DispStr jmp $ DispStr: mov ax,BootMessage mov bp,ax mov cx,16 mov ax,01301h mov bx,000ch mov dl,0 int 10h ret BootMessage: db "Hello, OS world!" times 510-($-$$) db 0 dw 0xaa55
2.使用nasm编译
cmd.exe中使用命令:
nasm boot.asm -o boot.bin
1.3 启动系统
将boot.bin文件放如软盘中,启动Tinix系统:
至此完成自制操作系统的第一步(boot sector)引导扇区.
第二小节:保护模式
2.1 实模式到保护模式 GDT表
参考:https://blog.csdn.net/yihaolovem/article/details/23483927
备注:寄存器参考:https://www.cnblogs.com/joey-hua/p/5347257.html
https://blog.csdn.net/jnu_simba/article/details/11712675
段页机制:https://www.cnblogs.com/chenwb89/p/operating_system_003.html
https://blog.csdn.net/suppercoder/article/details/9422093
https://blog.csdn.net/fallingu/article/details/75221276
https://blog.csdn.net/bian1029/article/details/49124593(GDT表第一项为0)
; ========================================== ; pmtest1.asm ; 编译方法:nasm pmtest1.asm -o pmtest1.com ; ========================================== %include "pm.inc" ; 常量, 宏, 以及一些说明 org 0100h jmp LABEL_BEGIN [SECTION .gdt] ; GDT ; 段基址, 段界限 , 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段, 32 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址 ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 选择子 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ; END of [SECTION .gdt] [SECTION .s16] [BITS 16] LABEL_BEGIN: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0100h ; 初始化 32 位代码段描述符 xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32 mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah ; 为加载 GDTR 作准备 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt 基地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加载 GDTR lgdt [GdtPtr] ; 关中断 cli ; 打开地址线A20 in al, 92h or al, 00000010b out 92h, al ; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处 ; END of [SECTION .s16] [SECTION .s32]; 32 位代码段. 由实模式跳入. [BITS 32] LABEL_SEG_CODE32: mov ax, SelectorVideo mov gs, ax ; 视频段选择子(目的) mov edi, (80 * 10 + 0) * 2 ; 屏幕第 10 行, 第 0 列。 mov ah, 0Ch ; 0000: 黑底 1100: 红字 mov al, 'P' mov [gs:edi], ax ; 到此停止 jmp $ SegCode32Len equ $ - LABEL_SEG_CODE32 ; END of [SECTION .s32]
1、[SECTION .XXX]为何物?
SECTION和SEGMENT的作用相类似,就是代表“段”的意思。从整个程序来看,该程序分为3个模块,分别是[SECTION .gdt]、[SECITON .s16]、[SECTION .s32]三部分。我们很容易就可以看出,其中的[SECTION .gdt]应该是数据段,其他的两个是代码段。通过[SECTION .XXX]将程序分成不同模块,完成不同的功能,使得程序看起来清晰明了。
2. 段描述符宏定义和初始化段描述符
描述符宏定义:
; 宏 ------------------------------------------------------------------------------------------------------ ; ; 描述符 ; usage: Descriptor Base, Limit, Attr 定义三个变量,段基址,段界限,段属性 ; Base: dd 4个字节 ; Limit: dd (low 20 bits available) 4个字节 ; Attr: dw (lower 4 bits of higher byte are always 0) 2个字节 %macro Descriptor 3 ;%macro是宏定义,Descriptor是宏名,3表示该宏有三个参数 dw %2 & 0FFFFh ; 段界限1,段界限值0-15位 注:汇编语言中数据不能以字母开头 dw %1 & 0FFFFh ; 段基址1,段基地址0-15 db (%1 >> 16) & 0FFh ; 段基址2,段基地址16-23位 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2 db (%1 >> 24) & 0FFh ; 段基址3,基地址24-31位 %endmacro ; 共 8 字节
Base是%1,Limit是%2,Attr是%3。
段描述符结构图:
P,present位,1表示所描述的段存在(有效),为0表示所描述的段无效,使用该描述符会引起异常
DPL,Descriptor privilege,描述符特权级别,说明所描述段的特权级别
S,描述符类型位,1说明当前描述符为存储段描述符,0为系统描述符或门描述符.
TYPE:
位0:A(accessed)位,表明描述符是否已被访问;把选择子装入段寄存器时,该位被标记为1
位3:E(EXECUTABLE?)位,0说明所描述段为数据段;1为可执行段(代码段)
当为数据段时,
位1为W位,说明该数据段是否可写(0只读,1可写)
位2为ED位,说明该段的扩展方向(0向高位扩展,1向低位扩展)
当为可执行段是,
位1为R位,说明该执行段是否可读(0只执行,1可读)
位2为C位,0说明该段不是一致码段(普通代码段),1为一致码段
G为粒度位,0说明LIMIT粒度为字节,1为4K字节.
D位:
1.在可执行段中,D为1,表示使用32位地址,32/8位操作数;为0表示使用16位地址,16/8位操作数
2.在由SS寻址的段描述符(堆栈段?)中,D为1表示隐含操作(如PUSH/POP)使用ESP为堆栈指针,
为0使用SP(隐含操作:未明确定义段属性类型USE16/USE32?66H,67H?)
3.在向低扩展的存储段中,D为1,表示段的上限为4G;为0上限为64K
参考:https://images2015.cnblogs.com/blog/790694/201606/790694-20160619001102057-493760916.png
GDT的作用是用来提供段式存储机制,这种机制是段寄存器和GDT中的描述符共同提供的。每个描述符在GDT中占8字节,也就是 2 个双字,或者说是 64 位。图中,下面是低32位,上面是高32位。
将Limit低16位赋值给描述符的BYTE0和BYTE1
将Base低16位赋值给描述符的BYTE2和BYTE3
将Base右移16位后的低8位(也就是原Base的第16—23位)赋值给描述符的BYTE4
将Limit右移8位之后的第8—11位和Attr的0—7和12—15位,组合起来存储到描述符的BYTE5和BYTE6
将Base右移24位后的低8位(也就是原Base的24—32位)赋值给描述符的BYTE7
初始化段描述符:
; 初始化 32 位代码段描述符 xor eax, eax mov ax, cs shl eax, 4 ;左移四位 = cs*16 add eax, LABEL_SEG_CODE32 ;有效地址 = 段值*16 + offset mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah
为什么要初始化?你会发现这里只是修改了段描述符基地址,即LABEL_DESC_CODE32的BYTE2,BYTE4,BYTE7。是不是突然恍然大悟?因为在我们初始化该LABEL_DESC_CODE32描述符时,将其基地址初始化为0,所以我们要修改描述符的基地址为其实际的地址。这也是在前面介绍段描述符的时候,我提醒大家需要注意的地方,即描述符的基地址所占有的字节是BYTE2,BYTE4,BYTE7(共32bit)。
详解:
mov ax,cs
shl eax,4
将cs的值(16bit 实模式下即为当前代码段的基地址)
左移4bit即得到当前代码段的物理基地址
add eax,LABEL_SEG_CODE32
此时eax即为实模式下LABEL_SEG_CODE32的物理地址
然后再用此地址分为三部分去初始化LABEL_DESC_CODE32
3.加载GDTR
; 为加载 GDTR 作准备 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt 基地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加载 GDTR lgdt [GdtPtr]
这个很好理解,我们就是对GdtPtr进行赋值,主要是初始化GDT的基地址。也就是将GDT的初始地址,赋值给GdtPtr的BYTE2,BYTE3,BYTE4,BYTE5。使GdtPtr的数据结构刚好符合GDTR,然后执行lgdt [GdtPtr],加载全局描述符表寄存器。将GDT的基地址和界限赋值给GDTR。
2.2 LDT 局部描述符表
LDT和GDT本质上都是段描述符表,具有相同的结构,区别仅在于全局和局部的不同。
参考:https://blog.csdn.net/wrx1721267632/article/details/52056910
- LDT在系统中可以存在多个
- LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。
- 每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
LDTR寄存器:
至此,我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图:
段选择子:
引用GDT和LDT中的段描述符所描述的段,是通过一个16-bit的数据结构来实现的,这个数据结构叫做Segment Selector——段选择子。它的高13位作为被引用的段描述符在GDT/LDT中的下标索引,bit 2用来指定被引用段描述符被放在GDT中还是到LDT中,bit 0和bit 1是RPL——请求特权等级,被用来做保护目的。如图所示:
2.3 特权级
在IA32的分段机制中,特权级总共有四个特权级别,从高到低分别对应0,1,2,3。数字越小表示特权越大。
作用:防止低特权级应用访问高特权级数据,代码。
特权级检验主要通过CPL,DPL,RPL来实现。
1.CPL
当前执行程序或任务的特权级。
被存储在CS的第0位和SS的第一位上。
通常表示代码段的特权级。
2.DPL
表示段或门的特权级。
存储在段描述符或门描述符的DPL字段中。
3.RPL
段选择子的第0位和第1位。
系统调用时使用RPL作为调用者的特权级。
补充:
参考:https://www.cnblogs.com/LittleHann/p/3850655.html
门描述符:
处理器对程序的执行主要时顺序和跳转两种方式,跳转也就是程序控制的转移,可以通过指令jmp、call、ret、sysenter、
sysexit、int n、iret引起,也可以由中断和异常机制引起。
在I386CPU中,除了"段描述符"(描述某种内存段)之外还有一种描述符叫做"门描述符"(描述控制转移的入口点,也就是异常控制中断的入口点),通过这种门可以实现特权级的转变和任务的切换。门描述符主要由以下几部分组成:
1. 选择子
2. 偏移地址
3. DPL
分为4种类型:
- 调用门
- 中断门
- 陷阱门
- 任务门
1. 调用门描述符 调用门一般用在特权级的切换,存在于GDT中或者LDT中。调用门的选择子指向代码段描述符,偏移地址对应代码段中的偏移量。当jump和call指令的操作数是调用门的时候,就会跳转到对应的代码处,并发生特权级的变化,也就会发生 堆栈的切换 2. 任务门描述符 任务门一般用在任务的切换,可以存放在GDT、LDT或IDT中。任务门的选择子指向GDT中的TSS选择符,偏移地址没有意义。当jmp和Call指令的操作数是任务门的时候,就会发生任务的切换。 3. 中断门描述符
4. 陷阱门描述符 中断门描述符、陷阱门描述符用来对中断服务例程进行寻址,从原理上来理解,中断例程的寻址本质上也是内存的寻址
保护模式初步总结:
- 描述符中段基址、段界限、段属性对段的一种保护
- 门描述符中特权级之间的变换
- 涉及到特权级的每一步中,处理器都会对CPL、DPL、RPL等内容进行比较。
关于段描述符和门描述符参考:https://blog.csdn.net/barech/article/details/4401417
可以理解为,段描述符是对存储访问的管理控制,门描述符是对调用,特权级转变的管理控制。2019-04-25
保护模式总结更新:
- 在GDT、LDT、IDT中,每一个描述符都有自己的界限和属性 -- 对描述符所描述对象的一种限定和保护
- 分页机制中的PDE和PTE都含有R/W和U/S位 -- 页级保护
- 页式存储使应用程序使用的是线性地址空间 -- 物理内存被保护起来
- 中断不再像实模式下一样使用 -- 特权检验,中断调用被保护
- I/O指令不再随便使用 -- 端口被保护起来
- 在程序运行过程中,如遇到不同特权级间的访问,会对CPL、RPL、DPL、IOPL等内容进行检验,同时进行堆栈切换 -- 对不同层级的程序进行了保护。