本次实践的目的:打破开机引导程序512字节的限制,并从实模式切换到保护模式。
我们知道,bios开机自检、找到启动设备后,把启动设备的第一个扇区加载内存0x7c00位置开始执行。前两次实践中,我们的引导程序小于512字节,这没造成什么问题。如果我们的引导程序超过512字节怎么办呢?我的第一个想法就是,利用加载到内存的这512字节,写个程序,把启动盘中真正的引导程序继续加载到内存中。看到《Orange’s 一个操作系统的实现》的第三章的时候,里面并没有采用这种做法,而是转而去用个DOS加载大于512字节的引导程序,那就自己动手写一个吧。
程序思路
- 写个少于512字节的引导程序,用于启动引导后,把软盘第2扇区(如果你写的代码多与512字节,需要修改下面的引导,这里简单只拷贝了1个扇区)的数据拷贝到 0x7e00 的位置(0x7e00 == 0x7c00 + 512)。具体实现是调用了bios的13h中断。
- 保护模式的寻址方式与实模式的寻址方式不同。 虽然从实模式到保护模式只需要设置cr0寄存器即可,但是切换过去后,其寻址依赖于GDT的实现,所以需切换前先设置好GDT。
bf.asm
org 07c00h
[BITS 16]
START:
mov ax,cs
mov ds,ax
mov es,ax
;拷贝软盘中的代码到内存区
COPY:
mov bx, COPY_CODE_START ;07c00h + 512(0100h) == 07e00h
mov dl,0 ;驱动器号,软驱从0开始:0:软驱A,1:软驱B
;磁盘从80h开始,80h:C盘,81h:D盘
mov dh,0 ;磁头号,对于软盘即面号,一个面用一个磁头来读写
mov ch,0 ;磁道号
mov cl,2 ;扇区号
mov al,2 ;读取的扇区数
mov ah,2 ;13h的功能号(2表示读扇区),es:bx指向
;接收从扇区读入数据的内存区
int 13h
jc COPY ;读取失败,CF表示为1,重试读取
jmp LABEL_BEGIN ;把程序读到内存区后,跳转到新的执行点
;补全512字节
times 510-($-$$) db 0
dw 0xaa55
;这个宏用来填充gdt描述符的,每个描述符8个字节,64位。
;参数1:段基址,32位
;参数2:段大小limit,传32位,只用其低20位。
;参数3:段属性,16位,只用高4位与低8位,中间4位为0。
%macro Descriptor 3
dw %2 & 0FFFFh
dw %1 & 0FFFFh
db (%1 >> 16) & 0FFh
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)
db (%1 >> 24) & 0FFh
%endmacro
;段属性的常量,具体参考《Orange's 一个操作系统的实现》
DA_32 equ 4000h
DA_DPL0 equ 00h
DA_DPL1 equ 20h
DA_DPL2 equ 40h
DA_DPL3 equ 60h
DA_DR equ 90h
DA_DRW equ 92h
DA_DRWA equ 93h
DA_C equ 98h
DA_CR equ 9ah
DA_CCO equ 9ch
DA_CCOR equ 9eh
DA_LDT equ 82h
DA_TaskGate equ 85h
DA_386TSS equ 89h
DA_386CGate equ 8ch
DA_386IGate equ 8eh
DA_386TGate equ 8fh
COPY_CODE_START:
;全局描述符GDT,在切换到保护模式前,需先设置好相应的描述符。
[SECTION .gdt]
LABEL_GDT: Descriptor 0, 0, 0
LABEL_DESC_CODE32: Descriptor 0,SegCode32Len - 1, DA_C + DA_32
LABEL_DESC_VIDEO: Descriptor 0b8000h, 0ffffh,DA_DRW
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1
dd 0
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
;这段16位的代码段,目的是实现从实模式到保护模式的切换。
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,0100h
;设置好进入保护模式后立刻要执行的代码段的描述符
xor eax,eax
mov ax,cs
shl eax,4
add eax,LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2],ax
shr eax,16
mov byte [LABEL_DESC_CODE32 + 4],al
mov byte [LABEL_DESC_CODE32 + 7],ah
;设置GDT
xor eax,eax
mov ax,ds
shl eax,4
add eax,LABEL_GDT
mov dword [GdtPtr + 2],eax
;加载gdt
lgdt [GdtPtr]
cli
;打开A20地址线,扩大寻址空间
in al,92h
or al,00000010b
out 92h,al
;从实模式切换到保护模式
mov eax,cr0
or eax,1
mov cr0,eax
;跳转到32位的保护模式的代码
jmp dword SelectorCode32:0
;这段代码的功能,只是在屏幕右边的中间位置显示一个黑底红色的字母'P'
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
mov ax,SelectorVideo
mov gs,ax
mov edi,(80 * 11 + 79) *2
mov ah,0ch
mov al,'P'
mov [gs:edi],ax
jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32
这里为了省去每次都敲一堆命令的麻烦,写了个简单的脚本
bf.sh
#!/bin/bash
/usr/bin/nasm bf.asm -o bf.bin
dd if=bf.bin of=bf.img bs=512 count=2 conv=notrunc
bochs -f bf.bochs #这里的bf.bochs配置文件,请参考前一节的配置
执行 ./bf.sh
结果下图(屏幕右边中间位置有个红色的’P’):
问题
gdt、gdtr结构如何?
这个虽然在不同书籍都看过了,没亲自写代码,还是会忘掉。网上的版本请参考: 《GDT 与 LDT》。gdtr limit字段如何设置?为什么用gdt长度减一?
参考gdt的limit字段,其实不应该理解为长度,而是与offset类似的,从0开始,比如说计算gdtr最高的地址的时候,就可以用基址+limit计算出来。打开a20线还有其它方法吗?
这个问题还没搞清楚,回头更新这里。在16位模式下jmp dword SelectorCode32:0 其中的dword 是修饰哪个?linux内核中用db写二进制是如何实现的?
按目前的理解,选择子只有13位有效,所以 SelectorCode32这个用16位足以,而后面的偏移量offset,则可以是32位的。这句代码反汇编之后结果为:
00007e71: 66ea 0000 0000 0800 jmpf 0x0008:0000 0000
至于linux内核如何实现,后续跟进。我猜想就是直接用类似上面的二进制代码方式实现。