MBR --> Loader --> Kernel
本节代码只涉及 MBR 和 Loader 部分,暂未考虑内核代码。同时,为规范操作,我们使用 [加载器-用户程序] 方式将 Loader 从硬盘载入内存。这种方式非常漂亮,同时能让你理解重定位的本质,详细请阅读程序加载器-重定位 (本节前置要求,务必阅读)。

本节代码对应分支 protected-mode

配置文件
本节的 MBR 可以直接引用 程序加载器 一文中的 MBR 代码,并在文件头引入配置文件:

%include "loader.inc"

SECTION mbr align=16 vstart=0x7c00                                     
;....................以下省略....................

其中 loader.inc 文件内容如下:

;文件说明:loader.inc
BASE_ADDR    equ 0x900 ;最好不超过0xFFFF,原因在下文解释
START_SECTOR equ 2     ;从硬盘的第2(lba)扇区将加载器载入内存

接着,定义保护模式的配置文件 boot.inc 。将此图和以下代码对比阅读:
其中的含义参见《全局描述符表 & 段选择子概述》

;文件说明:boot.inc ,包含保护模式中要用到的段选择子,描述符等内容
;以下为段描述符的子属性
DESC_G_4K         equ   1000_0000_0000_0000_0000_0000B
DESC_DB_32        equ    100_0000_0000_0000_0000_0000B 
DESC_L            equ     00_0000_0000_0000_0000_0000B
DESC_AVL          equ      0_0000_0000_0000_0000_0000B
DESC_LIMIT_CODE2  equ        1111_0000_0000_0000_0000B
DESC_LIMIT_DATA2  equ        DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ        0000_0000_0000_0000_0000B
DESC_P            equ             1000_0000_0000_0000B
DESC_DPL_0        equ              000_0000_0000_0000B
DESC_DPL_1        equ              010_0000_0000_0000B
DESC_DPL_2        equ              100_0000_0000_0000B
DESC_DPL_3        equ              110_0000_0000_0000B
DESC_S_SYS        equ                0_0000_0000_0000B
DESC_S_DATA       equ                1_0000_0000_0000B
DESC_TYPE_CODE    equ                  1000_0000_0000B;只执行1000
DESC_TYPE_DATA    equ                  0010_0000_0000B;可读写0010

;以下为段描述符的高四位(低四位在loader.s中定义)
DESC_CODE_HIGH4   equ   (0x00<<24) + DESC_G_4K + DESC_DB_32 + DESC_L + DESC_AVL +\
                        DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_DATA +\
                        DESC_TYPE_CODE + 0X00
DESC_DATA_HIGH4   equ   (0x00<<24) + DESC_G_4K + DESC_DB_32 + DESC_L + DESC_AVL +\
                        DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA +\
                        DESC_TYPE_DATA + 0X00
DESC_VIDEO_HIGH4  equ   (0x00<<24) + DESC_G_4K + DESC_DB_32 + DESC_L + DESC_AVL +\
                        DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA +\
                        DESC_TYPE_DATA + 0X0b
;========段选择子属性=============
RPL0   equ   00B
RPL1   equ   01B
RPL2   equ   10B
RPL3   equ   11B
TI_GDT equ   000B
TI_LDT equ   100B

