第04章 保护模式入门

1 实模式

1.1 实模式缺点

保护模式强调的是保护,是在Intel 80286中首次出现.

实模式的特点:

  1. 实模式下,操作系统和用户程序属于同一特权级.
  2. 用户程序所使用的地址都指向真实的物理地址,也就是说逻辑地址等于物理地址
  3. 用户程序可以使用任意段基址,修改内存中任意数据.
  4. 访问超过64KB数据需要切换段基址
  5. 一次只能运行一个程序
  6. 共20条地址线,最大可寻址的内存为1MB

为了克服这种不安全的内存管理.处理器厂商,开发出保护模式.物理内存不能直接被程序访问,程序内部的地址需要被转化位物理地址再去访问.这个过程对程序透明.地址转换需要由处理器和操作系统共同完成.处理器提供地址转换部件,操作系统提供转化过程中需要的页表.

1.2 实模式与保护模式下寄存器

32位cpu具有保护模式和实模式两种运行模式,可以兼容实模式下的程序,兼容实模式值得是能够正确处理实模下的程序.起寄存器还是32位.

2 保护模式

2.1 寄存器

32位cpu中:保护模式下通用寄存器都扩展位32位,其中低16位,为了兼容,abcd四个寄存器仍然可以再拆分使用,例如ax拆分为alah.但是高16位,不能拆分.

段寄存器仍然是16位.

2.2 段基址+偏移地址

保护模式下,偏移地址和实模式下一样.为了增加对内存的安全管理.对段基址进行了改造,段基址不在对应物理地址(实模式下的段基址其实是对应的物理地址的一段的首地址),而是作为一个选择子(其实就是一个下标),在全局描述符表GDT中索引出一个表项,该表项中由选择子对应的段基址,还有该段的长度,以及读写属性.

全局描述符表和段选择子,存在两个问题:

  1. 全局描述符表实在内存中的,访问内存对cpu来说是一个比较慢的动作,效率不高
  2. 全局描述符表的表象格式奇葩,段基址分成三块

因此,80286为了提高效率,对段寄存器使用了缓存技术,将短信息用一个寄存器缓存,段描述符缓冲寄存器,对程序员不可见,也不能够修改.cpu每次将全局段描述符整理以后,存入段描述符缓冲寄存器中,以后每次访问相同的段,就直接读取对应的段描述符缓冲寄存器.在实模式下,也用到了该缓存寄存器.只要向ds寄存器中赋值,不管是否与之前一样还是不一样,段描述符缓冲寄存器都会被刷新.

80268 cpu是一个16位cpu,通用寄存器是16位的,但是地址总线却是24位.它能够访问全部的16\(MB\)的原因是:段描述符中,段基址是用24位.因此可以访问全部的\(16MB\).

2.3 保护模式寻址

实模式中,内存寻址中的基址寻址,变址寻址,基址变址寻址中基址寄存器只能使用bx,bp,变址寄存器只能使用si,di.

在保护模式下,基址寄存器,变址寄存器都可以是所有的32位通用寄存器(变址寄存器不能使esp)

2.4 实模式与保护模式切换

cpu运行模式由实模式和保护模式两种.并且在实模式的时候,可以使用32位的寄存器的全部32位.

这两个模式下,相同的指令对应的机器码不同,因为保护模式是32位的,其地址是32位的.

当从实模式切换到保护模式的时候,需要显示的告诉编译器:[bits 16]或是[bits 32]

表示,从该标签开始到下一个标签,或是结尾,编译为对应位的机器码

从保护模式进入实模式需要三个步骤:

  1. 打开A20
  2. 加载gdt
  3. cr0pe位设为1

2.5 指令区别

3 全局描述符表

所有用到的内存,都需要在全局段描述符表中注册,当使用到了局部段描述表的时候,其所占的内存,也需要在全局段描述符表中,占一个表项.全局描述符表中的表项(可以理解为是一个数组,其中的每个元素)是段描述符,是一个8字节的数据结构.

cpu有一个专门的寄存器GDTR,来保存全局描述符表在内存中的地址,以及全局段描述符表的界限.界限的意思是:全局描述符表的地址加上界限是全局描述符表的最后一个字节.

专用的指令位:lgdt 48位数据

加载局部段描述符表的指令位:lldt 48位数据

由于GDT长度位16位,因此最大\(65536\)字节,每个描述符8字节,因此最多\(8192\)个段描述符.

