《操作系统真象还原》笔记 (上)

0. 常见概念

陷入内核:应用程序特权级3,操作系统内核特权级0。CPU通过系统调用进入内核态

CPU 内部的段寄存器 (Segment reg)

  • CS -- 代码段寄存器 (CodeSegmentRegister),其值为代码段的段基值。

  • DS -- 数据段寄存器 (DataSegmentRegister),其值为数据段的段基值。

  • ES -- 附加段寄存器 (ExtraSegmentRegister),其值为附加数据段的段基值,称为“附加”是 因为此段寄存器用途不像其他 sreg 那样固定,可以额外做他用。

  • FS -- 附加段寄存器 (ExtraSegmentRegister),其值为附加数据段的段基值,同上,用途不固定, 使用上灵活机动。

  • GS -- 附加段寄存器 (Extra Segment Register),其值为附加数据段的段基值。

  • SS -- 堆栈段寄存器 (Stack Segment Register),其值为堆枝段的段值。

实模式和保护模式:内存访问都是以 段基址:段内偏移 进行的

  • 实模式 -- 段寄存器中直接存放段基址,可以直接算出物理地址,直接访问物理地址不安全。

  • 保护模式 -- 段寄存器中不存放段基址而是存放段选择子,根据段选择子在GDT (全局描述符) 表中找到段基址。

编译:

  • 预处理:预处理器将高级语言中的宏展开,去掉代码注释,为调试器添加行号等;

  • 编译阶段:编译阶段是将预处理后的高级语言进行词法分析、语法分析、 语义分析、优化,最后生成汇编代码;

  • 汇编阶段:汇编阶段是将汇编代码编译成目标文件,也就是转换成了目标机器 平台上的机器指令

  • 链接阶段:链接阶段是将目标文件连接成可执行文件。

Section 和 Segment 的区别:

  • Section 称为节,是指在汇编源码中经由关键字 section 或 segment 修饰、逻辑划分的指令或数据区域, 汇编器会将这两个关键字修饰的区域在目标文件中编译成节,也就是说“节”最初诞生于目标文件中。(开发过程中人为划分的逻辑区域)

  • Segment 称为段,是链接器根据目标文件中属性相同的多个 section 合并后的 section 集合,这个集合称为segment,也就是段,链接器把目标文件链接成可执行文件,因此段最终诞生于可执行文件中。 我们 平时所说的可执行程序内存空间中的代码段和数据段就是指的 segment。(最终可执行文件中的同类区域集合)

1. 环境搭建

1.1 整体环境结构

主机 -> 虚拟机 (virtualBox, 统一环境) -> Linux (CentOS, 开发环境) -> 虚拟机 (bochs, 代码运行环境)

1.2 环境安装

2. 编写 MBR

MBR:主引导记录(MBR,Master Boot Record),是采用 MBR 分区表的硬盘的第一个扇区,即 C / H / S 地址的 0 柱面 0 磁头 1 扇区,也叫做 MBR 扇区。

  1. 编写主引导程序,MBR 代码 mbr.S

  1. 使用 NASM 编译汇编代码

  1. 使用 dd 覆写虚拟硬盘 .img 的指定扇区

  1. 使用 bochs 启动,input c 跳转到 MBR

nasm -o mbr.bin mbr.S
dd if=/home/kli4/Code/01_mbr_1/mbr.bin of=/home/kli4/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc

vstart: 对于汇编文件的 Section 给出虚拟起始地址,当程序预先知道自己会被加载器加载到内存的什么位置时,可以以此指定本 Section 的起始地址,从而和最终程序所在位置匹配。(编译器通过这个起始地址对程序内的跳转操作计算地址,加载器负责将程序加载到指定位置,CPU 则只负责按实际地址进行运行)

代码路径:./Code/01_mbr_1/

  • mbr.S

; MBR code
; ----------------------------------------
SECTION MBR vstart=0x7c00  ; Start addr compiled as 0x7c00
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov fs,ax
    mov sp,0x7c00

; 0x06 for screen clean
; ----------------------------------------
; INT 0x10   Func Code: 0x06  Clean screen
; ----------------------------------------
; Input:
; AH Func-Code=0x06
; AL = Lines (0 for all)
; BH = Line attribute
; (CL,CH) = Window left-up (X, Y) position
; (DL,DH) = Window right-down (X, Y) position
; No reture value:
    mov ax, 0x600
    mov bx, 0x700
    mov cx, 0        ; left-up: (0, 0)
    mov dx, 0x184f   ; right-down: (80, 25)
                     ; VGA text mode, one line only 80 chars, totally 25 lines.
                     ; Start from 0, 0x18=24, 0x4f=79
    int     0x10

; Get cursor position to print chars
    mov ah, 3        ; Input: No.3 sub-func is get cursor pos, stored in ah register
    mov bh, 0        ; bh store for cursor page number

    int 0x10         ; Output: ch = cursor start line, cl = cursor end line
                     ; dh = cursor current line, dl = cursor current column
; Get cursor position end

; Print char string
; Use 10h interrupt, No.13 sub-func to print string
    mov ax, message
    mov bp, ax       ; es:bp is string start addr, es = cs now
                     ; Already init for sreg
; dx include cursor posi for use, cx can be ignore
    mov cx, 5        ; cx is string length, not-include \0
    mov ax, 0x1301   ; sub-func 13 to show string attri, stored in ah
                     ; al set write char method, cursor follow string show
    mov bx, 0x2      ; bh is show page, No.0 page here
    int 0x10         ; Exec BIOS 0x10 interrupt
; Print char string end

    jmp $            ; Stop process here($ is current line addr)

    message db "1 MBR"times 510-($-$$) db 0  ; $ is line number, $$ is section start line number, $-$$ is already used size, 510(512-2) sub used size is left space, filled with 0
    db 0x55,0xaa     ; MBR last 2 magic number

3. 完善 MBR

3.1 实模式与汇编基础

3.1.1 CPU工作原理示意图

  1. 控制单元取下一条待运行指令,位于 PC 处,x86CPU 为 cs:ip,CPU 从地址总线获取到地址;

  1. CPU 将指令存到指令寄存器 IR 中,由译码器 ID 确定操作码和操作数类型,若操作数在内存中,则获取到存储单元 SRAM,若在寄存器中,则直接使用;

  1. 操作控制器 OC 控制运算单元进行执行,ip 寄存器加上当前指令的大小,指向下一条指令。

3.1.2 通用寄存器

3.1.3 寻址方式

  • 寄存器寻址:mov ds, ax,操作数放在寄存器中,指定寄存器号(如 ax)操作

  • 立即数寻址:mov ax, 0x01,操作数作为指令的一部分,放在代码段后面

  • 内存寻址:直接寻址 / 基址寻址 / 变址寻址 / 基址变址寻址

  1. 直接寻址:mov ax [0x1234],操作数在存储器(ds:0x1234)中,指令包含有效地址

  1. 基址寻址:mov ax [bx],寄存器间接寻址,有效地址在(ds:bx)中

  1. 变址寻址:mov ax [si+6],有效地址为基址寄存器(bp/bx)或变址寄存器(si/di)加上位移

  1. 基址变址寻址:mov [bx+di], ax,有效地址位于基址+变址

3.1.4 函数调用

  • call / ret: 近(同一个段内)调用,将 IP(2字节)压栈 / 出栈,call 0x03(机器码指令中为相对地址)

  • call (far) 0:0x1111 / retf: 将 CS 和 IP (共4字节)压栈 / 出栈

  • jmp: 直接跳转,不保存任何信息,jmp 相对地址

  • 其中,jmp/call shor/near 分别为 8/16 位

3.1.5 eflags寄存器

3.2 改进MBR -- 直接操作显卡

