加载 bootloader & 进入保护模式

Posted by lzzmm on March 30, 2021
About 15 minutes to read

DCS218 - Operating Systems Lab 2021 Spring

前言

本文章介绍了 LBA 和 C/H/S 读取磁盘加载 bootloader,进入保护模式并执行自定义的汇编程序。

1. bootloader 的加载

1.1 LBA方式读取磁盘加载 bootloader

  1. 原理:

    1. mbr 启动后使用 mbr 中的代码加载 bootloader,此时 bootloader 中的代码长度就不会被限制在 512B 了。注意,MBR 在内存中从 0x7C000x7E00 ,bootloader 从 0x7E00 0x8800
    2. 硬盘是外围设备的一种,处理器和外围设备的交换是通过I/O端口进行的。实际上,I/O端口是一些寄存器,位于I/O接口电路中。当我们需要和外围设备进行数据交换时,我们首先需要将命令和数据放入到指定的I/O端口,待外围设备处理完成后,我们再从指定的I/O端口取出外围设备的处理结果。同时,当外围设备正在处理时,我们也可以从指定的端口获取外围设备的状态。每一个端口在I/O电路中都会被统一编址。例如,主硬盘分配的端口地址是 0x1f0~0x1f7,从硬盘分配的端口地址是 0x170~0x177,我们的实验只会用到主硬盘。因为端口是独立编址的,因此我们无法使用 mov 指令来对端口赋值,我们使用的是 in,out 指令。读端口使用in指令,写端口使用out指令,in/out 指令的操作数有严格规定,in 指令的源操作数只能是立即数或dx,目的操作数只能是 axalout 指令的源操作数只能是 alax,目的操作数只能是立即数或 dx
    3. 我使用 LBA 方式读取硬盘。首先,要设置起始的逻辑扇区号。使用的是LBA28(28表示使用28位来表示逻辑扇区的编号)的方式读取硬盘,其中,逻辑扇区的 0~7 位被写入 0x1F3 端口,8~15位被写入 0x1F4 端口,16~23位被写入 0x1F5 端口,最后4位被写入 0x1F6 端口的低4位。
    4. 接着,要把想读取的扇区的数量写入 0x1F2 端口。
    5. 然后,向 0x1F7 端口写入 0x20 ,请求硬盘读。此时硬盘可能在处理其他操作。因此我们需要等待其他读写操作完成后才能开始本次读写操作。硬盘的状态可以从 0x1F7 读入,可以读到8个状态——其他操作完成的标志是第7位为0,第3位为1,第0位为0。
    6. 等硬盘就绪,我们 0x1F0 中连续读入一个扇区的数据。0x1F0 是硬盘接口的数据端口,16位。
  2. 首先创建 mbr.asm ,在其中写入读取bootloader的代码,具体如下:

    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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    
     org 0x7c00
     [bits 16]
     xor ax, ax ; eax = 0
     ; 初始化段寄存器, 段地址全部设为0
     mov ds, ax
     mov ss, ax
     mov es, ax
     mov fs, ax
     mov gs, ax
        
     ; 初始化栈指针
     mov sp, 0x7c00
     mov ax, 1                 ; 逻辑扇区号第0~15
     mov cx, 0                 ; 逻辑扇区号第16~31
     mov bx, 0x7e00            ; bootloader的加载地址
     load_bootloader:          ; 加载一共5个扇区的bootloader
         call asm_read_hard_disk  ; 读取硬盘
         inc ax
         cmp ax, 5注意,MBR 在内存中从0x7C00  0x7E00 bootloader  0x7E00  0x8800 .
         jle load_bootloader
     jmp 0x0000:0x7e00        ; 跳转到bootloader
        
     jmp $ ; 死循环
        
     asm_read_hard_disk:                             
     ; 从硬盘读取一个逻辑扇区
        
     ; 参数列表
     ; ax=逻辑扇区号0~15
     ; cx=逻辑扇区号16~28
     ; ds:bx=读取出的数据放入地址
        
     ; 返回值
     ; bx=bx+512
        
         mov dx, 0x1f3
         out dx, al    ; LBA地址7~0
        
         inc dx        ; 0x1f4
         mov al, ah
         out dx, al    ; LBA地址15~8
        
         mov ax, cx
        
         inc dx        ; 0x1f5
         out dx, al    ; LBA地址23~16
        
         inc dx        ; 0x1f6
         mov al, ah
         and al, 0x0f
         or al, 0xe0    ; LBA地址27~24
         out dx, al
        
         mov dx, 0x1f2
         mov al, 1
         out dx, al    ; 读取1个扇区
        
         mov dx, 0x1f7    ; 0x1f7
         mov al, 0x20     ;读命令
         out dx,al
        
         ; 等待处理其他操作
      .waits:
         in al, dx        ; dx = 0x1f7
         and al,0x88
         cmp al,0x08
         jnz .waits                           
            
        
         ; 读取512字节到地址ds:bx
         mov cx, 256    ; 每次读取一个字,2个字节,因此读取256次即可           
         mov dx, 0x1f0
      .readw:
         in ax, dx
         mov [bx], ax
         add bx, 2
         loop .readw
        
         ret
        
     times 510 - ($ - $$) db 0
     db 0x55, 0xaa
    
  3. 然后创建 bootloader.asm ,写入 bootloader 代码。这些代码会显示 run bootloader 19335025_CYH :

    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
    
     org 0x7e00
     [bits 16]
     mov ax, 0xb800
     mov gs, ax
     mov ah, 0x03 ;青色
     ; mov al, '1'
     ; mov [gs:2 * 4], ax
     ; mov al, '9'
     ; mov [gs:2 * 5], ax
     ; mov al, '3'
     ; mov [gs:2 * 6], ax
     ; mov al, '3'
     ; mov [gs:2 * 7], ax
     ; mov al, '5'
     ; mov [gs:2 * 8], ax
     ; mov al, '0'
     ; mov [gs:2 * 9], ax
     ; mov al, '2'
     ; mov [gs:2 * 10], ax
     ; mov al, '5'
     ; mov [gs:2 * 11], ax
        
     mov ecx, bootloader_tag_end - bootloader_tag
     xor ebx, ebx
     mov esi, bootloader_tag
     output_bootloader_tag:
         mov al, [esi]
         mov word[gs:bx], ax
         inc esi
         add ebx,2
         loop output_bootloader_tag
     jmp $ ; 死循环
        
     bootloader_tag db 'run bootloader 19335025_CYH'
     bootloader_tag_end:
    
  4. 编译两个汇编文件,写入硬盘相应的位置。

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     chen@chen-virtual-machine:~/os2021/lab3/assignment1$ nasm -f bin bootloader.asm -o bootloader.bin
     chen@chen-virtual-machine:~/os2021/lab3/assignment1$ dd if=bootloader.bin of=hd.img bs=512 count=5 seek=1 conv=notrunc
     记录了0+1 的读入
     记录了0+1 的写出
     52字节已复制,0.00551219 s,9.4 kB/s
     chen@chen-virtual-machine:~/os2021/lab3/assignment1$ nasm -f bin mbr.asm -o mbr.bin
     chen@chen-virtual-machine:~/os2021/lab3/assignment1$ dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
     记录了1+0 的读入
     记录了1+0 的写出
     512字节已复制,0.000403622 s,1.3 MB/s
    
  5. 使用 qemu 运行

    0
    
     qemu-system-i386 -hda hd.img -serial null -parallel stdio 
    
  6. 结果如下

    image-20220329005237159

