当前位置:   article > 正文

0x200-从头开始写操作系统-进入32-Bit Protected Mode

0x200-从头开始写操作系统-进入32-Bit Protected Mode

目录

回顾

上一篇文章,我们讨论了以下内容:

  • Boot Sector 被 BIOS 加载到 0x7c00 的内存位置
  • 用程序证实了 0x7c00 物理内存位置上,确实是我们的 Boot Sector 程序
  • 寄存器分为通用寄存器,指针寄存器,段寄存器以及控制寄存器,我们分别列出了各个寄存器的名称及基本功能
  • 用段寄存器替代 org 指令来完成寻找 Boot Sector 前两个字节内容的任务
  • 一个段中的内存寻址可以通过 [段寄存器:内存偏移量] 来完成
  • 必要的汇编指令,如 jmp,cmp,times,pusha,popa等

代码仓库

今日目标

今天的目标,我们要告别 16-bit Real Mode,进入 32-bit Protected Mode。意味着我们离内核只有一步之遥了。

这篇文章中,我们即将要学习

  • 如何读取磁盘上的数据,为读取内核代码做准备
  • 什么是 GDT
  • 如何在汇编中定义 GDT
  • 如何切换到 32-bit Protected Mode

我将前两篇文章及今天这篇文章中涉及的代码,整理了一下,列在 这里

我们开始吧。

BIOS 读取硬盘数据

硬盘数据的读取,需要在寄存器设置一系列的参数。有一些参数,有关硬盘的工作方式。所以,我们先来简单了解一下硬盘的必要知识。

硬盘

硬盘由盘片,和读写磁头组成。为了扩大容量,几张盘片,重叠在一起,由磁头来读写数据。由于盘片是有两面的,因此,一张盘片就有两个磁头,分别负责读取/写入该磁盘面上的数据。

引用书上的图片为例。

这是典型的机械硬盘的内部构造。

在这里插入图片描述

硬盘的盘片,是可以磁化的,一个比特的数据,磁化即为 1,非磁化即为 0。数据在盘片高速旋转时,由读写头读取和写入。

推荐大家看一下希捷的关于硬盘的视频。里面提到了 1个 bit 的实际物理大小,是 84 纳米(nanometer,也称毫微米)。

硬盘的物理构造,由专门的名词来描述。盘片叠加在一起,盘片上的每一圈,我们称之为磁道(Track),因为硬盘由多个盘片叠加组成,这些磁道,构成如一个圆柱体,我们称之为柱面(Cylinder)。读写装置,我们称之为磁头(Head)。每个盘面,被逻辑分成多个扇区(Sector),每个扇区通常是 512 个字节。

那么,在这样的物理构造下,我们要读取特定一个位置上的数据,就需要 3 个参数来确定。哪一个柱面(磁道),哪一个磁头,哪一个扇区。

这个 3D 坐标被称为 Cylinder-Head-Sector (CHS)地址。更多关于 CHS 的信息,可以阅读这篇 Wiki

  • Cylinder,即柱面,描述的是我们需要读取数据的在第几个磁道。
  • Head,即磁头,描述的是我们需要的数据具体在哪一个盘面。
  • Sector,即扇区,描述的是我们需要的数据在第几个扇区。

应用书上的图片作为例子。将 CHS 地址视觉化。

在这里插入图片描述

读取硬盘数据的参数

如同之前的文章中,我们要调用显示设备,在屏幕上输出字符,就要在 ah 中写入 0x0e,并触发中断。硬盘读写,也需要我们将相应的指令写入到寄存器,来告诉 BIOS 我们要读取的数据的位置和长度。

硬盘与与 CPU 有多种不同的连接总线,如 ATA/IDE,SATA,SCSI,USB。BIOS 为这些常见的设备提供了统一的指令。