32位cpu,保护模式下地址总线宽度是32位,段基址需要32位来表示.(段基址可以从任意字节开始,因此需要完整的32位),

段描述符中有一个段界限(全局段描述符表地址加上该值,是全局段描述符所在的区域.),数值上是一个20位的无符号数,其单位可以是字节,也可以是\(4KB\),当是\(4KB\)的时候,那么一个段界限就是\(4GB\),就可以访问完整的32位的内存.

3.1 段描述符格式

其格式为:

  1. 0~15:段界限15~0:拆分的原因:历史遗留原因,80286是第一个intel有保护模式的cpu,是一个24位的.

  2. 16~31:基址15~0

  3. 32~39:段基址23~16

  4. 40~43:TYPE,段描述符的类型:

S为0时,系统段:

S为1时,数据段:第一位是否是代码段X.

  1. 当是代码段的时候:从高到底三位依次位,是否可读R,是否是一致性代码段C
  2. 当不是代码段,是数据段的时候,依次位:是否向上扩展E,是否可写W.(当是栈的时候,是向下扩展的)

一致性代码段的意思:后面解释

最后一位为A,已访问,当cpu访问过后,该位被设置为1

  1. 44:S,描述符是系统段0,还是数据段1.凡是硬件运行需要用到的都称之为系统,凡是软件(包括操作系统)运行需要的都是数据.因此,绝大多数都是数据段.属于系统段的有:门结构,他是硬件运行的东西,TSS段,局部段描述符表(需要现在全局段描述符中注册),

  2. 45~46:DPL,特权级,00特权级最大,11最小

  3. 47:P,是否存在与内存中,P字段由cpu检查,如果为0,cpu抛出异常,转到相应的异常处理程序,该异常处理程序是程序员来写的,异常处理程序要将P设位1.在当内存不足的时候,全局描述符表所在内存被交换到硬盘的时候,P被设为0.

  4. 48~51:段界限23~16

  5. 52:AVL,

  6. 53:L,是否是64位代码段,0为32位代码段

  7. 54:D/B,有效地址以及操作数的位数.当为代码段的时候:D为1表示是32位.当位数据段的时候,B为0表示32位

  8. 55:G:是段界限的单位:0的时候表示单位是字节,1的时候表示\(4KB\),因此当1的时候,一个段就可以完整的方位\(4GB\),

  9. 56~63:段基址31~24

注意:段描述符的特权级使用DPL来表示.

3.2 段选择子

结构:

  1. 0~1:RPL
  2. 2:TI ,为1是表示在局部段描述符表中索引,当为0时,表示在全局段描述符表中索引
  3. 3~15:描述符索引

选择子索引部分是13位,因此也是8192个段.

3.3 局部段描述符表

按照cpu厂商的建议,一个任务对应一个LDT.

4 进入保护模式

  1. 加载GDT
  2. 打开A20地址线
  3. 打开保护模式

这三步都做完才会进入保护模式,而且,步骤任意

4.1 加载GDT

首先是构造GDT表项,这里需要构造4个:

  1. 第0个表项:全0,硬件要求的
  2. 第一个表项:code段的
  3. 第二个表项:data段的
  4. 第三个表项:专门指向显卡0xb8000

code段和,data段使用平坦模式,因此段基址(GDT表项中的)是0,而段界限设为最大,单位为\(4KB\)

首先按照16位为一组,定义出每个位.最后采用相加和移位的形式构造出完整的64位

; ------------------------------------构造GDT需要的数据 ----------------------------------
; 16位为一组,最后通过位操作拼接.
GDT_48_G_4K  equ 00000000_10000000b
GDT_48_D_32  equ 00000000_01000000b
GDT_48_L_32  equ 00000000_00000000b
GDT_48_AVL   equ 00000000_00000000b
GDT_48_LEN_H equ 00000000_00001111b

GDT_32_P         equ 10000000_00000000b
GDT_32_DPL_0     equ 00000000_00000000b
GDT_32_DPL_3     equ 01100000_00000000b
GDT_32_S_SYS     equ 00000000_00000000b
GDT_32_S_USER    equ 00010000_00000000b
GDT_32_TYPE_CODE equ 00001000_00000000b
GDT_32_TYPE_DATA equ 00000010_00000000b 
;  -----------------------------------构造GDT需要的数据 ----------------------------------

然后拼接高32位:

注意,汇编里面位操作<<优先级是小于+的,所以尽可能的多加括号吧.

;  -----------------------------------三个段描述符标表项 ----------------------------------
GDT_CODE_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \
             (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_CODE+00000000b)