1.2 C/H/S方式读取磁盘加载bootloader

  1. 原理:

    1. 有一个中断:直接磁盘服务(Direct Disk Service—— INT 13H)

    2. 功能 02H

      功能描述:读扇区

      入口参数:AH=02H

      AL=扇区数

      CH=柱面

      CL=扇区

      DH=磁头

      DL=驱动器,00H~7FH:软盘;80H~0FFH:硬盘

      ES:BX=缓冲区的地址

    3. 出口参数:CF=0——操作成功,AH00HAL=传输的扇区数,否则,AH=状态代码,参见功能号 01H 中的说明

    4. LBA 和 CHS 转换: LBA = (cylinder * HPC + head) * SPT + sector - 1

    5. 因此我们的 bootloader 应该在 0C 0H 2-6S

  2. 修改 mbr.asm ,从 LBA 到 CHS,新增函数如下:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    
     asm_read_hard_disk_chs: 
         mov cl, 2        ; 扇区
         mov al, 05h     ; 扇区数
         mov ah, 02h     ; 功能参数,读取扇区
         mov ch, 00h     ; 柱面
         mov dh, 00h     ; 磁头
         mov dl, 80h     ; hd
         int 0x13           
     ret
    

    修改 load_botloader 函数

    0
    1
    2
    3
    4
    5
    6
    7
    8
    
     load_bootloader:          ; 加载1-5扇区的bootloader
         call asm_read_hard_disk_chs  ; call asm_read_hard_disk 读取硬盘
         ; inc cx
         ; cmp cx, 5
         ; jle load_bootloader
         ; add bx, 0x200
     jmp 0x0000:0x7e00        ; 跳转到bootloader
        
     jmp $ ; 死循环
    
  3. 结果如下

    image-20220329005250308