我们将这些指令(包括寄存器中的参数和中断)列举在下面:

  • AH 0x02 ; BIOS 读取磁盘扇区的模式(原书中写成了 al,有误)
  • AL 0x5 ; 读取的扇区数(1 - 128)
  • CH 0x3 ; 磁道/柱面 (0 - 1023)
  • CL 0x4 ; 扇区(1-63)
  • DH 0x1 ; 磁头(0 - 255)
  • DL 0x0 ; 存储介质 (0 => 1 号软驱;1 => 2 号软驱;0x80 => 第 1 块硬盘;0x81 => 第 2 块硬盘)
  • ES:BX ; 磁盘数据将被读取并写入到这个内存地址
  • INT 0x13 ; 触发中断读取指定位置上指定长度的数据,并写入到内存的指定位置

读取操作完成之后,CPU 会设置几个返回值到寄存器,说明读取操作是否成功,我们可以做错误处理:

更多关于每个参数的索引起始,以及索引范围的信息,可以阅读这篇 Wiki


硬盘参数小结:

  • AX 寄存器中,高位 AH 部分存储读取模式,低位 AL 部分存储要读取的扇区数。例:AH = 0x02,AL = 0x05,读取模式,读取 5 个扇区
  • BX 寄存器中,存储的是读取到的数据要被加载到内存地址的内存地址偏移量。例:BX = 0x8000,若 ES 为 0x0,读取的数据就被加载到 0x8000 的内存地址上
  • CX 寄存器中,高位 CH 部分存储柱面信息,低位 CL 部分存储要读取第几扇区的数据。例:CH = 0x03,CL = 0x02,读取第 3 柱面,第 2 扇区
  • DX 寄存器中,高位 DH 部分存储磁头信息,低位 DL 部分存储要读取第几块软驱或者硬盘的数据。例:DH = 0x01,DL = 0x0 读取第二号磁头,读取第一块存储介质

调用中断读取硬盘数据

记得上一篇中,我们给出的 boot sector 在内存中的位置,我们将选取 0x8000 作为加载我们磁盘数据的内存地址。它在我们的 boot sector 之后的空闲空间里。

在这里插入图片描述

我们将读取两个扇区的测试数据。

来看代码,磁盘读取的参数设置,在 read_from_disk.asm 中。

read_from_disk.asm

read_from_disk:
    pusha

checking
    push bx ; 之后打印出测试数据被加载到内存的位置
    push dx

    ; 各个参数
    mov ah, 0x02 ; 读取模式
    mov al, dh ; 读取两个扇区的数据 (dead...beef...)
    mov ch, 0x00 ; 从第 1 柱面开始读
    mov cl, 0x02 ; 从第 2 扇区开始读 (第 1 个扇区是 boot sector)
    mov dh, 0x00 ; 从第 1 磁头开始读
    ;mov dl, 0x00 ; 从第1 块存储介质开始读

    int 0x013 ; 触发中断

    mov bx, READ_START
    call print
    call print_nl

    jc op_error ; 如果操作失败, CF(Carry Bit) 寄存器会被设置为 1, 如果 CF 寄存器被设置为 1,jc 就会跳转
    
    pop dx
    cmp al, dh
    jne read_error

    mov bx, READ_COMPLETE
    call print 

    ; 打印出测试数据被加载到哪里
    pop bx
    mov dx, bx
    call print_hex
    call print_nl

    popa
    ret

op_error:
    ; 错误信息
    mov bx, OP_ERROR
    call print
    call print_nl

    ; 如果有错误发生,我们打印出错误码信息
    mov dh, ah
    call print_hex
    ; just hang the cpu on error
    jmp disk_loop

read_error:
    mov bx, READ_ERROR
    call print
    jmp disk_loop

disk_loop:
    jmp $

READ_START: db "Reading start...", 0
READ_COMPLETE: db "Reading complete, data loaded to ", 0
OP_ERROR: db "Disk read error...", 0
READ_ERROR: db "Incorrect number of sectors read...", 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

print_hex.asm

print_hex:
    pusha

    mov cx, 0 ; our indbx variable