下面对以上宏定义进行说明:

  • 将各个子属性进行宏定义,最后相加组成段选择子的高四字节(低四字节后续定义)。相比一大串莫名奇妙的数字,这样更加直观。
  • 第 7, 8 行,将 DATA 和 CODE 的 4 位段界限全设置为 1(后面会将 DATA 和 CODE 的另外16位段界限全设为1),由于 G=1,粒度为 4KB,所以实际段大小为 4GB。第 21,24 行 0x00<<24 以及末尾加上 0x00,这是在将高4字节中的段基址设为 0(后面会将 DATA 和 CODE 的另外16位段基址全设为0),所以段基址为 0。将段基址设为 0,段界限设为 4GB,这样做是为了形成平坦模型 ,即整个内存都在一个段中。平坦模型使用起来很方便,后期我们会慢慢体会到。
  • 第 29 行,为啥最后加的 0x0b?之前说过,文本显示适配器的内存地址为 0xb8000~0xbffff为了方便显存的操作,显存段不使用平坦模型 ,所以将段基址设置为 0xb8000,其中的 b 在段描述符的高 4 字节上,这就是为啥 26 行末尾加 0x0b;显存的段大小为 0xbffff-0xb8000=0x7fff ,粒度为 4KB,因此段界限为 0x7fff÷4KB=7 ,这将在段描述符的低 4 位设置,高 4 位直接设 0 即可。
  • 第 15,16 行,SYS 表明该段是系统段;DATA 不是指数据段,而是相对于 SYS 而言的,代码段/数据段/栈段都属于 DATA 。
  • 以上宏定义并未定义栈段,这是因为此处将栈段和数据段定义在了一起,即 DATA 段。关于为什么栈段和数据段能够放在一个段中,参见内存段与段寄存器保护
  • _ 仅作分隔符,方便阅读,编译时会自动忽略。

loader.s

;文件说明:loader.s
%include "boot.inc"
%include "loader.inc"

SECTION loader vstart=BASE_ADDR              ;定义用户程序头部段 
    program_length  dd program_end           ;程序总长度[0x00]    
    ;用户程序入口点
    code_entry      dw start-BASE_ADDR       ;偏移地址[0x04]
                    dd section.loader.start  ;段地址[0x06] 
    realloc_tbl_len dw 0                     ;段重定位表项个数为0
;=========================================================
;GDT
;第0描述符不可用
    GDT_BASE        dd    0x00000000
                    dd    0x00000000
;第1描述符CODE
    DESC_CODE       dd    0x0000FFFF
                    dd    DESC_CODE_HIGH4
;第2描述符DATA   
    DESC_DATA       dd    0x0000FFFF
                    dd    DESC_DATA_HIGH4
;第3描述符VIDEO
    DESC_VIDEO      dd    0x80000007
                    dd    DESC_VIDEO_HIGH4
    
    GDT_SIZE        equ   $ - GDT_BASE
    GDT_LIMIT       equ   GDT_SIZE - 1    
;GDT指针
    gdt_ptr         dw    GDT_LIMIT    
                    dd    GDT_BASE
					
    SELECTOR_CODE   equ   ((DESC_CODE - GDT_BASE)/8)<<3 + TI_GDT + RPL0
    SELECTOR_DATA   equ   ((DESC_DATA - GDT_BASE)/8)<<3 + TI_GDT + RPL0
    SELECTOR_VIDEO  equ   ((DESC_VIDEO- GDT_BASE)/8)<<3 + TI_GDT + RPL0
    
    loader_msg      db    'r',11000010b,'e',11000010b,'a',11000010b,'l',11000010b,'-',11000010b
                    db    'm',11000010b,'o',11000010b,'d',11000010b,'e',11000010b
;=======================================================

start: ;程序入口          
	mov ax,0             ;转移到loader代码后,
	mov ds,ax            ;将各段寄存器清0是头等大事
	mov es,ax
	mov ss,ax
	mov gs,ax
	mov fs,ax
print:
	mov ax,0xb800        ;彩色字符模式视频缓冲区
	mov es,ax
	mov si,loader_msg    ;ds:si
	mov di,0             ;es:di
	mov cx,18            ;9个字符,占18字节
	cld
	rep movsb
;========================================================
;1.打开A20
;2.加载GDT
;3.置PE=1
prepare: 
;关中断
    cli
;打开A20
    in   al,0x92
    or   al,0000_0010B
    out  0x92,al
;加载GDT
    mov  ax,0
    mov  ds,ax
    lgdt [gdt_ptr]
;CR0的第0位置1
    mov  eax,cr0
    or   eax,0x0000_0001
    mov  cr0,eax