2. 进入保护模式

  1. 原理:

    1. 段(segment)实际上是程序员人为划分的一块块连续的内存区域,或者称为地址空间。保护模式,顾名思义,就是 CPU 会提供保护的机制。在保护模式下,所有的程序都会运行在自己的段中,一旦程序错误地访问其他段的地址空间,那么 CPU 就会产生异常(exception, 注意和中断 interrupt 的区别)来阻止程序访问。因此,在这里,我们可以简单理解保护模式保护的是段地址空间,阻止程序越界访问,因此也被称为段保护。CPU 需要知道当前运行中程序的段地址空间信息,然后才能执行地址保护,阻止程序越界访问。段地址空间信息是通过段描述符(segment descriptor)来给出的。

    2. 段描述符中包含了段基地址(段的起始地址)、段界限(段的长度)等,共计64字节 段描述符

      • 段基地址。段基地址共32位,是段的起始地址,被拆分成三部分放置。

      • G位。G表示粒度, G=0表示段界限以字节为单位, G=1表示段界限以4KB为单位。

      • D/B位。D/B位是默认操作数的大小或默认堆栈指针的大小,在保护模式下,该位置为1,表示32位。

      • L位。L位是 64 位代码段标志,由于这里我们使用的是32位的代码,所以L置0。

      • AVL。AVL位是保留位。

      • 段界限。段界限表示段的偏移地址范围,我们在后面详细讨论这个问题。

      • P位。P位是段存在位, P=1表示段存在, P=0表示段不存在。

      • DPL。DPL指明访问该段必须有的最低优先级,优先级从0-3依次降低,即0拥有最高优先级,3拥有最低优先级。

      • S位。S位是描述符类型。S=0表示该段是系统段,S=1表示该段位代码段或数据段。

      • TYPE。TYPE指示代码段或数据段的类型,如下所示。

      第11位(X) 第10位(E) 第9位(W) 第8位(A) 含义
      0 0 0 * 只读,向上扩展
      0 0 1 * 读写,向上扩展
      0 1 0 * 只读,向下扩展
      0 1 1 * 读写,向下扩展
      1 0 0 * 只执行,非一致代码段
      1 0 1 * 执行、可读,非一致代码段
      1 1 0 * 只执行,一致代码段
      1 1 1 * 执行、可读、一致代码段

      A位表示是否被使用过,A=1表示使用,A=0表示未被使用,由CPU负责设置,因此我们不关心。一致代码段和非一致代码段的问题比较复杂,这里先将E位置为0。

      向上扩展和向下扩展指的是段的线性基地址和段的线性尾地址的大小关系。例如,对于数据段,尾地址是大于基地址的,因此是向上扩展;对于栈段,栈是从高地址向低地址增长的,因此尾地址是小于基地址的,是向下扩展。CPU 通过段描述符中的段线性基地址和段界限来执行段保护,如下所示,假设寻址的地址是 offset,读出或写入的长度为 length。

    3. 保护模式下的寻址依然通过段地址和偏移地址的方式来寻址,此时线性地址 = 段地址 + 偏移地址,表示为“选择子:偏移地址”。在保护模式下,CPU 先通过选择子在描述符表中找到段描述符,然后得到段线性基地址,最后将段线性基地址加上偏移地址便得到线性地址。

    4. 80286 及以后的 CPU 首先进入实模式,然后通过切换机制再进入到保护模式。也就是说,在 BIOS 加电启动后,我们需要在实模式下的 MBR 中编写16位进入保护模式的代码,然后再跳转到保护模式,执行接下来的32位代码。进入保护模式的步骤如下。

      1. 准备GDT,用lgdt指令加载GDTR信息。GDT 实际上是一个段描述符数组,保存在内存中。GDT 的起始位置和大小由我们来确定,保存在寄存器 GDTR 中。全局描述符边界实际上是GDT的界限,和上面的段界限相同。段描述符的数量是有限的,段描述符最大数量=2^16/8=8192,每一个段描述符是64位,因此是8个字节。当我们确定好 GDTR 的内容后,我们就使用 lgdt 指令将 GDTR 的内容送入 GDTR。当我们在指令中显示给出段选择子时,CPU 会根据 GDTR 的内容找到 GDT 在内存中的位置,然后从 GDT 中取出对应的段描述符。
      2. 打开第21根地址线。在实模式下,第21根地址线的值恒为0,使得当访问越界超过 1MB 时,自然溢出使得地址的值仍然小于 1MB,即相当于取模 1MB。所以,当我们想进入保护模式时,首先需要打开第 21 根地址线。
      3. 开启cr0的保护模式标志位CR0 是 32 位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位,其第0位是保护模式的开关位,称为 PE(protect mode enable)位。 PE 置1,CPU 进入保护模式。
      4. 远跳转,进入保护模式。段寄存器的使用保护模式与实模式不同,我们需要将代码段的选择子放入段寄存器 cs 才可以正确执行保护模式的代码,cs 无法直接使用 mov 指令修改,我们需要借助于 jmp 指令。执行完 jmp 指令后,我们便正式进入了保护模式。
  2. 首先对内存地址进行规划:

    项目 长度 起始 终止
    MBR 0x200 (512B) 0x7C00 0x7E00
    Bootloader 0xA00 (512B*5) 0x7E00 0x8800
    GDT 0x80 (8B*16) 0x8800 0x8880

    将上述常量定义在一个独立的文件 boot.inc 中,如下所示:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     ; 常量定义区
     ; _____________Loader_____________
     ; 加载器扇区数
     LOADER_SECTOR_COUNT equ 5
     ; 加载器起始扇区
     LOADER_START_SECTOR equ 1
     ; 加载器被加载地址
     LOADER_START_ADDRESS equ 0x7e00
     ; _____________GDT_____________
     ; GDT起始位置
     GDT_START_ADDRESS equ 0x8800
    

    其中,equ 是汇编伪指令。例如,编译器会在编译时将 LOADER_SECTOR_COUNT 出现的地方替换成 5LOADER_SECTOR_COUNT equ 5 不会对应任何的二进制指令,即不会出现在最终的bin格式文件中。

  3. 创建 bootloader.asm 。要在 bootloader 中跳转到保护模式,首先,我们需要定义段描述符,包括代码段描述符、数据段描述符、栈段描述符和视频段描述符。GDT 的第0个描述符必须是全0的描述符。接着,我们在 GDT 中依次放入0描述符,数据段描述符、堆栈段描述符、显存段描述符和代码段描述符,代码如下。

    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
    
     %include "boot.inc"
     org 0x7e00
     [bits 16]
     mov ax, 0xb800
     mov gs, ax
     mov ah, 0x03 ;青色
     mov ecx, bootloader_tag_end - bootloader_tag
     xor ebx, ebx
     mov esi, bootloader_tag
     output_bootloader_tag:
         mov al, [esi]
         mov word[gs:bx], ax
         inc esi
         add ebx,2
         loop output_bootloader_tag
        
     ;空描述符
     mov dword [GDT_START_ADDRESS+0x00],0x00
     mov dword [GDT_START_ADDRESS+0x04],0x00  
        
     ;创建描述符,这是一个数据段,对应0~4GB的线性地址空间
     mov dword [GDT_START_ADDRESS+0x08],0x0000ffff    ; 基地址为0,段界限为0xFFFFF
     mov dword [GDT_START_ADDRESS+0x0c],0x00cf9200    ; 粒度为4KB,存储器段描述符 
        
     ;建立保护模式下的堆栈段描述符       
     mov dword [GDT_START_ADDRESS+0x10],0x00000000    ; 基地址为0x00000000,界限0x0 
     mov dword [GDT_START_ADDRESS+0x14],0x00409600    ; 粒度为1个字节
        
     ;建立保护模式下的显存描述符    
     mov dword [GDT_START_ADDRESS+0x18],0x80007fff    ; 基地址为0x000B8000,界限0x07FFF 
     mov dword [GDT_START_ADDRESS+0x1c],0x0040920b    ; 粒度为字节
        
     ;创建保护模式下平坦模式代码段描述符
     mov dword [GDT_START_ADDRESS+0x20],0x0000ffff    ; 基地址为0,段界限为0xFFFFF
     mov dword [GDT_START_ADDRESS+0x24],0x00cf9800    ; 粒度为4kb,代码段描述符 
    
  4. 为了让 CPU 知道 GDT 的位置,我们需要设置GDTR寄存器。回忆一下 GDTR 寄存器,其高32位表示 GDT 的起始地址,低16位表示 GDT 的界限。所谓界限,就是现在 GDT 的长度减去1。此时,我们已经放入5个段描述符,因此,GDT 的界限=8∗5−1=39。

    我们在内存中使用一个48位的变量来表示GDTR的内容。

    0
    1
    
     pgdt dw 0 
         dd GDT_START_ADDRESS
    

    然后把GDT的信息写入变量 pgdt ,把 pgdt 的内容加载进GDTR。

    0
    1
    2
    
     ;初始化描述符表寄存器GDTR
     mov word [pgdt], 39       ;描述符表的界限    
     lgdt [pgdt]
    

    然后根据段描述符的内容设置段选择子。

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     ; _____________Selector_____________
     ;平坦模式数据段选择子
     DATA_SELECTOR equ 0x8
     ;平坦模式栈段选择子
     STACK_SELECTOR equ 0x10
     ;平坦模式视频段选择子
     VIDEO_SELECTOR equ 0x18
     VIDEO_NUM equ 0x18
     ;平坦模式代码段选择子
     CODE_SELECTOR equ 0x20
    

    以代码段选择子为例来解释段选择子的含义,段选择子的结构如下。

    • 代码段描述符是GDT中第4个描述符,因此高13位为4。
    • TI=0 表示 GDT,第2位为0。
    • 我们的特权级设为最高特权级,因此RPL也就是低2位为0。
    • 因此代码段选择子为 0x20

    此时,GDT已经设置完毕,我们现在结合段描述符的结构研究下代码段的内存和具体含义,段描述符如下所示。

    段描述符

    对于代码段描述符,描述符高32位为 0x00cf9800,低32位为 0x0000ffff,因此各个部分含义如下。

    • 段线性基地址。段线性基地址由三部分组成,但都是0,因此段线性基地址为0。
    • G=1,表示段界限以4KB为单位。
    • D/B=1,表示操作数大小为32位。
    • L=0,表示32位代码。
    • AVL,保留位,不关心,置0即可。

    段界限由两个部分组成,值为 0xfffff,共20位。结合粒度和段界限,整个代码段的长度计算如下。 \(长度=(段界限+1)*粒度=(\text{0xfffff}+1)*4KB=2^{20}*2^{12}B=2^{32}B=4GB\)

    因此整个代码段表示的范围是 0x00000000~0xffffffff。由于基地址为0,偏移地址直接表示线性地址,因此也被称为平坦模式。

    • P=1,表示段存在。
    • DPL=0,表示最高优先级。
    • S=1,表示代码段。
    • TYPE=0x8,表示只执行,非一致代码段。

    其他段描述符的分析方法类似。注意,数据段和栈段的寻址空间都是 0x00000000~0xffffffff,线性地址也都是由偏移地址直接给出,非常方便。视频段并不是平坦模式,而是仅限于显存的表示范围。

  5. 打开 第21根地址线

    0
    1
    2
    
     in al,0x92                           ;南桥芯片内的端口 
     or al,0000_0010B
     out 0x92,al                          ;打开A20
    
  6. 设置PE位。

    0
    1
    2
    3
    
     cli                                  ;中断机制尚未工作
     mov eax,cr0
     or eax,1
     mov cr0,eax                          ;设置PE
    
  7. 远跳转进入保护模式。

    0
    
     jmp dword CODE_SELECTOR:protect_mode_begin
    

    此时,jmp 指令将 CODE_SELECTOR 送入 cs,将 protect_mode_begin + LOADER_START_ADDRESS 送入 eip,进入保护模式。

  8. 将选择子放入对应的段寄存器。

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     ;16位的描述符选择子:32位偏移
     ;清流水线并串行化处理器
     [bits 32]            
     protect_mode_begin:                                
        
     mov eax, DATA_SELECTOR                       ;加载数据段(0..4GB)选择子
     mov ds, eax
     mov es, eax
     mov eax, STACK_SELECTOR
     mov ss, eax
     mov eax, VIDEO_SELECTOR
     mov gs, eax
    
  9. 输出 “enter protect mode 19335025_CYH”

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
     mov ecx, protect_mode_tag_end - protect_mode_tag
     mov ebx, 80 * 2
     mov esi, protect_mode_tag
     mov ah, 0x3
     output_protect_mode_tag:
         mov al, [esi]
         mov word[gs:ebx], ax
         add ebx, 2
         inc esi
         loop output_protect_mode_tag
        
     jmp $ ; 死循环
        
     pgdt dw 0
         dd GDT_START_ADDRESS
        
     bootloader_tag db 'run bootloader 19335025_CYH'
     bootloader_tag_end:
        
     protect_mode_tag db 'enter protect mode 19335025_CYH'
     protect_mode_tag_end:
    
  10. 修改 mbr.asm

    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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    
    %include "boot.inc"
        
    [bits 16]
    xor ax, ax ; eax = 0
    ; 初始化段寄存器, 段地址全部设为0
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
        
    ; 初始化栈指针
    mov sp, 0x7c00    
        
    mov ax, LOADER_START_SECTOR
    mov cx, LOADER_SECTOR_COUNT
    mov bx, LOADER_START_ADDRESS    
        
    load_bootloader: 
        push ax
        push bx
        call asm_read_hard_disk  ; 读取硬盘
        add sp, 4
        inc ax
        add bx, 512
        loop load_bootloader
        
        jmp 0x0000:0x7e00        ; 跳转到bootloader
        
    jmp $ ; 死循环
        
    ; asm_read_hard_disk(memory,block)
    ; 加载逻辑扇区号为block的扇区到内存地址memory
        
    asm_read_hard_disk:                             
        push bp
        mov bp, sp
        
        push ax
        push bx
        push cx
        push dx
        
        mov ax, [bp + 2 * 3] ; 逻辑扇区低16
        
        mov dx, 0x1f3
        out dx, al    ; LBA地址7~0
        
        inc dx        ; 0x1f4
        mov al, ah
        out dx, al    ; LBA地址15~8
        
        xor ax, ax
        inc dx        ; 0x1f5
        out dx, al    ; LBA地址23~16 = 0
        
        inc dx        ; 0x1f6
        mov al, ah
        and al, 0x0f
        or al, 0xe0    ; LBA地址27~24 = 0
        out dx, al
        
        mov dx, 0x1f2
        mov al, 1
        out dx, al    ; 读取1个扇区
        
        mov dx, 0x1f7    ; 0x1f7
        mov al, 0x20     ;读命令
        out dx,al
        
        ; 等待处理其他操作
       .waits:
        in al, dx        ; dx = 0x1f7
        and al,0x88
        cmp al,0x08
        jnz .waits                           
            
        
        ; 读取512字节到地址ds:bx
        mov bx, [bp + 2 * 2]
        mov cx, 256    ; 每次读取一个字,2个字节,因此读取256次即可           
        mov dx, 0x1f0
       .readw:
        in ax, dx
        mov [bx], ax
        add bx, 2
        loop .readw
               
        pop dx
        pop cx
        pop bx
        pop ax
        pop bp
        
        ret
        
    times 510 - ($ - $$) db 0
    db 0x55, 0xaa
    
  11. 编写 make 文件

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    run:
        @qemu-system-i386 -hda hd.img -serial null -parallel stdio 
    debug:
        @qemu-system-i386 -s -S -hda hd.img -serial null -parallel stdio &
        @sleep 1
        @gnome-terminal -e "gdb -q -x gdbinit"
    build:
        @nasm -g -f elf32 mbr.asm -o mbr.o
        @ld -o mbr.symbol -melf_i386 -N mbr.o -Ttext 0x7c00
        @ld -o mbr.bin -melf_i386 -N mbr.o -Ttext 0x7c00 --oformat binary
        
        @nasm -g -f elf32 bootloader.asm -o bootloader.o
        @ld -o bootloader.symbol -melf_i386 -N bootloader.o -Ttext 0x7e00
        @ld -o bootloader.bin -melf_i386 -N bootloader.o -Ttext 0x7e00 --oformat binary
        
        @dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
        @dd if=bootloader.bin of=hd.img bs=512 count=5 seek=1 conv=notrunc
    clean:
        @rm -fr *.bin *.o *.symbol
        
    
  12. 把 gdb 的初始化命令写入文件 gdbinit 中,如下所示。

    0
    1
    2
    3
    
    target remote:1234
    set disassembly-flavor intel
    add-symbol-file mbr.symbol 0x7c00
    add-symbol-file bootloader.symbol 0x7c00
    
  13. 调试运行,结果如下

    image-20220329005331091

    image-20220329005339401

    image-20220329005344803

    image-20220329005350676

    image-20220329005356953

    image-20220329005402573

    image-20220329005408971