GDT_DATA_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \
             (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00000000b)
GDT_VGA_H32  equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+00000000_00000000b)<<16)+ \
             (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00001011b)

这样,就构造出了,高32位.指向显卡的段.

然后拼接上低32位:

GDT_BASE equ 00000000b<<(24+32)
GDT_CODE equ (GDT_CODE_H32<<32)+0x0000FFFF
GDT_DATA equ (GDT_DATA_H32<<32)+0x0000FFFF
GDT_VGA  equ (GDT_VGA_H32<<32 )+0x80000007

第0个全为0,是系统要求的,第1个和第2个在平坦模式下,因此段基址为0,段长度为1.

显卡段:显卡内存映射在:1011_1000000000000000第16位,应该在GDT表项的低32位里.所以会有这样的形式.

显卡段的段长度:(0xbffff / 0xb8000) /4K = 0x07,所以其低32位为:10000000000000000000000000000111,也就是0x80000007

对应的选择子为:

;  -----------------------------------构造选择子需要的数据 ----------------------------------
SELECT_RPL_0 equ 00b 
SELECT_RPL_3 equ 11b 
SELECT_TI_GDT equ 000b 
SELECT_TI_LDT equ 100b 
;  -----------------------------------构造选择子需要的数据 ----------------------------------

4.2 打开A20地址线

实模式下寄存器都是16位的,地址总线是20位的,当超出\(1MB\)时,自动回绕会0地址,继续从0地址开始映射.超出的部分内存成为高端内存区HMA

in al,0x92
or al,00000010b
out 0x92,al

4.3 打开保护模式

控制寄存器crx系列,是cpu的窗口,用来展示cpu的内部状态,也可以控制cpu的运行机制.

cr0寄存器的第0位PE位,是把哦哦胡模式的开关.打开的方式是.

mov eax,cr0
or eax,0x00000001
mov cr0,eax

5 代码

目录结构:

└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04
   ├── boot.inc
   ├── loader.asm
    ├── mbr.asm
   └── start.sh

思路:在之前的基础上:

  1. boot.inc定义了一些构造gdt表项需要的一些数据,是宏
  2. loader.asm中,构造4个GDT表项(物理上连续),然后构造GDTR寄存器需要的48位数值.并加载到GDTR中
  3. 开启保护模式
  4. 在保护模式下打印字符

5.1 boot.inc

; -------------------------------------loader.bin -------------------------------------
; 将要加载在内存的位置,和在虚拟磁盘的扇区位置
LOADER_IN_MEM equ 0x900 
LOADER_IN_DISK equ 2

; loader 执行的栈基址
LOADER_STACK_TOP equ LOADER_IN_MEM
; -------------------------------------loader.bin -------------------------------------

; ------------------------------------构造GDT需要的数据 ----------------------------------
; 16位为一组,最后通过位操作拼接.
GDT_48_G_4K  equ 00000000_10000000b
GDT_48_D_32  equ 00000000_01000000b
GDT_48_L_32  equ 00000000_00000000b
GDT_48_AVL   equ 00000000_00000000b
GDT_48_LEN_H equ 00000000_00001111b

GDT_32_P         equ 10000000_00000000b
GDT_32_DPL_0     equ 00000000_00000000b
GDT_32_DPL_3     equ 01100000_00000000b
GDT_32_S_SYS     equ 00000000_00000000b
GDT_32_S_USER    equ 00010000_00000000b
GDT_32_TYPE_CODE equ 00001000_00000000b
GDT_32_TYPE_DATA equ 00000010_00000000b 
;  -----------------------------------构造GDT需要的数据 ----------------------------------

;  -----------------------------------三个段描述符标表项 ----------------------------------
GDT_CODE_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \
             (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_CODE+00000000b)

GDT_DATA_H32 equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+GDT_48_LEN_H)<<16)+ \
             (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00000000b)
GDT_VGA_H32  equ (((00000000b<<8)+GDT_48_G_4K+GDT_48_D_32+GDT_48_L_32+GDT_48_AVL+00000000_00000000b)<<16)+ \
             (GDT_32_P+GDT_32_DPL_0+GDT_32_S_USER+GDT_32_TYPE_DATA+00001011b)

GDT_BASE equ 00000000b<<(24+32)
GDT_CODE equ (GDT_CODE_H32<<32)+0x0000FFFF
GDT_DATA equ (GDT_DATA_H32<<32)+0x0000FFFF
GDT_VGA  equ (GDT_VGA_H32<<32 )+0x80000007