; Strategy: get the last char of 'dx', then convert to ASCII
; Numeric ASCII values: '0' (ASCII 0x30) to '9' (0x39), so just add 0x30 to byte N.
; For alphabetic characters A-F: 'A' (ASCII 0x41) to 'F' (0x46) we'll add 0x40
; Then, move the ASCII byte to the correct position on the resulting string
hex_loop:
    cmp cx, 4 ; loop 4 times
    je end
    
    ; 1. convert last char of 'dx' to ascii
    mov ax, dx ; we will use 'ax' as our working register
    and ax, 0x000f ; 0x1234 -> 0x0004 by masking first three to zeros
    add al, 0x30 ; add 0x30 to N to convert it to ASCII "N"
    cmp al, 0x39 ; if > 9, add extra 7 to represent 'A' to 'F'
    jle step2
    add al, 7 ; 'A' is ASCII 65 instead of 58, so 65-58=7

step2:
    ; 2. get the correct position of the string to place our ASCII char
    ; bx <- base address + string length - indbx of char
    mov bx, HEX_OUT + 5 ; base + length, starts last last char of HEX_OUT
    sub bx, cx  ; our indbx variable
    mov [bx], al ; copy the ASCII char on 'al' to the position pointed by 'bx'
    ror dx, 4 ; 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234

    ; increment indbx and loop
    add cx, 1
    jmp hex_loop

end:
    ; prepare the parameter and call the function
    ; remember that print receives parameters in 'bx'
    mov bx, HEX_OUT
    call print

    popa
    ret

HEX_OUT:
    db '0x0000',0 ; reserve memory for our new string
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

print.asm

print:
    pusha

start:
    mov al, [bx] ; 'bx' is the base address for the string
    cmp al, 0 
    je done

    mov ah, 0x0e
    int 0x10 ; 'al' already contains the char

    add bx, 1 ; print next char
    jmp start

done:
    popa
    ret

; print new line
print_nl:
    pusha
    
    mov ah, 0x0e
    mov al, 0x0a ; newline char
    int 0x10
    mov al, 0x0d ; carriage return
    int 0x10
    
    popa
    ret
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

boot_sect_main.asm

[org 0x7c00]

; 我们将利用栈保存一些寄存器的值,所以将栈的内存位置设置在空闲区域
mov ax, 0x8000
mov bp, ax
mov sp, bp
; 测试数据会被加载到 [ES:BX] => 0x8000
mov ax, 0x0
mov es, ax
mov bx, 0x9000

; 读取两个扇区的数据,dh 这里用来传递 0x2 这个数据
mov dh, 0x2

; 开始读取
call read_from_disk

; 打印第 2 扇区第一个字  => 0xdead
mov dx, [es:bx]
call print_hex
; 打印第 3 个扇区第一个字  => 0xbeef
mov dx, [es:bx + 512]
call print_hex

jmp $

%include "print.asm"
%include "print_hex.asm"
%include "read_from_disk.asm"

times 510 - ($ - $$) db 0x0
dw 0xaa55

; 写入 512 个字节到第 2 扇区(第 1 扇区是 boot sector)
times 256 dw 0xdead
; 写入 512 个字节到第 3 扇区
times 256 dw 0xbeef
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

在 boot_sect_main.asm 中,我们分别读取了第二和第三个扇区的前两个字节,可以看到结果如下。

在这里插入图片描述

我们可以用 od 命令查看 bin 文件中的内容,看到紧接着我们的 boot sector,写入了我们的测试数据。

在这里插入图片描述

大家可以尝试修改代码,加载测试数据到不同的内存地址,观察程序的变化。

现在,我们已经具备加载内核的能力。下面,我们告别 16-bit Real Mode,开启 32-bit Protected Mode。

32-Bit Protected Mode

16-bit Real Mode 以下称 16 位模式, 32-Bit Protected Mode 以下称 32 位模式。

首先,经过前两篇文章的学习,我们已经很熟悉 16 位模式了。现在,我们要思考一下为什么还需要切换到 32 位模式,它和 16 位模式有什么区别。