3. 保护模式执行自定义的汇编程序

修改 Lab2-Assignment 4 , 放入 bootloader.asm 如下

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
%include "boot.inc"
; org 0x7e00
[bits 16]


mov dword [GDT_START_ADDRESS+0x00],0x00
mov dword [GDT_START_ADDRESS+0x04],0x00  

mov dword [GDT_START_ADDRESS+0x08],0x0000ffff    ; 基地址为0,段界限为0xFFFFF
mov dword [GDT_START_ADDRESS+0x0c],0x00cf9200    ; 粒度为4KB,存储器段描述符 

;建立保护模式下的堆栈段描述符       
mov dword [GDT_START_ADDRESS+0x10],0x00000000    ; 基地址为0x00000000,界限0x0 
mov dword [GDT_START_ADDRESS+0x14],0x00409600    ; 粒度为1个字节

;建立保护模式下的显存描述符    
mov dword [GDT_START_ADDRESS+0x18],0x80007fff    ; 基地址为0x000B8000,界限0x07FFF 
mov dword [GDT_START_ADDRESS+0x1c],0x0040920b    ; 粒度为字节

;创建保护模式下平坦模式代码段描述符
mov dword [GDT_START_ADDRESS+0x20],0x0000ffff    ; 基地址为0,段界限为0xFFFFF
mov dword [GDT_START_ADDRESS+0x24],0x00cf9800    ; 粒度为4kb,代码段描述符 

