• 从裸机启动开始运行一个C++程序(七)


    前序文章请看:
    从裸机启动开始运行一个C++程序(六)
    从裸机启动开始运行一个C++程序(五)
    从裸机启动开始运行一个C++程序(四)
    从裸机启动开始运行一个C++程序(三)
    从裸机启动开始运行一个C++程序(二)
    从裸机启动开始运行一个C++程序(一)

    重新写一份MBR代码

    前面我们花了不少的篇幅来介绍保护模式,以及通过汇编指令进入保护模式的方法。那么这一节,我们就来上一个完整的实例,首先在实模式下加载Kernel,然后配置GDT,之后进入保护模式,跳转至Kernel,再在Kernel里再打印一些文字用以区分。

    前面的实例中我们已经把工程代码分为了mbr和kernel,在mbr中读盘,加载到内存中,然后再通过跳转指令运行kernel文件。虽然照理来说,在kernel中再配置GDT然后进入保护模式也没什么问题,但这样就会使得kernel中同时夹杂两种模式的指令代码(后续我们介绍完386模式以后,就还可能会同时混有16位和32位指令),不方便管理和维护。因此,我们在mbr中就做好这一些,最后以保护模式跳转至kernel部分,这样我们的kernel就会纯粹许多。

    下面是我们重新写的一份MBR代码:

    ; 调用0x10号BIOS中断,清屏
    mov al, 0x03
    mov ah, 0x00
    int 0x10 
    
    ; LBA28模式,逻辑扇区号28位,从0x00000000xFFFFFFF
    ; 设置读取扇区的数量
    mov dx, 0x01f2
    mov al, 2 ; 读取连续的几个扇区,每读取一个al就会减1
    out dx, al
    ; 设置起始扇区号,28位需要拆开
    mov dx, 0x01f3
    mov al, 0x02 ; 从第2个扇区开始读(1起始,0留空),扇区号0~7out dx, al
    mov dx, 0x01f4 ; 扇区号8~15mov al, 0
    out dx, al
    mov dx, 0x01f5 ; 扇区号16~23mov al, 0
    out dx, al
    mov dx, 0x01f6
    mov al, 111_0_0000b ;4位是扇区号24~27位,第4位是主从盘(01从),高3位表示磁盘模式(111表示LBA)
    ; 配置命令
    mov dx, 0x01f7
    mov al, 0x20 ; 0x20命令表示读盘
    out dx, al
    
    wait_finish:
    ; 检测状态,是否读取完毕
    mov dx, 0x01f7
    in al, dx ; 通过该端口读取状态数据
    and al, 1000_1000b ; 保留第7位和第3cmp al, 0000_1000b ; 要检测第7位为0(表示不在忙碌状态)和第3位是否是1(表示已经读取完毕)
    jne wait_finish ; 如果不满足则循环等待
    
    ; 从端口加载数据到内存
    mov cx, 512 ; 一共要读的字节除以2(表示次数,因为每次会读2字节所以要除以2mov dx, 0x01f0
    mov ax, 0x0800
    mov ds, ax
    xor bx, bx ; [ds:bx] = 0x08000
    read:
    in ax, dx ; 16位端口,所以要用16位寄存器
    mov [bx], ax
    add bx, 2 ; 因为ax是16位,所以一次会写2字节
    loop read
    
    ; 下面配置GDT
    mov ax, 0x07e0
    mov es, ax
    
    ; 空白段
    mov [es:0x00], dword 0
    mov [es:0x04], dword 0
    
    ; 1号段
    ; 基址0x8000,大小8KB
    mov [es:0x08], word 0x1fff ; Limit=0x1fff
    mov [es:0x0a], word 0x8000 ; Base=0x008000,这是低16位
    mov [es:0x0c], byte 0      ; 这是Base的高8位
    mov [es:0x0d], byte 1_00_1_100_0b ; P=1, DPL=0, S=1, Type=100b, A=0
    mov [es:0x0e], word 0      ; 保留位都置0
    
    ; 下面是gdt信息的配置(暂且放在0x07f00的位置)
    mov ax, 0x07f0
    mov es, ax
    mov [es:0x00], word 15      ; 因为目前配了2个段,长度为16,所以limit为15
    mov [es:0x02], dword 0x7e00 ; GDT配置表的首地址
    ; 把gdt配置进gdtr
    lgdt [es:0x00]
    
    mov eax, cr0
    or eax, 0x01 ; PE位置1,启动保护模式
    mov cr0, eax
    
    jmp 00001_00_0b:0 ; 远跳指令可以刷新cs,使用1号段,正好跳转至kernel的加载位置(0x8000)
    
    times 510-($-$$) db 0 ; MBR剩余部分用0填充
    dw 0xaa55
    
    
    • 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

    可以看到最后有一个远跳指令jmp 00001_00_0b:0,由于这个时候我们已经通过控制cr0寄存器来进入保护模式了,所以前面段的部分就已经不是段基址而是段选择子了。通过远跳指令我们就可以刷新cs寄存器,让他表示选择子1号段,而根据GDT的配置,1号段的段基址就是0x8000,也就正好是我们Kernel加载的位置。

    所以接下来,我们只需要在Kernel中,输出一下信息,观察程序能否正常运行即可。但有个严重的问题是,要想输出信息我们需要写显存,可是显存在0xb8000~0xb8f9f,而1号段的界限在0x1fff,所以,我们当前的情况是没法操作显存的。那怎么办?我相信有读者可能会想到,那要不我们把1号段配长一点,包括到显存,是不是就可以操作了?

    答案是:不可以!因为除了界限问题,保护模式下段是具有属性的,也就是GDT中的TypeXEW这3位,我们配置的时候配置的是100W0的时候是不可写的,所以我们不可以向这个段里写数据。

    那,把Type配成101不就可以解决了么?理论上来说是的,但并不推荐大家这样去做,因为我们不希望指令段在执行时轻易被更改,这样程序的风险极大。当然了,如果大家仅仅是自己做实验方便的话,倒也无妨。

    不过更稳妥的做法,是再配一个段,专门去管理显存。这样我们就要回到MBR里加一个GDT,把显存区域划分成一个段,然后再在显存中写数据即可。

    ; 2号段
    ; 基址0xb8000,上限0xb8f9f,覆盖所有显存
    mov [es:0x10], word 0x0f9f ; Limit=0x0f9f
    mov [es:0x12], word 0x8000 ; Base=0x0b8000,这是低16位
    mov [es:0x14], byte 0x0b   ; 这是Base的高8位
    mov [es:0x15], byte 1_00_1_001_0b ; P=1, DPL=0, S=1, Type=001b, A=0
    mov [es:0x16], word 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    别忘了对应的GDT的配置也要变,要让2号段生效才行:

    ; 下面是gdt信息的配置(暂且放在0x07f00的位置)
    mov ax, 0x07f0
    mov es, ax
    mov [es:0x00], word 23      ; 因为目前配了3个段,长度为24,所以limit为23
    mov [es:0x02], dword 0x7e00 ; GDT配置表的首地址
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面给出修正后的完整MBR代码:

    ; 调用0x10号BIOS中断,清屏
    mov al, 0x03
    mov ah, 0x00
    int 0x10 
    
    ; LBA28模式,逻辑扇区号28位,从0x00000000xFFFFFFF
    ; 设置读取扇区的数量
    mov dx, 0x01f2
    mov al, 2 ; 读取连续的几个扇区,每读取一个al就会减1
    out dx, al
    ; 设置起始扇区号,28位需要拆开
    mov dx, 0x01f3
    mov al, 0x02 ; 从第2个扇区开始读(1起始,0留空),扇区号0~7位
    out dx, al
    mov dx, 0x01f4 ; 扇区号8~15位
    mov al, 0
    out dx, al
    mov dx, 0x01f5 ; 扇区号16~23位
    mov al, 0
    out dx, al
    mov dx, 0x01f6
    mov al, 111_0_0000b ;4位是扇区号24~27位,第4位是主从盘(01从),高3位表示磁盘模式(111表示LBA)
    ; 配置命令
    mov dx, 0x01f7
    mov al, 0x20 ; 0x20命令表示读盘
    out dx, al
    
    wait_finish:
    ; 检测状态,是否读取完毕
    mov dx, 0x01f7
    in al, dx ; 通过该端口读取状态数据
    and al, 1000_1000b ; 保留第7位和第3位
    cmp al, 0000_1000b ; 要检测第7位为0(表示不在忙碌状态)和第3位是否是1(表示已经读取完毕)
    jne wait_finish ; 如果不满足则循环等待
    
    ; 从端口加载数据到内存
    mov cx, 512 ; 一共要读的字节除以2(表示次数,因为每次会读2字节所以要除以2)
    mov dx, 0x01f0
    mov ax, 0x0800
    mov ds, ax
    xor bx, bx ; [ds:bx] = 0x08000
    read:
    in ax, dx ; 16位端口,所以要用16位寄存器
    mov [bx], ax
    add bx, 2 ; 因为ax是16位,所以一次会写2字节
    loop read
    
    ; 下面配置GDT
    mov ax, 0x07e0
    mov es, ax
    
    ; 空白段
    mov [es:0x00], dword 0
    mov [es:0x04], dword 0
    
    ; 1号段
    ; 基址0x8000,大小8KB
    mov [es:0x08], word 0x1fff ; Limit=0x1fff
    mov [es:0x0a], word 0x8000 ; Base=0x008000,这是低16位
    mov [es:0x0c], byte 0      ; 这是Base的高8位
    mov [es:0x0d], byte 1_00_1_100_0b ; P=1, DPL=0, S=1, Type=100b, A=0
    mov [es:0x0e], word 0
    
    ; 2号段
    ; 基址0xb8000,上限0xb8f9f,覆盖所有显存
    mov [es:0x10], word 0x0f9f ; Limit=0x0f9f
    mov [es:0x12], word 0x8000 ; Base=0x0b8000,这是低16位
    mov [es:0x14], byte 0x0b   ; 这是Base的高8位
    mov [es:0x15], byte 1_00_1_001_0b ; P=1, DPL=0, S=1, Type=001b, A=0
    mov [es:0x16], word 0
    
    ; 下面是gdt信息的配置(暂且放在0x07f00的位置)
    mov ax, 0x07f0
    mov es, ax
    mov [es:0x00], word 23      ; 因为目前配了3个段,长度为24,所以limit为23
    mov [es:0x02], dword 0x7e00 ; GDT配置表的首地址
    ; 把gdt配置进gdtr
    lgdt [es:0x00]
    
    mov eax, cr0
    or eax, 0x01 ; PE位置1,启动保护模式
    mov cr0, eax
    
    jmp 00001_00_0b:0 ; 远跳指令可以刷新cs,使用1号段,正好跳转至kernel的加载位置(0x8000)
    
    times 510-($-$$) db 0 ; MBR剩余部分用0填充
    dw 0xaa55
    
    • 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

    然后我们在Kernel中打印随便打印点东西,查看效果即可。注意,要写显存的话,需要将段寄存器调整到2号选择子。

    以下是Kernel例程:

    begin:
    
    mov ax, 00010_00_0b ; 选择2号段,以操作显存
    mov ds, ax
    ; 打印Hello
    mov [0x0000], byte 'H'
    mov [0x0001], byte 0x0f
    mov [0x0002], byte 'e'
    mov [0x0003], byte 0x0f
    mov [0x0004], byte 'l'
    mov [0x0005], byte 0x0f
    mov [0x0006], byte 'l'
    mov [0x0007], byte 0x0f
    mov [0x0008], byte 'o'
    mov [0x0009], byte 0x0f
    
    hlt
    
    times 1024-($-begin) db 0 ; 补满2个扇区
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    以下是运行效果图:
    运行效果

    小结

    由此,我们成功在保护模式下写入数据,并完成的打印。

    我会将这一节的整个工程文件打包上传到附件中,读者可以自行使用。

    下一篇开始我们会继续进军386模式,也就是后来非常成熟的IA-32架构,并介绍对应的32位指令。要知道IA-32架构的代码就已经可以跟C语言代码做链接了哟!离我们的目标就近了许多,大家敬请期待~

    从裸机启动开始运行一个C++程序(八)

  • 相关阅读:
    17_C++_面向对象_构造函数_析构函数
    etcd v3版本生产级集群搭建以及实现一键启动脚本
    linux/mac 下查看、修改文件权限的命令
    react-state hook
    python 异常机制
    基于 BP 神经网络特征提取的指纹识别应用(Matlab代码实现)
    【C++】vector的介绍 | 常见接口的使用
    Rust 实战丨绘制曼德博集
    第九周实验记录
    【Matlab】数据统计分析
  • 原文地址:https://blog.csdn.net/fl2011sx/article/details/133699233