代码路径:./Code/02_mbr_2/

  • mbr.S

; MBR code
; ----------------------------------------
SECTION MBR vstart=0x7c00  ; Start addr compiled as 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

; 0x06 for screen clean
; ----------------------------------------
; INT 0x10   Func Code: 0x06  Clean screen
; ----------------------------------------
; Input:
; AH Func-Code=0x06
; AL = Lines (0 for all)
; BH = Line attribute
; (CL,CH) = Window left-up (X, Y) position
; (DL,DH) = Window right-down (X, Y) position
; No reture value:
    mov ax, 0x600
    mov bx, 0x700
    mov cx, 0        ; left-up: (0, 0)
    mov dx, 0x184f   ; right-down: (80, 25)
                     ; VGA text mode, one line only 80 chars, totally 25 lines.
                     ; Start from 0, 0x18=24, 0x4f=79
    int     0x10

; Print char string "1 MBR"
; Output background is green, front is red
    mov byte [gs:0x00],'1'
    mov byte [gs:0x01],0xA4  ; A is green backgroud, 4 is red front

    mov byte [gs:0x02],' '
    mov byte [gs:0x03],0xA4

    mov byte [gs:0x04],'M'
    mov byte [gs:0x05],0xA4

    mov byte [gs:0x06],'B'
    mov byte [gs:0x07],0xA4

    mov byte [gs:0x08],'R'
    mov byte [gs:0x09],0xA4

    jmp $            ; Stop process here($ is current line addr)

    times 510-($-$$) db 0  ; $ is line number, $$ is section start line number, $-$$ is already used size, 510(512-2) sub used size is left space, filled with 0
    db 0x55,0xaa     ; MBR last 2 magic number

运行结果:红字绿底的“1 MBR”不停闪烁

3.3 bochs调试方法

3.3.1 关于bochs

相关文件

  • bochs.out -- bochs 运行过程中的日志文件

  • bochsrc.disk -- bochs 的配置文件

  • hd60M.img -- 使用 bin/bximage 创建出来的虚拟硬盘,需要在 bochsrc.disk 中指定后使用

启动命令

bochs -f bochsrc.disk

命令类型

3.3.2 bochs 常用命令表

bochs 常用命令

3.4 改进MBR -- 使用硬盘

3.4.1 硬盘控制器端口

配置文件

在 bochsrc.disk 中配置了 ioaddr

ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14

端口列表

  • Command Block register: 用于向硬盘写入命令字或者从硬盘控制器获取硬盘状态

  • Control Block register: 用于控制硬盘工作状态

LBA (Logical Block Address)

逻辑上为硬盘扇区进行编号,而不用考虑物理结构,编号从 0 开始递增,每个扇区 512KB。LBA 分为两种,LBA28 和 LBA48,分别用 28/48 比特来表示扇区地址。例如 0x01 表示编号为 1 的逻辑扇区。

LBA 寄存器

  • LBA low: 用来存储 28 位的 0-7 位

  • LBA mid: 用来存储 28 位的 8-15 位

  • LBA high: 用来存储 28 位的 16-23 位

  • Device: 低 4 位存储 LBA 地址的 24-28 位,第 4 位指定通道上的主盘 (0) / 从盘 (1),第 6 位用来设置是否启用 LBA 方式,1 为 LBA (从 0 扇区开始) 模式,0 为 CHS (从 1 扇区开始) 模式,第 5/7 位为 MBS 位,固定为 1。

Status / Command 寄存器

0x1F7 在写操作时,是 command 寄存器,主要使用下面 3 个命令:

  1. identify: 0xEC,即硬盘识别

  1. read sector: 0x20,即读扇区

  1. write sector: 0x30,即写扇区

在读操作时,是 status 寄存器:

3.4.2 硬盘操作基本方法

一旦 command 寄存器被写入之后,硬盘便开始工作,因此最后再写 command 寄存器。

  1. 先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。

  1. 往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。

  1. 往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4位,选择操作的硬盘 (master 硬盘或 slave 硬盘)。

  1. 往该通道上的 command 寄存器写入操作命令。

  1. 读取该通道上的 status 寄存器,判断硬盘工作是否完成。

  1. 如果以上步骤是读硬盘,进入下一个步骤。否则结束。

  1. 将硬盘数据读出,可通过不断的状态查询,在数据准备完毕之后读取。

3.5 编写 MBR + Loader 代码

3.5.1 使用硬盘的 MBR 代码

代码路径:./Code/03_loader/

  • mbr.S

; MBR code
; ----------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00  ; Start addr compiled as 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

; 0x06 for screen clean
; ----------------------------------------
; INT 0x10   Func Code: 0x06  Clean screen
; ----------------------------------------
; Input:
; AH Func-Code=0x06
; AL = Lines (0 for all)
; BH = Line attribute
; (CL,CH) = Window left-up (X, Y) position
; (DL,DH) = Window right-down (X, Y) position
; No reture value:
    mov ax, 0x600
    mov bx, 0x700
    mov cx, 0        ; left-up: (0, 0)
    mov dx, 0x184f   ; right-down: (80, 25)
                     ; VGA text mode, one line only 80 chars, totally 25 lines.
                     ; Start from 0, 0x18=24, 0x4f=79
    int     0x10

; Print char string "1 MBR"
; Output background is green, front is red
    mov byte [gs:0x00],'1'
    mov byte [gs:0x01],0xA4  ; A is green backgroud, 4 is red front

    mov byte [gs:0x02],' '
    mov byte [gs:0x03],0xA4

    mov byte [gs:0x04],'M'
    mov byte [gs:0x05],0xA4

    mov byte [gs:0x06],'B'
    mov byte [gs:0x07],0xA4

    mov byte [gs:0x08],'R'
    mov byte [gs:0x09],0xA4
    ; pass parameter for rd_disk_m_16
    mov eax,LOADER_START_SECTOR  ; lba start sector address
    mov bx,LOADER_BASE_ADDR      ; Write address
    mov cx,1                     ; Sector number to read
    call rd_disk_m_16            ; read sector function, 16 bit mode read disk

    jmp LOADER_BASE_ADDR         ; Loader is saved in 0x900, to load kernel

; ----------------------------------------
; Func: read n sector from disk
rd_disk_m_16:
; ----------------------------------------
                                 ; exa=LBA sector number, Logical Block Address, LBA
                                 ; bx=the address to write
                                 ; cx=Sector number to read
    mov esi,eax  ; backup eax, since out al would affect eax low 8 bits
    mov di,cx    ; backup cx, cx would be written by others when read data
; Read disk:
; Step 1: set sector number need to read
    mov dx,0x1f2 ; in bochsrc.disk, ioaddr1=0x1f0, sector count register is accessed by 0x1f2 port
    mov al,cl
    out dx,al    ; Sector number to read

    mov eax,esi  ; restore eax

; Step 2: Set LBA address to 0x1f3 - 0x1f6
    ; 0x1f3 is LBA low, 0x1f4 is LBA mid, 0x1f5 is LBA high, 0x1f6 is device register, for LBA config
    ; LBA address 7-0 bit write to port 0x1f3
    mov dx,0x1f3  ; LBA low
    out dx,al

    ; LBA address 15-8 bit write to port 0x1f4
    mov cl,8
    shr eax,cl  ; shift right, to get high bits address for al
    mov dx,0x1f4  ; LBA mid
    out dx,al

    ; LBA address 23-16 bit write to port 0x1f5
    shr eax,cl
    mov dx,0x1f5  ; LBA high
    out dx,al

    shr eax,cl
    and al,0x0f  ; LBA 24-27 bit
    or al,0xe0   ; Set 7-4 bit to 1110, lba mode, 5&7 bit is fixed(1), 6 bit 1 is enable LBA
    mov dx,0x1f6 ; Device register
    out dx,al