;  -----------------------------------三个段描述符标表项 ----------------------------------

;  -----------------------------------构造选择子需要的数据 ----------------------------------
SELECT_RPL_0 equ 00b 
SELECT_RPL_3 equ 11b 
SELECT_TI_GDT equ 000b 
SELECT_TI_LDT equ 100b 
;  -----------------------------------构造选择子需要的数据 ----------------------------------

新加了很多代码.

其中LOADER_STACK_TOP equ LOADER_IN_MEM这句的含义:程序在执行的时候就需要用到栈,我们将切换到保护模式后的栈顶设置为loader在内存中的位置,也就是说:loader被加载到0x900以上的内存中,而0x900一下的内存就被用作是程序的栈.因此在loader.asm中为ss寄存器赋的值也是data段

5.2 loader.asm

改动比较大,因为加的东西多.

%include "boot.inc"

SECTION loader vstart=LOADER_IN_MEM
; 上来就跳转
    jmp 0:loader_start
    
    gdt_base: dq GDT_BASE 
    gdt_code: dq GDT_CODE 
    gdt_data: dq GDT_DATA 
    gdt_vga:  dq GDT_VGA

    gdt_size equ $-gdt_base

; 这里预留出 60 个段描述符的位置
    times 60 dq 0

; 界限,也就是全局段描述符表在内存中的地址位:gdt_base + 界限
; 因此界限=长度-1
    gdt_ptr dw (gdt_size-1)
            dd gdt_base 

;构建选择子
    select_code equ (0x1<<3)+SELECT_TI_GDT+SELECT_RPL_0
    select_data equ (0x2<<3)+SELECT_TI_GDT+SELECT_RPL_0
    select_vag  equ (0x3<<3)+SELECT_TI_GDT+SELECT_RPL_0
    
loader_start:
    mov ax,0xb800
    mov gs,ax

    mov byte [gs:1120],'l'
    mov byte [gs:1121],00000111b
    mov byte [gs:1122],'o'
    mov byte [gs:1123],00000111b
    mov byte [gs:1124],'a'
    mov byte [gs:1125],00000111b
    mov byte [gs:1126],'d'
    mov byte [gs:1127],00000111b
    mov byte [gs:1128],'e'
    mov byte [gs:1129],00000111b
    mov byte [gs:1130],'r'
    mov byte [gs:1131],00000111b
    mov byte [gs:1132],'!'
    mov byte [gs:1133],00000111b

; 开启A20
    in al,0x92
    or al,000000010b 
    out 0x92,al
; 开启保护模式
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax
; 加载GDT
    lgdt [gdt_ptr]

; 流水线的原因,要强制刷新一次流水线
; 这里要用远跳转,因为进入了保护模式了,cs寄存器中存的不在是段基址,而是选择子
    jmp select_code:p_mode

; 主动告诉编译器下面的代码按照32位机器码编译
[bits 32]
p_mode:
; 注意这里ss段寄存器,也被赋值为 select_data
    mov ax,select_data
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov esp,LOADER_STACK_TOP

    mov ax,select_vag
    mov gs,ax
    mov byte [gs:1440],'p'

    jmp $

jmp原因,后面讲.

5.3 mbr.asm

改动较小,首先,因为loader.asm中有64个段描述符,因此\(64\times 8=512\)字节,超出了一个扇区的字节数(512),因此,在mbr.asm中,我们要将读取的磁盘数据,扩充为4个扇区,也就是\(4\times 512\)字节

%include "boot.inc"

; mbr.asm 主引导程序 
SECTION MBR vstart=0x7c00

; ---------- 初始化各个寄存器 ----------
    ; 使用cs寄存器的值去初始化其他的段寄存器.
    mov ax,cs
    mov ds,ax
    mov ss,ax
    mov fs,ax
    
    ; 设置栈指针
    mov sp,0x7c00
    
; ---------- 使用中断清除屏幕----------
; 清除屏幕
; INT 0x10 功能号0x06,AH中存功能号,AL中存上卷行数,BH中存上卷行属性
; ah:子功能号,al:0表示0x06子功能号,向上卷屏幕的行数,0表示全部
; bh:空白区域缺省属性
; cx,dx:上卷时候认为屏幕的大小,也就是只上卷cx,dx标识的矩形区域内的字符.
    mov ax,0x600
    mov bx,0x0700
    mov cx,0x0
    mov dx,0x184f

    int 0x10

