本篇文章所用图片来自田宇——《一个64位操作系统的设计与实现》https://github.com/belowthetree/Make-a-systemgithub.com
项目地址
经过上一篇的改造,我们已经成功进入了loader
文件中执行,并且进入了保护模式,需要说明的是,开启保护模式之后BIOS中断不可用,利用int
进行中断输出的功能也无法使用。 因为16位寻址能力有限,接下来就是开启保护模式,在32位环境下找寻并加载内核程序。
进入保护模式及全局段表
具体参考我上一篇文章
大树之下:自制操作系统(4)——进入保护模式与全局段表zhuanlan.zhihu.com改造searchfile
因为是在进入32位后再加载内核,所以需要让搜索函数的寄存器转变为32位。
; esi 存放文件名地址
; edi 目标位置
TmpFile equ 0x500
BaseOfRootdir equ 19
SectorOfRoot equ 14
DataOffset equ 17
Search_File:
mov [.Desindex], edi
push esi
mov ecx, 1
mov ebx, BaseOfRootdir
mov edi, TmpFile
call Read_Disk
pop esi
mov ecx, 0
mov edx, 0
mov edi, TmpFile
.read_one_sector: ; 读取一个扇区,一个扇区16个表项,用 ecx 计数
cmp ecx, 16
je .one_sector_finish
add edi, 32
inc ecx
mov ebx, -1
.cmp_name: ; 比较文件名,长度为11
inc ebx
cmp ebx, 11
jz .find_filename
mov al, byte [esi + ebx]
cmp al, byte [edi + ebx]
jz .cmp_name
jmp .read_one_sector
.one_sector_finish: ; edx 记录已读目录项数
inc edx
cmp edx, SectorOfRoot
jz .search_fail
push esi
push edx
mov ebx, BaseOfRootdir
add ebx, edx
mov edi, TmpFile
call Read_Disk
pop edx
mov ecx, 0
mov edi, TmpFile
pop esi
jmp .read_one_sector
.find_filename: ; 查找成功后找寻起始簇号
add edi, 0x1a
mov ax, word [edi]
mov [.FATEntry], eax
call .copy_file
ret
.copy_file:
; 先将 FAT 表放进 0x500
mov si, word [.FATEntry]
push si
mov ecx, 9
mov ebx, 1
mov edi, TmpFile
call Read_Disk
pop si
and esi, 0xff
mov ebx, esi
.copy:
push ebx
add ebx, SectorOfRoot
add ebx, DataOffset
mov ecx, 1
mov edi, [.Desindex]
call Read_Disk
add word [.Desindex], 0x200
pop ecx
mov eax, 3
mul ecx
mov ebx, 2
div ebx
add eax, TmpFile
mov edi, eax
mov ebx, [edi]
cmp edx, 0
jz .next
shr ebx, 4
.next:
; 如果属于正常文件,则继续进行读取
and ebx, 0x0fff
cmp ebx, 0xff8
jge .copy_finish
jmp .copy
.copy_finish:
ret
.search_fail:
ret
.ReadFail db "Read Fail", 0
.ReadSuccess db "Read Success", 0
.CopyFinish db "CopyFinish", 0
.FATEntry dd 0
.Desindex dd 0
这里的做法比较简单,就是单纯的将某些寄存器(特别是目标地址)变为32位。其中的输出取消,因为中断失效了,不取消就会出错。
设置64位段表
64位段表与保护模式的蕾丝,长度都是8个字节。不过接下来使用的是页表,段表与保护模式中的一样,设置为基址0,并覆盖整个内存
GDTBASE64: dq 0x0000000000000000
CODEBASE64: dq 0x0020980000000000
DATABASE64: dq 0x0000920000000000
GdtLen64 equ $ - GDTBASE64
GdtPtr64 dw GdtLen64 - 1
dd GDTBASE64
SelectorCode64 equ CODEBASE64 - GDTBASE64
SelectorData64 equ DATABASE64 - GDTBASE64
配置页表开启64位
[bits 32]
Start_Code32:
mov ax, SelectorData32
mov ds, ax
mov fs, ax
mov es, ax
mov ss, ax
mov esp, 0x7c00
mov esi, KernelFile
mov edi, BaseOfKernel
call Search_File
; 页目录
mov dword [0x10000], 0x11007
mov dword [0x10800], 0x11007
; 二级页表
mov dword [0x11000], 0x12007
; 三级,2M 页表
mov dword [0x12000], 0x000083
mov dword [0x12008], 0x200083
mov dword [0x12010], 0x400083
mov dword [0x12018], 0x600083
mov dword [0x12020], 0x800083
mov dword [0x12028], 0xa00083
; 加载 64 位 GDT
db 0x66
lgdt [GdtPtr64]
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
mov fs, ax
mov gs, ax
mov esp, 0x7c00
; 开启 PAE
mov eax, cr4
bts eax, 5
mov cr4, eax
; 加载页目录
mov eax, 0x10000
mov cr3, eax
; 开启长模式,ECX 选择寄存器组号,读取后放置在 EAX
mov ecx, 0x0c0000080
rdmsr
bts eax, 8
wrmsr
; 开启分页
mov eax, cr0
bts eax, 31
mov cr0, eax
jmp SelectorCode64:BaseOfKernel
其中的KernelFile
是你的内核文件名,BaseOfKernel
是内核将要加载的地址。页表部分,第一级页表放置在0x10000
处,指向二级页表0x11000
处。但是这里写的是0x11083
,83是属性部分,具体意义可以对照下图。
需要特别提醒的是,IA-32e模式采用的是Canonical地址,有一些地址是无法使用的,并且高16位是标志位,默认设置成1,例如ffff800000010000
是一个合法的地址。
生成、链接文件
我的文件结构是src
放置内核源文件,include
放置头文件
Makefile
CFLAGS = -mcmodel=large -Iinclude -m64 -fno-builtin
vpath %.c ./src
cfiles := $(wildcard src/*.c)
objects := $(cfiles:.c=.o)
all: system
objcopy -I elf64-x86-64 -S -R ".eh_frame" -R ".comment" -O binary system kernel.bin
system: $(objects) src/head.o src/entry.o
ld -b elf64-x86-64 -o system src/head.o src/entry.o $(objects) -T ./Kernel.lds
$(objects):%.o:%.c
gcc -c $(CFLAGS) $< -o $@
head.o: ./src/head.S
gcc -E ./src/head.S > head.s
as --64 -o head.o head.s
entry.o: src/entry.S
gcc -E ./src/entry.S > entry.s
as --64 -o entry.o entry.S
clean:
-rm ./src/*.o *.o
Kernel.lds
作为链接配置文件
其中 . =0xffff800000000000+0x100000;
将内核链接到 0xffff800000100000
处。
OUTPUT_FORMAT("elf64-x86-64","elf64-x86-64","elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SECTIONS
{
. = 0xffff800000000000 + 0x100000;
.text :
{
_text = .;
*(.text)
_etext = .;
}
. = ALIGN(8);
.data :
{
_data = .;
*(.data)
_edata = .;
}
.rodata :
{
_rodata = .;
*(.rodata)
_erodata = .;
}
. = ALIGN(32768);
.data.init_task : { *(.data.init_task) }
.bss :
{
_bss = .;
*(.bss)
_ebss = .;
}
_end = .;
}
内核文件
这里使用的是可以用gcc编译的gas(应该是这个名字)。方便编译与嵌入。
head.S
.section .text
.global _start
_start:
mov $10, %ax
mov %ax, %ds
mov %ax, %fs
mov %ax, %ss
mov %ax, %es
mov $0x7c00, %esp
jmp main
main.c
void main(){
while(1)
;
}
大功告成