接着,我们要学习 32 位模式中最重要的概念,全局描述符(Global Descriptor Table)。

最后,我们学习怎么在汇编中定义 GDT,并切换到 32 位模式。

关于 32-Bit Protected Mode

在切换操作之前,我们必须先了解一下 32 位模式。

什么是 32-bit Protected Mode?

Protected Mode,保护模式,是自 80286 以来的现代 CPU 的主要工作模式。

32 位模式加入了虚拟内存的概念,并且加强了内存读写保护,提供了通过 Rings 限制可用指令的能力。

总而言之,32 位模式向着更加高级,更加安全的方向发展,为现代操作系统提供了一个更好的运行环境。

为什么我们需要 32-bit Protected Mode?

我们从 16 位模式切换到 32 位模式,有两个最主要的目的。

  • 第一,为了完全释放 CPU 的能力
  • 第二,为了更好地理解硬件的内存保护机制

我们不能容忍那可怜的 1MB 内存,不能容忍我们程序的内存毫无保护,所以,32 位模式势在必行。

32-bit Protected Mode vs 16-bit Real Mode

到了 32 位模式之后所发生的变化总结如下:

  • 寄存器扩展到了 32-bit,之后,寄存器的使用都要加上 e,意思是 extended,例如:mov eax, 0x80808080
  • 通用寄存器增加了两个,FS 和 GS
  • 内存分段的技术更加高效,同时也更加复杂
    • 我们可以防止一个段中的代码被执行
    • CPU 支持虚拟内存和分页,用户程序将会以分页的形式在磁盘和内存之间进行切换(swapping)
    • 中断的处理也更加的高级

32 位模式下的字符打印

在继续下面的内容之前,我们必须先做一点代码上的调整。能够打印字符对于程序的调试是很重要的,所以,我们现在要将 16 位模式下的打印字符的代码,调整到 32 位可用。在调整代码之前,我们需要先对 32 位模式下的底层调用有所了解,才能顺利在 32 位模式下打印字符。

告别 BIOS

BIOS 下的中断和系统调用,是专门为 16 位模式设计的,因此,在 32 位模式下不可用。书中提到,有办法可以暂时切换回 16 位模式去使用 BIOS 的系统调用,但是这没有意义,十分复杂,也违背我们要切换到 32 位的初衷。

那么,我们必须丢弃 BIOS,重新调整我们的思路去适应 32 位模式。

32 位模式下的显示设备调用

这里要说明的是 32 位模式下,关于显示设备调用需要理解的一些概念。

Memory-Mapped Device(Memory-Mapped I/O)

计算机的外围设备,分为 Memory-Mapped(内存映射) 和 Port-Mapped(端口映射)两种。我们这里讨论的显示设备,是 Memory-Mapped Device(暂译为内存映射设备)的一种。

计算机外围设备,都以某种方式连接至 CPU,他们都与 CPU 有输入输出的操作。因此,外围设备的输入输出操作被统称为 Memory/Port-Mapped I/O

内存映射设备使用同一内存空间来记录数据内存地址与设备内存地址。这是我总结的,原文是 “Memory-mapped I/O uses the same address space to address both memory and I/O devices.”。可以这样理解, CPU 访问设备内存上的数据时,其实就是在访问设备本身。有这样特征的设备,就被称为 Memory-Mapped Device。

例如显示设备,我们只需要往设备内存中写入数据,就可以在屏幕上展示这些数据。

所以,接下来要讲到的在 32 位模式下打印字符,我们只需要 CPU 去访问特定的显示设备内存(Video Memory),即可完成显示设备调用,打印字符到屏幕。

VGA 模式(Video Graphics Array)

显示设备有两种模式可以设置:

  • 文本模式(text mode)
  • 图形模式(graphics mode)

在计算机启动的时候,无论计算机上有多么高级的显示设备(RTX 2080Ti

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/算法编织者/article/detail/63217
推荐阅读
相关标签