嘿嘿 OS 嘿嘿🤤
其实说好听叫写操作系统 其实就是抄啦
参考项目: https://github.com/StevenBaby/onix/
asm; 无限循环 jmp $ ; 填充 times 510-($-$$) db 0 ; 主引导扇区最后两个字节必须是0x55 0xaa db 0x55,0xaa
编译使用nasm
即可
shellnasm -f bin boot.asm -o boot.bin
然后使用qemu
启动
qemu-system-x86_64 boot.bin
asm; BIOS要求主引导扇区的结尾必须是0x55 0xaa(主引导扇区大小为512) mov ah,0xe ; 显示字符的系统调用 mov al,'h' int 10h mov al,'e' int 10h mov al,'l' int 10h mov al,'l' int 10h mov al,'o' int 10h times 510-($-$$) db 0 dw 0xaa55
读取内核加载器并执行
其实我们的主引导程序在加载时会自动加载到0x7c00
的位置 这里可以做个实验验证一下
asm; BIOS要求主引导扇区的结尾必须是0x55 0xaa(主引导扇区大小为512) test_word: db 'a' mov ah,0xe mov bx,test_word add bx,0x7c00 mov al,[bx] int 10h times 510-($-$$) db 0 dw 0xaa55
可以看到 通过0x7c00+$test_word
成功索引到了定义的变量
但是每次如果想要输出都需要通过0x7c00
进行索引显得太过笨拙了 所以可以设定一个固定的内存偏移 来方便输出
asm[org 0x7c00] test_word: db 'a' mov ah,0xe mov bx,test_word add bx,0x7c00 mov al,[bx] int 10h mov al,[test_word] int 10h times 510-($-$$) db 0 dw 0xaa55
由于在第一次输出中 针对test_word相当于加了两次0x7c00
所以输出结果会不正确 而第二次输出定义了全局偏移 便可以直接打印变量了
asm; 标记程序在内存中的位置 [org 0x7c00] ; 设置屏幕模式为文本模式 清除屏幕 mov ax,3 int 0x10 ; 初始化寄存器 mov ax,0 mov ds,ax mov es,ax mov ss,ax mov sp,0x7c00 ; 无限循环 jmp $ ; 填充 times 510-($-$$) db 0 ; 主引导扇区最后两个字节必须是0x55 0xaa db 0x55,0xaa
每次都是每个字符单独系统调用显得太不优雅了 干脆直接将输出写成函数 以后有需要可以直接调用
asmmov si,test_string call print print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret test_string: db "Booting...",10,13,0 ; \n\r
每次手动编译也不是很优雅 直接写一个Makefile好了
makefileboot.bin: boot.asm
nasm -f bin boot.asm -o boot.bin
master.img:
qemu-img create -f raw master.img 16M
dd if=boot.bin of=master.img bs=512 count=1 conv=notrunc
.PHONY: clean
clean:
rm -rf *.bin
rm -rf *.img
qemu: master.img
qemu-system-i386 -m 32M -boot c -hda $< -fda boot.bin
首先需要先创建一个硬盘出来给qemu使用
shellqemu-img create -f raw master.img 16M
这样便可以创建一个 格式为raw
大小为16M的磁盘
当启动时需要附加磁盘时 可以执行如下命令来启动
shellqemu-system-x86_64 -hda master.img boot.bin
asmmov edi,0x1000; 数据写入的目标内存 mov ecx,0; 起始扇区 mov bl,1; 读取的扇区数量 call read_disk read_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al mov dx,0x1f3 mov al,cl; 起始扇区的前8bit out dx,al; mov dx,0x1f4 shr ecx,8 mov al,cl out dx,al mov dx,0x1f5 shr ecx,8 mov al,cl out dx,ax mov dx,0x1f6 shr ecx,8 and cl,0b1111; 将高四位置为0 mov al,0b1110_0000; 主盘 - LBA模式 or al,cl out dx,al mov dx,0x1f7 mov al,0x20 out dx,al xor ecx,ecx mov cl,bl .read: push cx; 保存现场 call .waits; 等待数据准备完毕 call .reads pop cx loop .read ret .waits: mov dx,0x1f7 .check: in al,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 and al,0b1000_1000 cmp al,0b0000_1000 jnz .check ret .reads: mov dx,0x1f0 mov cx,256; 一个扇区是256个字 .readw: in ax,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 mov [edi],ax add edi,2 loop .readw
asmwrite_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al mov dx,0x1f3 mov al,cl; 起始扇区的前8bit out dx,al; mov dx,0x1f4 shr ecx,8 mov al,cl out dx,al mov dx,0x1f5 shr ecx,8 mov al,cl out dx,ax mov dx,0x1f6 shr ecx,8 and cl,0b1111; 将高四位置为0 mov al,0b1110_0000; 主盘 - LBA模式 or al,cl out dx,al mov dx,0x1f7 mov al,0x30 out dx,al xor ecx,ecx mov cl,bl .write: push cx; 保存现场 call .writes call .waits; 等待硬盘繁忙结束 pop cx loop .write ret .waits: mov dx,0x1f7 .check: in al,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 and al,0b1000_0000 cmp al,0b0000_0000 jnz .check ret .writes: mov dx,0x1f0 mov cx,256; 一个扇区是256个字 .writew: mov ax,[edi] out dx,ax jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 mov [edi],ax add edi,2 loop .writew
在执行之后可以看到 硬盘中0x400
的位置会有数据的拷贝
到了这一步 我们的启动完整流程就是
makefile%.bin: %.asm
nasm -f bin $< -o $@
master.img: boot.bin loader.bin
qemu-img create -f raw master.img 16M
dd if=boot.bin of=master.img bs=512 count=1 conv=notrunc
dd if=loader.bin of=master.img bs=512 count=4 seek=2 conv=notrunc
.PHONY: clean
clean:
rm -rf *.bin
rm -rf *.img
qemu: master.img
qemu-system-i386 -m 32M -boot c -hda $< -fda boot.bin
目前我们也没有什么必须要实现的内核功能 所以对于现在的内核加载器 只要能够正常调用即可 要求不必太高
asm; loader.asm [org 0x1000] dw 0x55aa; 用于判断错误 mov si,loading call print jmp $ print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret loading: db "Loading...",10,13,0 ; \n\r
相对应的 在主引导扇区的汇编中也需要做些许的调整 主要就是调用loader内部的代码 并且对其载入情况进行判别 如果载入失败直接退出
asm; 标记程序在内存中的位置 [org 0x7c00] ; 设置屏幕模式为文本模式 清除屏幕 mov ax,3 int 0x10 ; 初始化寄存器 mov ax,0 mov ds,ax mov es,ax mov ss,ax mov sp,0x7c00 mov edi,0x1000; 数据写入的目标内存 mov ecx,2; 起始扇区 mov bl,4; 读取的扇区数量 call read_disk mov si,boot_string call print cmp word [0x1000],0x55aa jnz error jmp 0:0x1002 ; 无限循环 jmp $ read_disk: ; 设置读扇区的数量 mov dx,0x1f2 mov al,bl out dx,al ... write_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al ... print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret boot_string: db "Booting...",10,13,0 ; \n\r error: mov si,.msg call print hlt jmp $ .msg db "Booting Error!!!",10,13,0 ; 填充 times 510-($-$$) db 0 ; 主引导扇区最后两个字节必须是0x55 0xaa db 0x55,0xaa
针对内存加载器 其中很重要的一个功能就是内存检测 因为操作系统需要识别当前内存的状态 以及当前的内存是否适配当前的操作系统 内存是否可用等等问题 所以需要进行内存检测
其中BIOS的0x15
系统调用 功能号0xe820
就可以实现相关的功能
返回值为一个名为ARDS(Address Range Descriptor Structure)的数据结构
调用参数
0x534d4150
返回值
0x534d4150
asmdetect_memory: ; ebx置0 xor ebx,ebx ; es:di 结构体的缓存 mov ax,0 mov es,ax mov edi,ards_buffer ; 固定签名 mov edx,0x534d4150 .next: mov eax,0xe820 mov ecx,20 int 15h jc error add di,cx inc word [ards_count] cmp ebx,0 jnz .next mov si,detecting call print mov cx,[ards_count] mov si,0 .show: ; debug用 mov eax,[ards_buffer+si+0]; 内存基地址低32位 mov ebx,[ards_buffer+si+8]; 内存长度低32位 mov edx,[ards_buffer+si+16]; 内存类型 add si,20 loop .show ards_count: dw 0 detecting: db "Detecting Memory Success",10,13,0 ; ards_buffer要放最后 以免出现内存覆盖问题 ards_buffer:
in
/out
指令80386的内存全局描述符如下
使用C语言的定义如下
ctypedef struct descriptor
{
unsigned short limit_low; // 段界限0~15位
unsigned int base_low:24; // 基地址0~23位
unsigned char type:4; //段类型
unsigned char segment:1; // 1代表代码段或数据段 0代表系统段
unsigned char DPL:2; // Descriptor Privilege Level 描述符权限等级 0~3
unsigned char present:1; // 存在位 1表示在内存中 0表示在磁盘中
unsigned char limit_high:4; // 段界限16~19
unsigned char avaliable:1; // 无用
unsigned char long_mode:1; // 64位拓展标志
unsigned char big:1; // 32位还是16位
unsigned char granularity:1; // 粒度4KB或是1B
unsigned char base_high; // 基地址24~31位
}
对于CPU如果需要知晓一段内存的属性的话 其实没必要使用到所有的全局描述符内的信息 当CPU知道内存的其实位置
内存的长度(界限)
内存属性
其实就能知道这段内存能否使用 是否报错了
全局描述符表 GDT(Global Descriptor Table)
cdescriptor gdt[8192];
对于全局描述符来说 其需要常驻内存 所以同样也需要一个数据结构来描述其起始位置和长度
ctypedef struct pointer
{
unsigned short limit; // size -1
unsigned int base;
}
CPU同样也提供了一个寄存器gdtr
用来存储全局描述符表的起始位置和长度 以及两个指令lgdt
和sgdt
来对GDT表进行操作
clgdt [gdt_ptr]; // 加载gdt
sgdt [gdt_ptr]; // 保存gdt
在代码执行时往往需要内存 而每次对内存操作都需要CPU对内存全局描述符的访问 所以就有了段选择子
因为在保护模式下不需要段寄存器来进行地址索引 所以这里会将一个16位数加载到段寄存器中供CPU使用 数据结构如下
ctypedef struct selector
{
unsigned char RPL:2; // Request Privilege Level
unsigned char TI:1; // 0表示全局描述符 1表示局部描述符(LDT)
unsigned short index:13; // 全局描述符表索引
}
在8086时代 最大内存只有1M 索引一个地址需要段地址*0x10+偏移地址
如果计算结果大于1M则丢弃进位 让地址回归到1M以内
在80286时代 内存就有16M了 也就是24根地址线 到了80386就有了32根地址线 也就是4G内存
但是为了兼容8086 在后续的CPU上设置了一个功能 即A20线
需要打开0x92
端口即可实现地址回绕
启用保护模式只需要将cr0寄存器
的第0位置为1即可
我们可以直接在检测内存完成之后直接开启保护模式
asmdetect_memory: ; ebx置0 xor ebx,ebx ; es:di 结构体的缓存 mov ax,0 mov es,ax mov edi,ards_buffer ; 固定签名 mov edx,0x534d4150 .next: mov eax,0xe820 mov ecx,20 int 15h jc error add di,cx inc word [ards_count] cmp ebx,0 jnz .next mov si,detecting call print jmp prepare_protected_mode prepare_protected_mode: cli; 关闭中断 ; 打开A20线 in al,0x92 or al,0b10 out 0x92,al ; 加载gdt lgdt [gdp_ptr] ;启动保护模式 mov eax,cr0 or eax,1 mov cr0,eax ; 用跳转刷新缓存启用保护模式 jmp dword code_selector:protect_mode [bits 32] protect_mode: ; 初始段化寄存器 mov ax,data_selector mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov esp,0x10000; 修改栈顶 mov byte [0xb8000],'p'; 显示p 表示已经可以直接访问1M以外的内存 jmp $ code_selector equ (1<<3); 代码段选择子 第二个描述符 data_selector equ (2<<3); 数据段选择子 第三个描述符 memory_base equ 0 ; 内存基地址 memory_limit equ ((1024*1024*1024*4) / (1024*4))-1; 内存界限 4G/4K-1 ; gdt指针 gdp_ptr: dw (gdt_end-gdt_base)-1; 界限 dd gdt_base; 基地址 ; NULL描述符 gdt_base: dd 0,0 ; 代码段描述符 gdt_code: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_1_0_1_0; 小端序 存在 特权级00 代码段 非依存 可读 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 ; 数据段描述符 gdt_data: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_0_0_1_0; 小端序 存在 特权级00 数据段 向上生长 可写 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 gdt_end:
因为 我们开启了保护模式之后 寄存器等全部都为16位寄存器了 便可以直接访问0xb8000 如果代码生效的话 应该可以将第一个字符变为p
后续就要开始Kernel相关的编写工作了 重新组织一下项目目录
makefileBUILD:=../BUILD
SRC:=.
$(BUILD)/boot/%.bin: $(SRC)/boot/%.asm
$(shell mkdir -p $(dir $@))
nasm -f bin $< -o $@
$(BUILD)/master.img: $(BUILD)/boot/boot.bin $(BUILD)/boot/loader.bin
qemu-img create -f raw $@ 16M
dd if=$(BUILD)/boot/boot.bin of=$@ bs=512 count=1 conv=notrunc status=progress
dd if=$(BUILD)/boot/loader.bin of=$@ bs=512 count=4 seek=2 conv=notrunc status=progress
.PHONY: clean
clean:
rm -rf $(BUILD)
qemu: $(BUILD)/master.img
qemu-system-i386 -m 32M -boot c -hda $< -fda $(BUILD)/boot/boot.bin
老样子 先简单构建一个原型 后续再填充功能
asm[bits 32] global _start _start: mov byte [0xb8000],'K' jmp $
然后在loader.asm中调用即可
asm[org 0x1000] dw 0x55aa; 用于判断错误 mov si,loading call print ; jmp $ call detect_memory print: mov ah,0x0e .next: mov al,[si] cmp al,0 jz .done int 10h inc si jmp .next .done: ret detect_memory: ; ebx置0 xor ebx,ebx ; es:di 结构体的缓存 mov ax,0 mov es,ax mov edi,ards_buffer ; 固定签名 mov edx,0x534d4150 .next: mov eax,0xe820 mov ecx,20 int 15h jc error add di,cx inc word [ards_count] cmp ebx,0 jnz .next mov si,detecting call print jmp prepare_protected_mode prepare_protected_mode: cli; 关闭中断 ; 打开A20线 in al,0x92 or al,0b10 out 0x92,al ; 加载gdt lgdt [gdp_ptr] ;启动保护模式 mov eax,cr0 or eax,1 mov cr0,eax ; 用跳转刷新缓存启用保护模式 jmp dword code_selector:protect_mode [bits 32] protect_mode: ; 初始段化寄存器 mov ax,data_selector mov ds,ax mov es,ax mov fs,ax mov gs,ax mov ss,ax mov esp,0x10000; 修改栈顶 ; 读取内核 mov edi,0x10000 ;0xb8000 mov ecx,10 mov bl,200 call read_disk jmp dword code_selector:0x10000 ud2; 表示出错 read_disk: ; 设置读写扇区的数量 mov dx,0x1f2 mov al,bl out dx,al mov dx,0x1f3 mov al,cl; 起始扇区的前8bit out dx,al; mov dx,0x1f4 shr ecx,8 mov al,cl out dx,al mov dx,0x1f5 shr ecx,8 mov al,cl out dx,ax mov dx,0x1f6 shr ecx,8 and cl,0b1111; 将高四位置为0 mov al,0b1110_0000; 主盘 - LBA模式 or al,cl out dx,al mov dx,0x1f7 mov al,0x20 out dx,al xor ecx,ecx mov cl,bl .read: push cx; 保存现场 call .waits; 等待数据准备完毕 call .reads pop cx loop .read ret .waits: mov dx,0x1f7 .check: in al,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 and al,0b1000_1000 cmp al,0b0000_1000 jnz .check ret .reads: mov dx,0x1f0 mov cx,256; 一个扇区是256个字 .readw: in ax,dx jmp $+2; 效果同nop 只不过延迟更大 jmp $+2 jmp $+2 mov [edi],ax add edi,2 loop .readw ret code_selector equ (1<<3); 代码段选择子 第二个描述符 data_selector equ (2<<3); 数据段选择子 第三个描述符 memory_base equ 0 ; 内存基地址 memory_limit equ ((1024*1024*1024*4) / (1024*4))-1; 内存界限 4G/4K-1 ; gdt指针 gdp_ptr: dw (gdt_end-gdt_base)-1; 界限 dd gdt_base; 基地址 ; NULL描述符 gdt_base: dd 0,0 ; 代码段描述符 gdt_code: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_1_0_1_0; 小端序 存在 特权级00 代码段 非依存 可读 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 ; 数据段描述符 gdt_data: dw memory_limit&0xffff; 段界限0~15位 dw memory_base&0xffff; 段基地址0~15位 db (memory_base>>16)&0xff; 段基地址16~23位 db 0b_1_00_1_0_0_1_0; 小端序 存在 特权级00 数据段 向上生长 可写 未被CPU访问过 db 0b1_1_0_0_0000 | (memory_limit>>16)&0xf ; 粒度4k 32位 不是64位 随意 段界限16~19位 db (memory_base>>24)&0xff; 段基地址24~31位 gdt_end: error: mov si,.msg call print hlt jmp $ .msg db "Loading Error!!!",10,13,0 ards_count: dw 0 loading: db "Loading...",10,13,0 ; \n\r detecting: db "Detecting Memory Success",10,13,0 ards_buffer:
这里需要特别注意
read_disk
的位置 放在上面是不行的 debug了半天..
本文作者:Du4t
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!