; 设置光标位置
; INT 0x10 子功能号0x02,AH中存子功能号
; bh:显示页码,dh:y,dl:x
    
    mov ax,0x02
    mov dx,0x0 

    int 0x10

; ---------- 使用中断在屏幕上打印字符串 ----------
; 显示字符串
; int 0x10 子功能号0x13 
; es:bp:字符串起始地址,bh:页码,cx:字符串长度,al输出模式

    mov ax,msg_start
    mov bp,ax
    
    mov cx,msg_end-msg_start
    mov bx,0x0002
    mov ax,0x1301
    int 0x10

; ---------- 直接操作显卡内存,打印字符 ----------
; 直接读写显卡映射内存
    mov ax,0xb800
    mov gs,ax

    mov byte [gs:320],' '
    mov byte [gs:321],00001111b
    mov byte [gs:322],'d'
    mov byte [gs:323],00001111b
    mov byte [gs:324],'i'
    mov byte [gs:325],00001111b
    mov byte [gs:326],'r'
    mov byte [gs:327],00001111b
    mov byte [gs:328],'e'
    mov byte [gs:329],00001111b
    mov byte [gs:330],'c'
    mov byte [gs:331],00001111b
    mov byte [gs:332],'t'
    mov byte [gs:333],00001111b

; ---------- 加载 loader ----------
; 设置调用rd_disk_m_16时候的3个参数.
; eax 是要读取磁盘的LBA地址
; bx 是读取出来的数据,加载到内存的位置
; cx 是要读取的扇区数.
    mov eax,LOADER_IN_DISK
    mov bx,LOADER_IN_MEM
    mov cx,4

    mov byte [gs:640],'r'
    mov byte [gs:641],00001111b
    mov byte [gs:642],'e'
    mov byte [gs:643],00001111b
    mov byte [gs:644],'a'
    mov byte [gs:645],00001111b
    mov byte [gs:646],'d'
    mov byte [gs:647],00001111b
    mov byte [gs:648],' '
    mov byte [gs:649],00001111b
    mov byte [gs:650],'d'
    mov byte [gs:651],00001111b
    mov byte [gs:652],'i'
    mov byte [gs:653],00001111b
    mov byte [gs:654],'s'
    mov byte [gs:655],00001111b
    mov byte [gs:656],'k'
    mov byte [gs:657],00001111b

; 开始读取
    call rd_disk_m_16

    mov byte [gs:800],'j'
    mov byte [gs:801],00001111b
    mov byte [gs:802],'m'
    mov byte [gs:803],00001111b
    mov byte [gs:804],'p'
    mov byte [gs:805],00001111b

; 因为读取的是 内核加载器loader ,因此读取完成以后,直接跳转过去执行
    jmp LOADER_IN_MEM

; ---------- 从磁盘中能够读取数据的函数 ----------
; 功能:读取硬盘n个扇区
; 参数:
; eax:开始读取的磁盘扇区
; cx:读取的扇区个数
; bx:数据送到内存中的起始位置

rd_disk_m_16:
; 这里要保存eax 的原因在与,下面section count 寄存器需要一个8位的寄存器
; 只有acbd这四个寄存器能够拆分为高低8位来使用,而dx作为寄存器号,被占用了
; 因此需要个abc三个寄存器中一个来用,这里选择了 ax
    mov esi,eax
    mov di,cx

; 0x1f2 寄存器:sector count ,读写的时候都表示要读写的扇区数目
; 该寄存器是8位的,因此送入的数据位 cl
    mov dx,0x1f2
    mov al,cl 
    out dx,al 

; 恢复eax
    mov eax,esi 

; eax中存放的是要读取的扇区开始标号,是一个32位的值,因此 al 是低8位
; 0x1f3 存放0~7位的LBA地址,该寄存器是一个8位的
    mov dx,0x1f3
    out dx,al 

; 下面的 0x1f4 和5 分别是8~15,16~23位LBA地址,这俩寄存器都是8位的
; 因此是用shr,将eax右移8位,然后每次都用al取eax中的低8位
    mov cl,8 
    shr eax,cl 
    mov dx,0x1f4 
    out dx,al 

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

; 0x1f6 寄存器低4位存放 24~27位LBA地址,
; 0x1f6 寄存器是一个杂项,其第六位,1标识LBA地址模式,0标识CHS模式
; 上面使用的是LBA地址,因此第六位位1
    shr eax,cl 
    and al,0x0f 
    or al,0xe0    
    mov dx,0x1f6 
    out dx,al 
    