; Step 3: Set readcommand 0x20 to port 0x1f7
    mov dx,0x1f7
    mov al,0x20  ; Read sector command is 0x20
    out dx,al    ; Write to 0x1f7 then disk start work

; Step 4: Check disk status
  .not_ready:
    ; Same port, write is forcommand write, read is for disk status
    nop          ; Do a small sleepin al,dx     ; 0x1f7 is status register when read
    and al,0x88  ; 4th bit is 1 presents that disk is ready for data transfer, 7th bit is 1 presents disk busy
    cmp al,0x08  ; If equal, ZF is 0, means ready, jnz to jump
    jnz .not_ready  ; Keep waiting until ready

; Step 5: Read data from 0x1f0
    mov ax,di
    mov dx,256
    mul dx     ; mul dx and ax/al, 8 bits mul for al, result 16 bits saved in ax, 16 bits for ax, result 32 bits saved in dx(high bits) and ax(low bits). mul to get readtimes, saved in ax
    mov cx,ax  ; di is the sector nums need to read, 512 bytes each sector, read 2 bytes each time
                ; Totally need di*512/2 time, equal to di*256
    mov dx,0x1f0
  .go_on_read:
    in ax,dx
    mov [bx],ax  ; Save read data in [bx] memory, maximum is 64KB
    add bx,2     ; Next 2 bytes address
    loop .go_on_read  ; loop times is in cx
    ret          ; Return to the call function line
; ----------------------------------------
; Func end: rd_disk_m_16
; ----------------------------------------

    times 510-($-$$) db 0  ; $ is line number, $$ is section start line number, $-$$ is already used size, 510(512-2) sub used size is left space, filled with 0
    db 0x55,0xaa     ; MBR last 2 magic number
  • include/boot.inc

; --------- loader and kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

编译加载:

nasm -I include/ -o mbr.bin mbr.S
dd if=/home/kli4/Code/03_loader/mbr.bin of=/home/kli4/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc

3.5.2 Loader 代码

代码路径:./Code/03_loader/

  • loader.S

%include "boot.inc"
section load vstart=LOADER_BASE_ADDR

; Print char string "2 LOADER"
; Output background is green, front is red
    mov byte [gs:0x00],'2'
    mov byte [gs:0x01],0xA4  ; A is green backgroud, 4 is red front

    mov byte [gs:0x02],' '
    mov byte [gs:0x03],0xA4

    mov byte [gs:0x04],'L'
    mov byte [gs:0x05],0xA4

    mov byte [gs:0x06],'O'
    mov byte [gs:0x07],0xA4

    mov byte [gs:0x08],'A'
    mov byte [gs:0x09],0xA4

    mov byte [gs:0x0a],'D'
    mov byte [gs:0x0b],0xA4

    mov byte [gs:0x0c],'E'
    mov byte [gs:0x0d],0xA4

    mov byte [gs:0x0e],'R'
    mov byte [gs:0x0f],0xA4

    jmp $            ; Stop process here($ is current line addr)

编译加载:

nasm -I include/ -o loader.bin loader.S
dd if=/home/kli4/Code/03_mbr_3/loader.bin of=/home/kli4/bochs-2.6.2/hd60M.img bs=512 count=1 seek=2 conv=notrunc

3.5.3 输出结果

4. 保护模式

4.1 相关概念

实模式存在的一些问题

安全方面:

  1. 操作系统与用户属于同一特权级别

  1. 用户程序引用的地址都是指向真实物理地址,即逻辑地址与物理地址相同

  1. 用户可以自由修改段基址,可以访问所有内存

使用方面:

  1. 访问超过 64KB 的内存区域时,要切换段基址,较为麻烦

  1. 一次只能运行一个程序,无法充分利用计算机资源

  1. 共 20 条地址线,最大可用内存仅为 1MB

最终处理器厂商开发出保护模式,物理内存地址不能直接被程序访问,程序内部的地址(虚拟地址)需要呗转化为物理地址后再去访问。地址转化由处理器合操作系统共同完成,处理器在硬件上提供地址转换部件,操作系统提供转换过程中所需要的页表。

实模式的 CPU 运行环境 16 位,保护模式的运行环境是 32 位。32 位的 CPU 可以以兼容的方式运行 16 位的实模式。

4.2 保护模式的扩展

4.2.1 寄存器扩展

保护模式下,将原有的 16 位寄存器扩展成 32 位,在寄存器前面加上了字符 E 表示扩展,而低 16 位的部分仍然保留作单独使用,为了兼容实模式。而高 16 位无法单独使用,只能在作为 32 位寄存器时使用。

段寄存器未进行扩展,仍保留 16 位。

4.2.2 寻址方式扩展

实模式只能使用 BX(默认段寄存器 ds,用于访问数据段)和 BP(默认段寄存器 ss,用于访问栈)。保护模式则可以使用更多的寄存器。

4.2.3 运行模式反转

由于实模式支持的指令是 16 位,保护模式 32 位,不同模式兼容时,需要向编译器指定指令需要编译成的位数,

  • [bits 16] 是告诉编译器,到下一个 bit 指令之前的代码编译成 16 位的机器码。

  • [bits 32] 是告诉编译器,到下一个 bit 指令之前的代码编译成 32 位的机器码。

  • 未使用 bits 指令的地方,默认为 [bits 16]

  • 操作数反转前缀 0x66,寻址方式反转前缀 0x67,两者都只针对当前指令临时反转

在 16 位模式下使用 eax 寄存器,会自动添加 0x66 前缀,16 位下使用寄存器寻址 [eax] 会添加 0x66 0x67前缀。

4.2.4 指令扩展

同一指令需要根据模式不同做出不同行为

add / sub / mul / div 指令

add al, cl   ; 支持8位操作数
add ax, cx   ; 支持16位操作数
add eax, ecx ; 支持32位操作数

sub al, cl   ; 支持8位操作数
sub ax, cx   ; 支持16位操作数
sub eax, ecx ; 支持32位操作数

mul cl   ; al为另一个乘数,结果为16位,存入ax
mul cx   ; ax为另一个乘数,结果为32位,存入eax
mul ecx  ; eax为另一个乘数,结果为64位,存入edx:eax, edx为积的高32位,eax为低32位

div cl   ; 被除数16位,为ax,所得结果,商在寄存器al,余数在ah
div cx   ; 被除数32位,高16位为dx,低16为ax,所得结果,商在寄存器ax,余数在dx
div ecx  ; 被除数64位,高32位为edx,低32为eax,所得结果,商在寄存器eax,余数在edx

push 指令

立即数:

  • push 8 位立即数:根据模式转变为 16 (实模式) / 32 (保护模式) 位数据压入

  • push 16 位立即数:直接压入,sp (实模式) / esp (保护模式) 减去 2

  • push 32 位立即数:直接压入,sp (实模式) / esp (保护模式) 减去 4

段寄存器:cs / ds / es / fs / gs / ss,按当前模式默认大小压入数据,sp-2 / esp-4

通用寄存器:压入数据原大小,16 / 32 位数据,栈指针减 2 / 4

4.3 全局描述符表

全局描述符表 (Global Descriptor Table, GDT) 是保护模式下内存段的记录表格。

4.3.1 段描述符

用来专门描述一个内存段,结构大小为连续的 8 字节。

低 32 位中

  • 0-15 位:段界限 0-15 位

  • 16-31 位:段基址 0-15 位

高 32 位中

  • 0-7 位:段基址 16-23 位

  • 24-31 位:段基址 24-31 位

  • 8-11 位:type 字段,共 4 位,用于指定描述符的类型。

  • 12 位:S 字段,0 表示系统段(硬件需要的结构为系统段),1 表示数据段(所有代码/数据等)