;初始化描述符表寄存器GDTR
mov word [pgdt], 39       ;描述符表的界限    
lgdt [pgdt]
       
in al,0x92                           ;南桥芯片内的端口 
or al,0000_0010B
out 0x92,al                          ;打开A20

cli                                  ;中断机制尚未工作
mov eax,cr0
or eax,1
mov cr0,eax                          ;设置PE
       
;以下进入保护模式
jmp dword CODE_SELECTOR:protect_mode_begin

;16位的描述符选择子:32位偏移
;清流水线并串行化处理器
[bits 32]            
protect_mode_begin:                                

;定义各个符号
    _DR equ 1
    _UR equ 2
    _UL equ 3
    _DL equ 4
    delay equ 200
    ddelay equ 100
    
;代码段
;初始化各个寄存器
START:

mov eax, DATA_SELECTOR
mov ds, eax
mov gs, eax
mov eax, STACK_SELECTOR
mov ss, eax
mov eax, VIDEO_SELECTOR
mov es, eax

; mov ecx,0
; mov eax,0
; _init_:
;     cmp ecx,0x00007FFF
;     je _loop
    ; mov dword[gs:ecx], eax
;     add ecx,1
;     jmp _init_
; _loop:
; mov ebx,2
; mov ecx,0
; mov esi,1        ;offset
; mov edi,1        ;offset
; ; dead loop
; ;initilizing the start_point
;     mov ax, cs
;     mov es, ax
;     mov ds, ax
;     mov ax, 0b800h
;     mov es, ax
    mov esi, 0
    mov edi, 0