;此后进入保护模式
    jmp  dword SELECTOR_CODE:p_mode_start ;刷新流水线,装载CODE选择子
;======================================================
[bits 32]
p_mode_start:
    mov ax,SELECTOR_DATA
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov esp,BASE_ADDR       ;可以找其他合适的地方作为栈顶,这里使用BASE_ADDR
    mov ax,SELECTOR_VIDEO
    mov gs,ax

    mov byte [gs:160],'p'
    mov byte [gs:161],11000010b
    jmp $
;========================================================
program_end  equ  $-BASE_ADDR 

以上代码的解释:

  • 为什么此 loader 段的 vstart 不能像程序加载器中的 loader 段一样设为 0 ?有以下两个原因:

    1. 注意第 75 行代码,该代码执行后,代码段的段基址为 0(因为CODE段描述符的段基址之前被全设为0了),进入了平坦模式,所以在内存中寻址并取得指令就全靠偏移地址啦!该行代码将直接跳转至 p_mode_start 处。那么,p_mode_start 的值为多少呢?这个问题至关重要。如果我们将 loader 段的 vstart 设为 0,那么标号 p_mode_start 的值就为 79 行代码相对于文件头的偏移量,本文件编译后所得二进制文件大小大概为 172 字节,所以 p_mode_start 相对于文件头的偏移大概为 140(0x8C) 字节,即 p_mode_start=0x8C 。问题在于,我们已经将此 loader 载入到内存 0x900 处,如果跳转到 0x8C 处,显然将执行错误的代码。实际应该跳转到 0x98C 处,而 vstart=BASE_ADDR 便能将 p_mode_start 以 0x900 开始计算偏移,这样就能跳转到正确位置啦!说清楚真不容易。。

    2. 再注意第 30 行的 GDT_BASE。要知道,GDT_BASE 为 32 位段基地址,CPU 是直接在内存中的 GDT_BASE 处来找到 GDT 的 。说到这读者就应该懂了吧?原因和上点相同。

      vstart 不好理解,具体参见 程序加载器

  • 整个 loader 自成一段,这是为了方便。你也可以将以上代码分成数据段和代码段,但务必注意,除 loader 段外的其他段不能用 vstart 修饰

  • 第 8 行,偏移地址为什么是 start-BASE_ADDR ,因为在 MBR 最后的跳转指令(71行) jmp far [0x04] 的效果是 jmp 0x900:偏移地址 ,所以要此处放置的必须是 start 标号相对于本文件开头的偏移量。

  • 第 29 行,注意 GDT 的 LIMIT 等于 SIZE-1(因为偏移从0开始算)。

  • 第 23 行,(DESC_CODE - GDT_BASE)/8 得到索引值(段描述符为8字节),<<3 将索引值移到正确的位置上。

  • 第 61 行,关闭中断。保护模式下的中断机制和实模式不同,原有的中断向量表不再适用,BIOS 中断无法继续使用

  • 第 87 行,之前我们说过,为了方便显存操作,对 VIDEO 段仍使用分段模型而非平坦模型。

  • 注意!最后 program_end equ $-BASE_ADDR 得到整个文件二进制代码的大小。

最后需要单独强调的是,第 41~46 行代码并非必须要执行。这主要针对的是打印信息,即后面要用到的 ds 寄存器。如果 ds 不清零,则后续寻找字符时,ds:loader_msg 就是错误的地址,原因见上面第 1、2 点。然而如果按照我们上面的这种方式,则该 loader 必须加载到内存 0xFFFF 以内!否则打印无法正常进行(保护模式仍然能够正确进入)。这是因为打印时还在实模式,有效地址最大还是 16 位,如果 loader_msg 的标号超过 0xFFFF,那么有效地址就无法容纳 loader_msg。如果想把这 loader 加载到内存任意位置,则无需清零段寄存器,且第 50 行代码需要改为:mov si,loader_msg-BASE_ADDR
执行结果如下:

文章作者: 极简
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
自制操作系统
喜欢就支持一下吧