主要关注非系统段,

  1. A 位表示 Accessed,由 CPU 设置,每当该段被 CPU 访问后,CPU 将此位置 1,新段为 0。

  1. C 位表示 Conforming,一致性代码段 (依从代码段),即共享代码段,若该段是转移的目标段,且是一致性代码段 (C 位为 1),则转移后的特权级不以自己 DPL (Decsriptor Privilege level) 为主。

  1. R 位表示 Readable,1 表示可读,0 表示不可读。用于限制代码段的访问。

  1. X 位表示 EXecutable,表示该段是否可以执行,1 可执行 0 不可执行。

  1. E 位表示 Extend,E 为 0 表示向上扩展,地址越来越高,常用于代码段和数据段,E 为 1 表示向下扩展,地址越来越低,常用于栈段。

  1. W 位表示 Writable,W 为 1 表示可写,通常用于数据段;W 为 0 表示不可写,通常用于代码段。

  • 13-14 位:DPL 字段,Descriptor Privilege Level,即描述符特权级,划分特权级 。0123级特权,数字越小特权越大,从实模式进入保护模式后,特权自动为 0,此时为操作系统的部分,处于最高特权,用户权限最小,为3。

  • 15 位:P 字段,Present,即段是否存在。若段在内存中,P 为1,否则为 0。CPU 访问 P 为 0 的段会抛出异常,异常处理不由 CPU 负责,通常异常处理中将内存段换入,再将 P 置 1。

  • 16-19 位:段界限的 16-19 位。

  • 20 位:AVL 字段,Available,针对用户而言,是否可用的,操作系统可以随意使用此位。

  • 21 位:L 字段,用于设置是否 64 位代码段,32 位 CPU 下编程,通常为 0。

  • 22 位:D/B 字段,用来指示有效地址(段内偏移地址)及操作数大小。为了兼容 286 的 16 位保护模式。对于代码段,此位为 D 位,D 为 0 表示有效地址和操作数为 16 位,有效地址寄存器为 IP 寄存器。D 为 1 则 32 位,用 EIP。对于栈段,此位为 B 位,指定操作数大小,B 为 0 则使用 sp 寄存器,栈的起始地址是 16 位寄存器的最大寻址范围,0xFFFF,B 为 1 则使用 esp 寄存器,最大范围 0xFFFFFFFF。

  • 23 位:G 字段,Granularity,粒度,用于指定段界限粒度单位大小,若 G 为 0,段界限的单位为 1 字节,最大段界限(共20 位)为 2^20*1字节,即 1MB;若 G 为 1,则粒度大小为 4KB,最大段界限为 2^20*4KB,即 4GB。

  • 24-31 位:段基址的第 24-31 位。

4.3.2 全局描述符表 GDT + 局部描述符表 LDT + 选择子 selector

全局描述符表 GDT

Global Descriptor Table,存放所有段描述符的数组,每个段描述符 8 字节,位于内存中。其中 GDT 的 0 号描述符不可用,防止选择子未初始化进行访问。

GDTR 寄存器

GDT Register,存放 GDT 内存所在地址及大小的寄存器,共 48 位。通过 lgdt 进行初始化,即

lgdt 48位内存数据,前 16 位为 GDT 以字节为单位的界限值,相当于 GDT 字节大小减 1。后 32 位为 GDT 的起始地址。

局部描述符表 LDT 与 LDTR 寄存器

用于存放每个任务自己的私有内存段表,即 LDT 表。每个任务都有自己的 LDT 表,位于内存中,由 LDTR 寄存器指向,加载 LDT 的命令为 lldt,每次任务切换的时候都需要使用 lldt 切换相应的 LDT 表。LDT 也是内存区域,需要先注册到 GDT 中。

命令格式:lldt 16位寄存器/16位内存。其内容为选择子,用于在 GDT 中找到 LDT 的段描述符

LDT 的 0 号描述符可用,因为使用 LDT 则选择子的 TI 已设置,选择子必然已初始化。

选择子 selector

用于在描述符表中选择对应段的索引值,共 16 位,

  • 0-1 位:RPL,即请求特权级,可以表示 0123 四种特权

  • 2 位:TI 位,即 Table Indicator,TI 为 0 表示在 GDT 中索引描述符, 1 表示在 LDT 中进行索引

  • 3-15 位:索引值,用于在 GDT/LDT 中索引对应描述符

段描述符与内存段

通过选择子从描述符表中获取段描述符后,CPU 从段描述符中取出段基址,加上段偏移,组成“段基址:段内偏移地址”的形式。

由于此时已进入保护模式,寄存器为 32 位,因此地址计算变为 < 段基址+段内偏移地址 > 的方式,无需对段基址进行 16 位偏移。

LDT 查找过程

参考 GDT 与 LDT

  1. 段选择器中 TI 位为1,说明段信息在 LDT 中,此时根据 GDTR 找到 GDT;

  1. 根据 LDTR 找到当前任务的 LDT 描述符在 GDT 中的位置;

  1. 根据 LDT 描述符找到 LDT 所在的位置;

  1. 根据段选择器中的索引值,查找 LDT 得到当前任务私有的段描述符;

  1. 根据段描述符的信息,找到最终对应的内存段。

4.3.3 打开 A20 地址线

地址环绕

实模式下存在地址环绕 (wrap-around),即地址线最大为 20 位,采用“段基址 : 段内偏移地址”的方式,最大可以得到 0xFFFF0+0xFFFF=0x10FFEF,而 20 位地址线最大寻址空间为 1MB,即 0xFFFFF,超过部分将环绕回 0 地址继续映射。因此 1MB 多出来的内存被称为高端内存区 HMA。保护模式下为了访问所有地址,需要关闭地址环绕,打开第 21 根地址线 A20。

  • 如果 A20Gate 被打开,当访问到 0x100000-0x10FFEF 之间的地址时, CPU 将真正访问这块物理内存。

  • 如果 A20Gate 被禁止,当访问到 0x100000-0x10FFEF 之间的地址时, CPU将采用 8086/8088 的地址回绕。

打开 A20Gate 的方式

将端口 0x92 的 1 位 (第 2 个 bit) 设置为 1 就可以了

in al, 0x92
or al, 0000_0010B
out 0x92, al

4.3.4 保护模式的开关

控制类寄存器 CRx,打开保护模式的开关需要设置 CR0 寄存器的第 0 位,即 PE (Protection Enable) 位,打开后便进入保护模式。PE=0 为实模式,PE=1 为保护模式。

打开保护模式方式

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

4.3.5 进入保护模式

A. 代码实现

代码路径:./Code/04_loader_protect/

  • mbr.S

diff --git a/03_loader/mbr.S b/03_loader/mbr.S
index 9fc91fd..b0503e0 100644
--- a/03_loader/mbr.S
+++ b/03_loader/mbr.S
@@ -49,7 +49,7 @@ SECTION MBR vstart=0x7c00  ; Start addr compiled as 0x7c00
     ; pass parameter for rd_disk_m_16
     mov eax,LOADER_START_SECTOR  ; lba start sector address
     mov bx,LOADER_BASE_ADDR      ; Write address
-    mov cx,1                     ; Sector number to read
+    mov cx,4                     ; Sector number to read
     call rd_disk_m_16            ; read sector function, 16 bit mode read disk
 
     jmp LOADER_BASE_ADDR         ; Loader is saved in 0x900, to load kernel

由于新的 loader.bin 文件大小超过了 512KB,需要读取的扇区超过了 1,mbr文件需要修改读取扇区数量,同时在将 bin 写入硬盘时,扇区数量也需要对应修改。

  • include/boot.inc

; --------- loader and kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