; 输出学号姓名
PRINT:    mov ebx, name
    mov al, [ebx+esi]
    cmp al, 0
    jz LOOP1    
    mov ebx, 52
    mov byte[es:ebx+edi], al    
    mov byte[es:ebx+edi+1], 1
    inc esi
    add edi, 2
    jmp PRINT

;循环实现延迟
LOOP1:
    dec dword[count]
    jnz LOOP1
    
    mov dword[count], delay
    dec dword[dcount]
    jnz LOOP1
    
    mov dword[count], delay
    mov dword[dcount], ddelay
    
    mov al,1
        cmp al, byte[rdul]
    jz DnRt 
       
    mov al, 2
           cmp al, byte[rdul]
    jz UpRt

           mov al, 3
           cmp al, byte[rdul]
    jz UpLt
       
    mov al, 4
           cmp al, byte[rdul]
    jz  DnLt

    jmp $

;往右下移动,判断是否碰壁并显示字符
DnRt:
    inc dword[x]
    inc dword[y]
    mov ebx, dword[x]
    mov eax, 25
    sub eax, ebx
          jz  dr2ur
    
    mov ebx, dword[y]
    mov eax, ebx
           jz  dr2dl
    
    jmp show

dr2ur:
           mov dword[x], 23
           mov byte[rdul], _UR    
           jmp show