; 0x1f7 寄存器,读取该寄存器的时候,其中的数据是磁盘的状态
; 写到该寄存器的时候,写入的僵尸要执行的命令,写入以后,直接开始执行命令
; 因此需要在写该寄存器的时候,将所有参数设置号
; 0x20 表示读扇区,0x30写扇区
    mov dx,0x1f7 
    mov al,0x20
    out dx,al 

    .not_ready:
; 读 0x1f7 判断数据是否就绪,没就绪就循环等待.
        nop
        in al,dx
        and al,0x88 
        cmp al,0x08 
        jnz .not_ready

; 到这一步表示数据就绪,设置各项数据,开始读取
; 一个扇区512字节,每次读2字节,因此读一个扇区需要256次从寄存器中读取数据
; di 中是最开始的cx也就是要读取的扇区数
; mul dx 是ax=ax * dx ,因此最终ax 中是要读取的次数
    mov ax,di
    mov dx,256 
    mul dx  
    mov cx,ax 

; 0x1f0 寄存器是一个16位寄存器,读写的时候,都是数据.
    mov dx,0x1f0

    .go_on_read:
        in ax,dx 
        mov [bx],ax 
        add bx,2  ;ax 是 16位寄存器,读出的也是2字节,因此读一次 dx+2
        loop .go_on_read
        ret 

    msg_start db "2 mbr start!!!!!"
    msg_end db 0

    times 510-($-$$) db 0
    db 0x55,0xaa

改动的只是,一个数而已

5.4 start.sh

同上,因此loader.asm最后编译出的文件大于512字节了,因此在dd刻录的时候需要,多刻录几块,也改为4

#! /bin/bash

# 编译mbr.asm
echo "----------nasm mbr.asm----------"

if !(nasm -o mbr.bin mbr.asm);then
    echo "nasm error"
    exit
fi

# 刻录mbr.bin
echo "----------dd mbr.bin  ----------"
if !(dd if=./mbr.bin of=../hd60m.img bs=512 count=1 conv=notrunc);then
    echo "dd error"
    exit
fi

# 编译 loader.asm
echo "----------nasm loader.asm----------"

if !(nasm -o loader.bin loader.asm -I include/);then
    echo "nasm error"
    exit
fi


# 刻录loader.bin
echo "----------dd loader.bin  ----------"
if !(dd if=./loader.bin of=../hd60m.img bs=512 count=4 seek=2 conv=notrunc);then
    echo "dd error"
    exit
fi

# 删除临时文件
sleep 1s  
rm -rf mbr.bin
rm -rf loader.bin

# 运行bochs
cd ..
bochs

改动也只是一个数而已

5.5 运行

./start/sh

然后c执行,ctrl+c打断,info gdt查看全局描述符表

Accessed表示被访问了,由cpu设置的.

6 处理器微架构简介

6.1 流水线

6.2 乱序执行

6.3 分支预测

7 保护模式下内存段的保护

保护模式的保护二字主要体现在段描述符的属性字段中.cpu用这些属性来检查指令的合法性,从而起到了保护作用.

7.1 加载选择子时候的保护

当更换段寄存器中的值时,实际上是向段寄存器中加载选择子,为了避免内存非法引用,cpu会进行下面这些检查:

  1. 首先根据选择自的值验证描述符是否越界.
  2. 选择子高13位是段描述符的索引值.

首先cpu必须保证选择子是正确的,判断选择子的索引值一定要小于等于描述符表中的描述符个数.

然后检查选择子的属性:

  1. 只有具备可执行属性的段(代码段),才能加载到CS寄存器中.
  2. 具有可执行属性的段,只能被加载到CS寄存器中
  3. 只有具备可写属性的段(数据段),才能被加载到SS栈段中
  4. 至少具备可读属性的段才能被加载到DS,ES,FS,GS段寄存器中

再然后,会检查段的P位,如果P位不为1,那么会自动专区执行相应的异常处理程序

7.2 代码段和数据段的保护

对于代码段和数据段,cpu没访问一个地址,都要确认改地址不能超过其所在内存段的范围.段可以访问的区域为:段基址~段基址+段界限

7.3 栈段的保护

段描述符中type中的E位标识段的扩展方向,栈向下扩展是push指令的作用,与E位无关.也就是说,数据段,本身就可以作为栈段.

猜你喜欢

转载自www.cnblogs.com/perfy576/p/9119000.html
今日推荐