; --------- gdt description attribute ----------
DESC_SEG_BASE3     equ  (0x00 << 24)                    ; Segment base / 24-31, flat mode all segment base address start from 0
DESC_G_4K          equ  1000_0000_0000_0000_0000_0000b  ; G bit / 23
DESC_D_32          equ   100_0000_0000_0000_0000_0000b  ; D/B bit / 22
DESC_L             equ    00_0000_0000_0000_0000_0000b  ; L bit / 21, not enable 64bit
DESC_AVL           equ     0_0000_0000_0000_0000_0000b  ; AVL bit / 20, CPU not use
DESC_LIMIT_CODE2   equ       1111_0000_0000_0000_0000b  ; Limit bit / 16-19
DESC_LIMIT_DATA2   equ  DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2  equ       0000_0000_0000_0000_0000b
DESC_P             equ            1000_0000_0000_0000b  ; P bit / 15
DESC_DPL_0         equ             000_0000_0000_0000b  ; DPL bit / 13-14
DESC_DPL_1         equ             010_0000_0000_0000b  ; DPL bit / 13-14
DESC_DPL_2         equ             100_0000_0000_0000b  ; DPL bit / 13-14
DESC_DPL_3         equ             110_0000_0000_0000b  ; DPL bit / 13-14
DESC_S_CODE        equ               1_0000_0000_0000b  ; S bit / 12
DESC_S_DATA        equ  DESC_S_CODE
DESC_S_SYS         equ               0_0000_0000_0000b  ; S bit / 12
DESC_TYPE_CODE     equ                 1000_0000_0000b  ; TYPE bit / 8-11, x=1, c=0, r=0, a=0 code segment is eXecutable, not Conforming, un-Readable, not Accessed
DESC_TYPE_DATA     equ                 0010_0000_0000b  ; TYPE bit / 8-11, x=0, e=0, w=1, a=0 code segment is not eXecutable, Extend up, Writable, not Accessed
DESC_SEG_BASE2     equ  0x00  ; Segment base / 0-7, flat mode all segment base address start from 0
DESC_CODE_HIGH4    equ  DESC_SEG_BASE3 + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + DESC_SEG_BASE2
DESC_DATA_HIGH4    equ  DESC_SEG_BASE3 + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + DESC_SEG_BASE2
DESC_VIDEO_HIGH4   equ  DESC_SEG_BASE3 + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b  ; Video memory start address is 0xb8000, so set this high last byte to 0x0b

; --------- selector attribute ----------
RPL0    equ   00b
RPL1    equ   01b
RPL2    equ   10b
RPL3    equ   11b
TI_GDT  equ  000b
TI_LDT  equ  100b
  • loader.S

%include "boot.inc"
section load vstart=LOADER_BASE_ADDR  ; Addr=0x900 here
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start                      ; 3 bytes code, Addr+=0x03
; dd: define double-word, 4 bytes
; Build gdt and inner description
GDT_BASE:  dd  0x00000000  ; Low 4 bytes
           dd  0x00000000  ; High 4 bytes
; No.1 selector
CODE_DESC: dd  0x0000FFFF
           dd  DESC_CODE_HIGH4  ; value is 0x00c09200 here
; No.2 selector
DATA_STACK_DESC: dd  0x0000FFFF
                 dd  DESC_DATA_HIGH4  ; value is 0x00c09200 here
; No.3 selector
VIDEO_DESC: dd  0x80000007  ; Base addr is video adaptor memory 0xb8000-0xbffff, size is 0x7fff, limit=(0xbffff-0xb8000)/4k=0x7, bit '0x0b' set in high 4 bytes
            dd  DESC_VIDEO_HIGH4  ; value is 0x00c0920b, if accessed, 0x00c0930b