dr2dl:
           mov dword[y], 78
          mov byte[rdul], _DL    
           jmp show

;往右上移动,判断是否碰壁并显示字符
UpRt:
    dec dword[x]
    inc dword[y]
    mov ebx, dword[y]
    mov eax, 80
    sub eax, ebx
           jz  ur2ul
    
    mov ebx, dword[x]
    mov eax, 0
    sub eax, ebx
          jz  ur2dr
    
    jmp show

ur2ul:
           mov dword[y], 78
           mov byte[rdul], _UL    
           jmp show
ur2dr:
           mov dword[x], 1
           mov byte[rdul], _DR    
           jmp show
    
;往左上移动,判断是否碰壁并显示字符
UpLt:
    dec dword[x]
    dec dword[y]
    mov ebx, dword[x]
    mov eax, 0
    sub eax, ebx
           jz  ul2dl

    mov ebx,dword[y]
    mov eax, -1
    sub eax, ebx
          jz  ul2ur
    
    jmp show

ul2dl:
           mov dword[x], 1
           mov byte[rdul], _DL    
           jmp show
ul2ur:
           mov dword[y], 1
           mov byte[rdul], _UR    
           jmp show

;往左下移动,判断是否碰壁并显示字符
DnLt:
    inc dword[x]
    dec dword[y]
    mov ebx, dword[y]
    mov eax, -1
    sub eax, ebx
           jz  dl2dr
    
    mov ebx, dword[x]
    mov eax, 25
    sub eax, ebx
           jz  dl2ul
    
    jmp show