; 4*8 bytes above, Addr+=4*0x8    ; dp: define quad-word, 8 bytes
GDT_SIZE  equ  $ - GDT_BASE
GDT_LIMIT equ  GDT_SIZE - 1
times 60 dq 0  ; Preserve 60 desc postions here ; 60*8 bytes here, Addr+=60*0x8
SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0  ; Equal to (CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0, selector num is 1
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0  ; Same as above
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0  ; Same as above

; Below is gdt pointer, first 2 bytes is gdt limit, last 4 bytes is gdt start address

gdt_ptr  dw  GDT_LIMIT
         dd  GDT_BASE            ; 6 bytes gdt prt, Addr+=0x6
loadermsg db '2 loader in real.' ; 17 bytes here, Addr+=0x11

loader_start:
; loader start addr is Addr=0x900+0x21a=0xb1a
;-----------------------------------------------------
; INT 0x10   Func Code: 0x13   Func Desc: print string
;-----------------------------------------------------
; Input:
; AH sub func code=13H
; BH = Page num
; BL = Attribute(if AL=00H or 01H)
; CX = String len
; (DH, DL)=Position(row, col)
; ES:BP = String addr
; AL = Show output method
;   0 -- String only contains char, and attribute is in BL, cursor postion not change after show
;   1 -- String only contains char, and attribute is in BL, cursor postion will change after show
;   2 -- String contains char and attribute, cursor postion not change after show
;   3 -- String contains char and attribute, cursor postion will change after show
; No return
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg    ; ES:BP = string addr
mov cx, 17           ; CX = string len
mov ax, 0x1301       ; AH = 13, AL = 01h
mov bx, 0x001f       ; Page number is 0(BH=0), blue backgroud pink char(BL=1fh)
mov dx, 0x1800
int 0x10             ; 10h interrupt

;----------- Prepare to enter protect mode -----------
; 1: open A20 gate (Set 0x92 left second bit to 1)
; 2: load gdt
; 3: set cr0 pe to 1 (Set cr0 0 bit to 1)


;----------- Open A20 -----------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------- Load GDT -----------
lgdt [gdt_ptr]


;----------- Set cr0 pe -----------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start  ; Refresh the pipeline, enter protect mode


[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 $
B. 编译加载
nasm -I include/ -o mbr.bin mbr.S
dd if=mbr.bin of=/home/kli4/bochs-2.6.2/hd60M.img bs=512 count=1 conv=notrunc
nasm -I include/ -o loader.bin loader.S
dd if=loader.bin of=/home/kli4/bochs-2.6.2/hd60M.img bs=512 count=4 seek=2 conv=notrunc
C. 运行结果
D. 调试问题
  1. 加载扇区大小错误

  • 问题现象:调试时只出现 1 MBR,未出现其余显示,调试时程序死循环

  • 问题原因:使用 dd 加载 loader.bin 时,count 仍然为 1 未设置对应大小,导致只加载了 512 bytes,而loader.bin 大小超过了 512 bytes,加载不完全导致程序异常

  • 解决方法:设置对应命令 count=4

  1. 显存地址错误(书中代码有误)

  • 问题现象:无法显示字符 P

  • 问题原因:书中显存地址错误,正确的地址应该是 0xb8000,而书中段选择子显存地址设置为0x08000,导致无法显示

  • 解决方法:DESC_VIDEO_HIGH4 的最后一位段基址设置为 0x0b

4.4 处理器微架构

4.4.1 流水线

以取址,译码,执行为例,CPU 中分别由不同硬件负责这三种操作,因此可以实现以下三级流水,

在一个时钟周期内可以同时执行取址,译码,执行操作,从而缩短运行时间。CPU 中的指令是按顺序进行填充的,当遇到类似 jmp 指令时,就会对流水线进行清空操作。

4.4.2 乱序执行

CISC (Complex Instruction Set Computer)

复杂指令集计算机,一条指令包含多个基本操作,例如 push eax 相当于:

  1. 先将栈指针减去字长,如 sub esp, 4

  1. 再将操作数 mov 到 esp 地址,如 mov [esp], eax

RISC (Reduced Instruction Set Computer)

精简指令集计算机,基本属于微操作级别的指令

乱序执行

x86 虽然还是使用 CISC 指令集,但是已采用 RISC 内核,译码讲 CISC 指令分解成多个 RISC 指令,多个微操作之间常常独立无关联,因此适合乱序执行。

mov eax, [0x1234]
push eax
call function

以上指令第一步需要内存访问,内存较慢因此寻址过程中可以处理其他事,而第二步可以拆分为 sub esp, 4 和 mov [esp], eax,在第三步中需要压栈返回地址。

因此在第二步的 sub esp, 4 之后,不需要等待 mov [esp], eax 完成 (即需要第一步内存访问完成),便知道了栈地址,可以直接压栈调用函数,即乱序执行。

4.4.3 缓存与分支预测

缓存

一级缓存 L1,二级缓存 L2,三级缓存 L3,都是静态随机访问存储器 SRAM,CPU 的存储电路与 SRAM 相同。

利用局部性原理,可以很好的配合缓存策略。

分支预测

依据分支预测部件 (分支目标缓冲器 Branch Target Buffer, BTB),其中记录了之前依据跳转分支算法得到的跳转的信息值,来判断预测分支。若没有 BTB 信息,则依据静态预测器,其中的策略是大量统计结果得到的固定策略进行跳转。

分支预测失败,惩罚代价即需要清空流水线

4.4.4 清空流水线与更新段描述符

基于上面的 loader.S,第 78 行中,使用了远转移 jmp 跳转,

jmp dword SELECTOR_CODE:p_mode_start

主要有两个目的,

  1. 清空流水线

在 76 行 mov cr0, eax 之后,便已进入了保护模式,此时由于流水线的功能,76 行之后的指令已经被译码加载完成了,且是以 16 位指令进行译码的,而 83 行指令为32 位指令,此时便会出错,因此需要对流水线进行清空。

  1. 更新段描述符

段描述符缓冲器,是为了加速描述符信息访问而设置的,例如 16 位实模式下,每次需要对段进行左移 4 位的操作,为了避免每次计算,会将计算结果保存在段描述符缓冲器中,在同一个段中直接使用,只有切换段基址时才会重新计算更新。同理保护模式下存放的是段选择子

而从实模式进入保护模式时,如果没有重新引用一个段,则段描述符缓冲寄存器不会更新,实模式下存的是 20 位的段基址,而保护模式下存放的是段选择子,若不更新则在保护模式下会造成错误。

因此,在这里使用远转移 jmp 指令,来清空流水线以及更新段描述符。

4.5 保护模式的内存段保护

主要基于保护模式的内存段保护(其他还有特权级等保护)进行分析。

4.5.1 段寄存器加载选择子的保护

引用一个内存段时,需要往段寄存器中加载选择子,此时主要检查有:

  1. 索引值

选择子的索引值需要小于等于描述符表 (GDT/LDT) 中描述符的个数

根据 TI 值判断是获取 GDT 还是 LDT 的界限值。

  1. 类型值

检查 type 位与加载到的寄存器是否匹配

  1. 是否存在

  • 检查 P 位来确认内存段是否存在,若存在,则将选择子载入段寄存器,且 CPU 将 A 位置为 1

  • 若 P 位为0,则段不存在,可能需要从硬盘中将段转移到内存中,再将 P 位置 1 后继续

其中,P 位由软件(通常为操作系统)设置,A 位由 CPU 设置。

4.6.2 代码段和数据段的保护

  • 代码段本身具有长度,需要保证执行代码段从起始位置+长度后,还能完整落在当前段内

  • 数据段需要保证访问的数据起始位置+长度后,能完整落在当前段内

图中当前指令为 ebfe,EIP 指向位置+指令长度后,跨越了段 A 和 B,超过了界限。

4.6.3 栈段的保护

  • 对于向上扩展的段,实际的段界限是段内可以访问的最后一字节。

  • 对于向下扩展的段,实际的段界限是段内不可以访问的第一个字节。

5. 进入内核

5.1 获取内存容量

5.1.1 三种内存容量获取方法

利用 BIOS 中断 0x15 实现,主要包含 3 个子功能,子功能号放在寄存器 EAX 或 AX中,

  1. EAX=0xE820: 遍历主机上全部内存

  1. AX=0xE801: 分别检测低 15MB 和 16MB-4GB 的内存,最大支持 4GB

为兼容老旧的 ISA 设备,保留了历史遗留的 1MB 缓冲区,因此 15-16MB 之间存在 memory hole,BIOS 中可以配置:

memory hole at address 15m-16m

  1. AH=0x88: 最多检测 64MB 内存,超过 64MB 按 64MB 返回

三种方式的返回结构为地址范围描述符 (Address Range Descriptor Structure, ARDS),

5.1.2 获取内存容量

A. 代码实现

代码路径:./Code/05_loader_mem/

  • mbr.S

diff --git a/05_loader_mem/mbr.S b/05_loader_mem/mbr.S
index d9b56f5..64009ac 100644
--- a/05_loader_mem/mbr.S
+++ b/05_loader_mem/mbr.S
@@ -52,7 +52,7 @@ SECTION MBR vstart=0x7c00  ; Start addr compiled as 0x7c00
     mov cx,4                     ; Sector number to read
     call rd_disk_m_16            ; read sector function, 16 bit mode read disk
 
-    jmp LOADER_BASE_ADDR         ; Loader is saved in 0x900, to load kernel
+    jmp LOADER_BASE_ADDR+0x300   ; Loader is saved in 0x900, to load kernel, 0x300 is start loader addr
 ; ----------------------------------------
 ; Func: read n sector from disk

此处修改 Loader 的跳转地址,配合新的 Loader 文件中的内存分布。

  • loader.S

%include "boot.inc"
section load vstart=LOADER_BASE_ADDR  ; Addr=0x900 here
LOADER_STACK_TOP equ LOADER_BASE_ADDR
; dd: define double-word, 4 bytes
; Build gdt and inner description
GDT_BASE:  dd  0x00000000  ; Low 4 bytes
           dd  0x00000000  ; High 4 bytes
; No.1 selector
CODE_DESC: dd  0x0000FFFF
           dd  DESC_CODE_HIGH4  ; value is 0x00c09200 here
; No.2 selector
DATA_STACK_DESC: dd  0x0000FFFF
                 dd  DESC_DATA_HIGH4  ; value is 0x00c09200 here
; No.3 selector
VIDEO_DESC: dd  0x80000007  ; Base addr is video adaptor memory 0xb8000-0xbffff, size is 0x7fff, limit=(0xbffff-0xb8000)/4k=0x7, bit '0x0b' set in high 4 bytes
            dd  DESC_VIDEO_HIGH4  ; value is 0x00c0920b, if accessed, 0x00c0930b
; 4*8 bytes above, Addr+=4*0x8    ; dp: define quad-word, 8 bytes
GDT_SIZE  equ  $ - GDT_BASE
GDT_LIMIT equ  GDT_SIZE - 1
times 60 dq 0  ; Preserve 60 desc postions here ; 60*8 bytes here, Addr+=60*0x8
SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0  ; Equal to (CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0, selector num is 1
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0  ; Same as above
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0  ; Same as above
; total_mem_bytes used for saving memory storage,
; Current postion is 0x200 bytes offset from loader.bin start, loader.bin load addr is 0x900
; total_mem_bytes addr is 0x900+0x200=0xb00 in memory, Will use this addr in kernel
total_mem_bytes dd 0
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Below is gdt pointer, first 2 bytes is gdt limit, last 4 bytes is gdt start address
gdt_ptr  dw  GDT_LIMIT
         dd  GDT_BASE            ; 6 bytes gdt prt, Addr+=0x6

; Manual Alignment: total_mem_bytes(4)+gdt_ptr(6)+ards_buf(244)+ards_nr(2), total 256 bytes
ards_buf times 244 db 0
ards_nr dw 0    ; ARDS struct numbers

loader_start:
; =================================================================
; 1. int 15h eax=0000E820h, edx=534D4150h ('SMAP') to get memory layout

xor ebx, ebx         ; Set ebx 0, first time ebx call should be 0
mov edx, 0x534d4150  ; edx only set 1 time, would not change in loop
mov di, ards_buf     ; ards struct buffer
.e820_mem_get_loop:
    mov eax, 0x0000e820  ; After exec int 0x15, eax change to 0x534d4150, so need to update sub func code each time in loop
    mov ecx, 20          ; ARDS addr range desc size is 20 bytes
    int 0x15
    jc .e820_failed_so_try_e801  ; If cf is 1, error occured, try 0xe801 sub func
    add di, cx           ; di add 20 bytes, point to next ARDS addr
    inc word [ards_nr]   ; Record ARDS nums, ards_nr++
    cmp ebx, 0           ; If ebx is 0 and cf is not 1, means all ARDS is return done
    jnz .e820_mem_get_loop

; Find the max (base_add_low + length_low) in all ards, that is memory size
    mov cx, [ards_nr]    ; Traverse all ARDS struct, loop time is ARDS nums
    mov ebx, ards_buf
    xor edx, edx         ; edx is max mem size, clean to 0 first
.find_max_mem_area:      ; No need to check type whether 1, max mem block is sure to be available
    mov eax, [ebx]       ; base_add_low
    add eax, [ebx+8]     ; length_low
    add ebx, 20          ; Point to next ARDS struct
    cmp edx, eax         ; Bubble sort, edx to save max
    jge .next_ards       ; If eax < edx, jump to nex ARDS
    mov edx, eax
.next_ards:
    loop .find_max_mem_area
    jmp .mem_get_ok
; =================================================================
; 2. int 15h ax=E801h    get mem size, support max 4G
;    After return, ax=cx, KB for unit, bx=dx, 64KB for unit,
;    in ax and cx register is low 16MB, in bx and dx is 16MB-4GB
.e820_failed_so_try_e801:
    mov ax, 0xe801
    int 0x15
    jc .e801_failed_so_try88  ; if e801 failed, try 0x88 method

; a. Get low 15MB mem first, ax and cx unit is KB, transfer to bytes unit
    mov cx, 0x400        ; cx and ax is same, cx as multiplier
    mul cx
    shl edx, 16
    and eax, 0x0000FFFF
    or edx, eax
    add edx, 0x100000     ; ax is 15MB, need to add 1MB, 1MB is mem hole
    mov esi, edx          ; mov low 15MB mem size data to esi for backup

; b. Then transfer upper 16MB mem to byte unit, bx and dx is 64KB unit mem nums
    xor eax, eax
    mov ax, bx
    mov ecx, 0x10000      ; 0x10000 is 64KB in decimal
    mul ecx               ; 32 bits multiply, eax * ecx, result is 64 bits, high 32 bits in edx, low 32 bits in eax
    add esi, eax          ; Since 0xE801 can only get max 4G mem, 32 bits eax is good enough, edx is 0, so only add eax. esi is step <a> mem size
    mov edx, esi          ; edx is total mem size
    jmp .mem_get_ok
; =================================================================
; 3. int 15h ah=0x88    get mem size, support max 64MB
.e801_failed_so_try88:
    ; After int 15, ax is the KB unit mem size
    mov ah, 0x88
    int 0x15
    jc .error_hlt
    and eax, 0x0000FFFF   ; ax is return value, keep ax only

    ; 16 bits multiply, ax*cx, result is 32 bits, high 16 bits in dx, low 16 bits in ax
    mov cx, 0x400  ; 0x400 is 1024, change mem size(in ax) to byte
    mul cx
    shl edx, 16    ; shift dx to high 16 bits
    or edx, eax    ; Merge dx and ax result, edx is final 32bits result
    add edx, 0x100000  ; 0x88 only return upper 1MB mem, so real mem need to add 1MB

.mem_get_ok:
    mov [total_mem_bytes], edx ; Transform to bytes unit, and save result to total_mem_bytes

    jmp ok_to_continue

.error_hlt:                ; Error halt if 3 mem get methods all failed
    mov ax,0xb800
    mov gs,ax
    mov byte [gs:160], 'E'
    jmp $

ok_to_continue:
;----------- Prepare to enter protect mode -----------
; 1: open A20 gate (Set 0x92 left second bit to 1)
; 2: load gdt
; 3: set cr0 pe to 1 (Set cr0 0 bit to 1)


;----------- Open A20 -----------
in al,0x92
or al,0000_0010B
out 0x92,al

;----------- Load GDT -----------
lgdt [gdt_ptr]


;----------- Set cr0 pe -----------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start  ; Refresh the pipeline, enter protect mode


[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 $

主要做了以下几处修改:

  1. 增加了 ARDS 描述结构,以及 ards_ns 用于存储返回的 ARDS 数量

  1. 起始地址手动对齐到 0x900+0x300=0xc00

  1. 增加了三种获取内存方式的实现

  1. 最终内存结果存储在 0xb00 位置处

B. 运行结果

显示结果与前面无区别,进入了保护模式,通过 xp 命令查看位于 0xb00 处的内存大小结果

可以看到内存结果为 0x02000000,即 32MB,这与 bochsrc.disk 中配置的 megs=32 相对应。

C. 调试问题
  1. 未实现 error_hlt 的内容

  • 问题现象:编译时报错显示没有实现 error_hlt

  • 问题原因:书中没有给出该部分的实现

  • 解决方法:补充完成,如果三种方法都失败,会进入到 error_hlt,并在字符 P 位置显示 E

  1. jmp .section 编译失败

  • 问题现象:若将代码中的 ok_to_continue 换成 .ok_to_continue,则编译错误

  • 问题原因:未知具体原因,可能与汇编 label / section 的使用区别有关 (后续发现,带 . 的 section 类似于局部变量,只能在局部范围内使用)

  • 解决方法:使用 ok_to_continue,可以正常运行

5.2 内存分页

  • 线性地址:虚拟地址,每个进程自有的虚拟内存空间内的地址,使用 x 命令调试

  • 物理地址:实际地址,实际物理内存上的地址,唯一的,使用 xp 命令调试

  • 分段:人为对程序进行逻辑上的划分,例如代码段/数据段等,通过类似 基址:偏移 的方式获取

  • 分页:操作系统将存储空间按固定大小划分,每块单位作为页,从而减少外部碎片的产生

5.2.1 分页的原因

首先保护模式下的内存段,在段描述符中,

  • 有 P 位表示当前段是否存在于内存中。

CPU 访问段时,首先查询 P 位,若存在,则将 A 位置 1 后使用,若不存在,则出发 NP 异常,调用对应的中断,中断由操作系统负责提供,将相应的段从外部存储转移到内存中,再将 P 置 1,中断函数返回,CPU 重复执行检查后使用段。

  • 有 A 位表示最近刚被 CPU 访问过。

访问段时,CPU 将 A 位置 1,而操作系统负责置 0,根据周期内 A 位置 1 的次数,统计出段使用的频率。当物理内存不足时,将使用频率最低的段先换出去。

此时,若一个进程的段比较大,此时将段整个换出将会较慢;或者当内存很小,无法容纳一个进程段,例如内存 4K,进程段大小 5K,此时将无法运行。

而将内存段划分为较小的页则可以避免该问题,但是实现时需要解除线性地址物理地址一一对应关系,线性地址在逻辑上依旧连续,但对应的物理地址则可以不连续。

由此产生了虚拟内存的映射。

分段与分页

在分段机制中,段基址:偏移地址 所得到的线性地址,直接对应了物理地址,而基于分段的分页机制中,根据是否打开了页表来决定物理地址的获取。

5.2.2 分页机制

分页机制的思想

通过映射的方式,让连续的线性地址,与任意的物理内存地址关联,逻辑上连续的线性地址其对应的物理地址可以是不连续的。

分页机制的作用

  • 将线性地址转换成物理地址

  • 用大小相等的页代替大小不等的段

分页机制的过程

图中三个空间的大小都是 4G,其中物理空间的 4G 为所有进程及操作系统共享,当前进程只可使用未分配的页。而每个程序都有自己的虚拟地址空间。

通常的页大小为 4KB,这样一来 4GB 的地址空间就被划分为 4GB/4KB=1M 个页,因此需要 1M 个页表项。

而在 32 位的虚拟地址中,用高 20 位来定位一个物理页索引,而低 12 位用于在物理页面内 4KB 范围地址偏移。每个页表项 4bytes,高 20 位的索引乘以 4 后便是页表项从页表所在的物理地址偏移量。而整个页表的物理地址存储于 cr3 寄存器中。cr3 的地址加上索引偏移量后得到页表项的物理地址,再从页表项中获得该表项对应的真实物理地址,加上低 12 位中的地址偏移便得到最终要访问的物理地址

即线性地址的高 20 位在页表中索引页表项,低 20 位与页表项中的对应物理地址相加,得到最终线性地址对应的物理地址。地址转换的过程由页部件自动完成。

mov ax, [0x1234] 为例:

段基址:偏移 得到线性地址,0x1234,高 20 位 0x00001,在页表中找到索引 1 的页表项,其对应地址为 0x9000,再加上0x234 偏移,得到最终物理地址 0x9234。

二级页表

内存线性地址有 4GB,每个页表 4KB,因此所有线性地址最多有 4G/4K=1M 个页表项,在一级页表中,这 1M 个页表项全部放在一个页表中,而二级页表则是将 1M 个页表项平均放在了 1K (1024) 个页表中,而这 1K 个页表的物理地址,都以页目录项 (Page Directory Entry, PDE) 的形式,存放于页目录表中,由于每张页表都是 1K 个项,每个项大小 4 字节,因此所有的页表都是一个标准页大小,即 4KB。

即通过页目录表查找到页表,通过页表查找到页表项,通过页表项得到最终物理页地址。

而二级页表的线性地址和物理地址转换方式为,

  • 虚拟地址的高 10 位,用于在页目录表中定位 (索引*4) 一个页目录项 PDE,从而找到对应的页表

  • 虚拟地址的中间 10 位,用于在页表中定位 (索引*4) 具体的页表项 PTE,从而找到对应的物理页地址

  • 虚拟地址的低 12 位,用于在物理页地址中偏移,找到最终实际的物理地址

每个进程都有自己的页表,独占 4G 的虚拟内存空间,当切换进程时,页表也需要跟着切换。

页表项和页目录项

4 字节大小,但只有 20 位用于表示物理地址,这是由于标准页大小 4KB,地址都是 4K 的倍数,因此只需要高 20 位表示即可,剩余的低 12 位为:

  • P - Present,表示该页是否存在于物理内存中

  • RW - Read/Write,读写位,1 表示可读可写,0 表示可读不可写

  • US - User/Supervisor,普通用户/ 超级用户,若为 1 则是 User 级,0/1/2/3 级别特权的程序都可以访问该页。若为 0,表示处于 Supervisor 级,特权级别为 3 的程序不允许访问该页,只有 0/1/2 级别可以

  • PWT - Page-level Write-Through,页级通写位/页级写透位。若为 1 则表示采用通写方式,该页不仅是普通内存,还是高速缓存。通写是高速缓存的工作方式,此处置 0

  • PCD - Page-level Cache Disable,页级高速缓存禁止位。若为 1 则表示启用高速缓存,0 则禁止将该页缓存,此处置 0

  • A - Accessed,访问位,同段的 A 位,访问后由 CPU 置为 1,操作系统定期清 0,并统计为 1 的频率。作为换页的依据。

  • D - Dirty,脏页位,当 CPU 对一个页面执行写操作时,会设置为 1,仅对页表项有效,并不会修改页目录中的 D 位。

  • PAT - Page Attribute Table,页属性表位,在页面级别的粒度上设置内存属性。此处置0。

  • G - Global,全局位,若该页设置为全局页,则该页会一直保留在 TLB 里面。

  • AVL - Available 位,表示对于软件/操作系统可用,CPU 没有限制。

分页机制启用

  1. 准备页目录和页表

  1. 将页表地址写入寄存器 cr3

  1. 寄存器 cr0 的 PG 位置 1

控制寄存器 cr3

cr3 - 页目录基址寄存器 (Page Directory Base Register, PDBR)

页目录的起始地址需要是 4KB 的整数倍,因此前 20 位用来表示其物理地址,低 12 位只有 PCD 和 PWT,其余均没有使用。

内存划分

对于每个用户进程,需要共享同一个操作系统,因此将 4GB 的 0-3GB 划分给用户进程,3-4GB 划分给操作系统。

5.2.3 启用分页机制

内存布局设计

页目录表的起始位置放在物理地址的 0x100000 处,

A. 代码实现

代码路径:./Code/06_loader_page/

  • include/boot.inc

diff --git a/06_loader_page/include/boot.inc b/06_loader_page/include/boot.inc
index 90ac215..f876dd9 100644
--- a/06_loader_page/include/boot.inc
+++ b/06_loader_page/include/boot.inc
@@ -1,6 +1,7 @@
 ; --------- loader and kernel ----------
 LOADER_BASE_ADDR equ 0x900
 LOADER_START_SECTOR equ 0x2
+PAGE_DIR_TABLE_POS equ 0x100000
 
 ; --------- gdt description attribute ----------
 DESC_SEG_BASE3     equ  (0x00 << 24)                    ; Segment base / 24-31, flat mode all segment base address start from 0
@@ -33,3 +34,11 @@ RPL2    equ   10b
 RPL3    equ   11b
 TI_GDT  equ  000b
 TI_LDT  equ  100b
+
+; --------- Page table attribute ----------
+PG_P    equ    1b
+PG_RW_R equ   00b
+PG_RW_W equ   10b
+PG_US_S equ  000b
+PG_US_U equ  100b
+
  • loader.S

diff --git a/06_loader_page/loader.S b/06_loader_page/loader.S
index 1d1468f..c05554e 100644
--- a/06_loader_page/loader.S
+++ b/06_loader_page/loader.S
@@ -120,21 +120,12 @@ mov di, ards_buf     ; ards struct buffer
     jmp $
 
 ok_to_continue:
-;----------- Prepare to enter protect mode -----------
-; 1: open A20 gate (Set 0x92 left second bit to 1)
-; 2: load gdt
-; 3: set cr0 pe to 1 (Set cr0 0 bit to 1)
-
-
 ;----------- Open A20 -----------
 in al,0x92
 or al,0000_0010B
 out 0x92,al
-
 ;----------- Load GDT -----------
-lgdt [gdt_prt]
-
-
+lgdt [gdt_ptr]
 ;----------- Set cr0 pe -----------
 mov eax, cr0
 or eax, 0x00000001
@@ -142,7 +133,6 @@ mov cr0, eax
 
 jmp dword SELECTOR_CODE:p_mode_start  ; Refresh the pipeline, enter protect mode
 
-
 [bits 32]
 p_mode_start:
     mov ax, SELECTOR_DATA
@@ -152,7 +142,88 @@ p_mode_start:
     mov esp, LOADER_STACK_TOP
     mov ax, SELECTOR_VIDEO
     mov gs, ax
-
     mov byte [gs:160], 'P'
+    ; jmp $
 
-    jmp $
+; ============== Paging Ena
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值