dl2dr:
           mov dword[y], 1
           mov byte[rdul], _DR    
           jmp show
dl2ul:
           mov dword[x], 23
          mov byte[rdul], _UL    
           jmp show

;在屏幕上显示字符
show:    
    xor eax, eax                  ; 计算显存地址
    mov eax, dword[x]
    mov ebx, 80
    mul ebx
    add eax, dword[y]
    mov ebx, 2
    mul ebx
    mov ebx, eax
    mov ah, byte[color]    ;  0000:黑底、1111:亮白字(默认值为0x07
    mov al, byte[char]    ;  AL = 显示字符值(默认值为20h=空格符)
    mov [es:ebx], eax       ;  显示字符的ASCII码值
    
    inc byte[char]
    cmp byte[char], 'z'+1
    jnz keep
    mov byte[char], '0'

keep:    
    inc byte[color]
    cmp byte[color], 0x10
    jnz LOOP1
    mov byte[color], 0x40 ;    循环显示不同样式的字符
    jmp LOOP1

end:
    jmp $

;数据定义
    count dd delay        ;一层延迟
    dcount dd ddelay    ;二层延迟
    rdul db _DR             ;方向变量
    color db 0x02        ;样式(颜色)变量
    x dd 0                 ;横坐标
    y dd 0                 ;纵坐标
    char db 'A'             ;要显示的字符
    name db '19335025 CYH', 0    ;学号姓名

    ; times 510-($-$$) db 0
    ; dw 0aa55h


pgdt dw 0
     dd GDT_START_ADDRESS

结果

image-20220329005426004

image-20220329005431050

4. 总结

此次实验中阅读资料和理解实验内容花费了颇长的时间,在之前的实验中学习了LBA,CHS两种读取磁盘的方式,开启了保护模式,学习了gdb。本次作业在移植过程中出现了许多bug,多亏了gdb才能debug成功。可惜虚拟机十分的卡,做这个实验很烦。

参考

https://gitee.com/nelsoncheung/sysu-2021-spring-operating-system/tree/main

Creative Commons License本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.