写在前面
项目地址:https://github.com/bajdcc/MiniOS
想起去年魔改(其实就是copy)了一份SilverRainZ/OS67的代码,用bochs运行没问题,但用qemu老是报错,关键是qemu调试起来顺手。故怒删代码,保留最精简部分,然后慢慢加功能,在coding过程中,也发现了OS67中的一些注释问题,我将资料网址直接贴在代码注释里,方便阅读 。
那么这个最精简的内核是怎样的呢?
内核初谈
目录分为:
- boot 引导文件
- include 头文件
- script 脚本
- src/kernel 内核代码
- usr/logo.txt 系统LOGO
- * makefile
加粗的部分是比较重要的。
所有代码保存在软盘中,最终编译后软盘的格局:[0-200H] 引导项,[200H-] 内核,剩余部分填零。
引导部分
我们编译的系统内核附带上引导文件,最终是以二进制形式保存在软盘(Floppy)中的,qemu启动的时候,读取软盘并执行代码。
引导文件用汇编写成。
; 引导扇区 FAT12
%INCLUDE "gdt.asm" ; 段描述表定义
[BITS 16]
org 0x7c00 ; 加载地址偏移 参考http://blog.csdn.net/u011542994/article/details/46707815
BS_jmpBoot jmp entry
db 0x90
BS_OEMName db "CCOSV587" ; OEM name / 8 B
BPB_BytsPerSec dw 512 ; 一个扇区512字节
BPB_SecPerClus db 1 ; 每个簇一个扇区
BPB_RsvdSecCnt dw 1 ; 保留扇区数, 必须为1
BPB_NumFATs db 2 ; FAT表份数
BPB_RootEntCnt dw 224 ; 根目录项数
BPB_TotSec16 dw 2880 ; RolSec16, 总扇区数
BPB_Media db 0xf0 ; 介质种类: 移动介质
BPB_FATSz16 dw 9 ; FATSz16 分区表占用扇区数
BPB_SecPerTrk dw 18 ; SecPerTrk, 磁盘
BPB_NumHeads dw 2 ; 磁头数
BPB_HiddSec dd 0 ; HiddSec
BPB_TotSec32 dd 2880 ; 卡容量
BS_DrvNum db 0 ; DvcNum
BS_Reserved1 db 0 ; NT保留
BS_BootSig db 0x29 ; BootSig扩展引导标记
BS_VolD dd 0xffffffff ; VolID
BS_VolLab db "FLOPPYCDDS " ; 卷标
BS_FileSysType db "FAT12 " ; FilesysType
times 18 db 0
_print16:
loop:
lodsb ; ds:si -> al
or al,al
jz done
mov ah,0x0e
mov bx,15
int 0x10 ; 打印字符
jmp loop
done:
ret
;============================================================
; 入口
entry:
mov ax,0
mov ss,ax
mov sp,0x7c00
mov ds,ax
mov es,ax ; bios interrupt expects ds
; shift to text mode, 16 color 80*25
; 参考自http://blog.csdn.net/hua19880705/article/details/8125706
; http://www.cnblogs.com/magic-cube/archive/2011/10/19/2217676.html
mov ah,0x0
mov al,0x03 ; 设置模式 16色 80x25矩阵
int 0x10 ; 设置颜色
mov si, msg_boot
call _print16
;============================================================
; 从软盘中读取内核代码
; 参考http://blog.chinaunix.net/uid-20496675-id-1664077.html
; http://chuanwang66.iteye.com/blog/1678952
; 读磁盘时,将读到的扇区放到[es:bx]开始的内存中
; 写磁盘时,将[es:bx]开始的一个扇区写到磁盘上
; 这两处,[es:bx]都称为 数据缓冲区
; read 20 sector (360 KB) form floppy
loadloader:
mov bx,0
mov ax,0x0800
mov es,ax ; [es:bx] buffer address point -> 0x8000 将读取数据存放至0x8000
mov cl,2 ; 扇区 Sector
mov ch,0 ; 磁道 Track
mov dh,0 ; 盘面 Cylinder
mov dl,0 ; 驱动器号 driver a:
; kernel locates after bootloader, which is the second sector
readloop:
mov si,0 ; 错误计数 err counter
retry:
mov ah,0x02 ; int 0x13 ah = 0x02 read sector form dirve
mov al,1 ; read 1 sector
int 0x13 ; 读取磁道1
jnc next ; 没有错误则继续读取
add si,1
cmp si,5 ; 累计错误出现5次就报错
jae error
mov ah,0
mov dl,0 ; driver a
int 0x13 ; 复位 reset
jmp next
next:
mov ax,es
add ax,0x20 ; 一个扇区是512B=0x200,es是段,故再除以16,得到0x20
mov es,ax
add cl,1 ; 读下一个扇区 sector + 1
cmp cl,18 ; 18 sector 如果读满了所有18个扇区,就
jbe readloop
mov cl,1
add dh,1 ; 盘面 + 1
cmp dh,1
jbe readloop
mov dh,0
add ch,1 ; 磁道 + 1
cmp ch,20 ; 只读取20个磁道共360KB
jbe readloop
jmp succ
error:
mov si,msg_err ; 报错
call _print16
jmp $ ; halt
succ:
mov si,msg_succ ; 读取成功
call _print16
; fill and load GDTR 读取全局描述符表寄存器
; 参考http://x86.renejeschke.de/html/file_module_x86_id_156.html
xor eax,eax
mov ax,ds
shl eax,4
add eax,GDT ; eax <- gdt base
mov dword [GdtPtr+2],eax ; [GdtPtr + 2] <- gdt base
lgdt [GdtPtr]
cli
; turn on A20 line
; 参考 http://blog.csdn.net/yunsongice/article/details/6110648
in al,0x92
or al,00000010b
out 0x92,al
; 切换到保护模式 shift to protect mode
mov eax,cr0
or eax,1
mov cr0,eax
; special, clear pipe-line and jump
; 前面读取软盘数据到0x8000处,现在跳转至0x8000
jmp dword Selec_Code32_R0:0x8000
msg_boot:
db "[Bootsector] loading...",13,10,0 ; 13 10(0x0D 0x0A)是'
'
msg_err:
db "[Bootsector] error",13,10,0
msg_succ:
db "[Bootsector] ok",13,10,0
msg_temp:
db 0,0,0
msg_get_mem_map_err:
db "[Bootsector] failed",0
GDT: ; 全局描述符表
DESC_NULL: Descriptor 0, 0, 0 ; null
DESC_CODE32_R0: Descriptor 0, 0xfffff - 1, DA_C+DA_32+DA_LIMIT_4K ; uncomfirm
DESC_DATA_R0: Descriptor 0, 0xfffff - 1, DA_DRW+DA_32+DA_LIMIT_4K ; uncomfirm ; 4G seg
DESC_VIDEO_R0: Descriptor 0xb8000, 0xffff, DA_DRW+DA_32 ; vram
GdtLen equ $ - GDT ; GDT len
GdtPtr dw GdtLen - 1 ; GDT limit
dd 0 ; GDT Base
; GDT Selector
Selec_Code32_R0 equ DESC_CODE32_R0 - DESC_NULL
Selec_Data_R0 equ DESC_DATA_R0 - DESC_NULL
Selec_Video_R0 equ DESC_VIDEO_R0 - DESC_NULL
times 510 - ($-$$) db 0 ; 填充零
db 0x55, 0xaa
上述代码做了一些事情:
- 在 times 18 db 0 上面的代码:主要是填充软盘的参数
- _print16:用来输出
- 接着进入entry
- 10H中断:设置VGA显示模式为16色 80x25矩阵
- 接着13H中断:读软盘数据(只读第2-20个扇区,即内核部分,而引导区位于第1个扇区内)到内存0x8000处。软盘大小:80(磁道)x 18(扇区)x 512 bytes(扇区的大小) x 2(双面)= 1440 x 1024 bytes = 1440 KB = 1.44MB
- 然后设置全局描述符
- 启用A20地址线
- 切换到保护模式
- 跳转到0x8000处,也就是内核代码开始处,内核就是src目录下c文件编译后的代码
内核部分
src/kernel下面有几个文件,都非常简单。loader.asm
; loader.asm
; jmp to C kernel, achieve some function in asm
;
; kernel code segment selector
SEL_KERN_CODE EQU 0x8
; kernel data segment selector
SEL_KERN_DATA EQU 0x10
; vedio memory
SEL_KERN_VEDIO EQU 0x18
; 用户地址起始
USER_BASE EQU 0xc0000000
align 4
[bits 32]
[section .text]
[extern os_main]
[global start]
start:
xor eax, eax
mov ax, SEL_KERN_DATA
mov ds, ax
mov ax, SEL_KERN_DATA
mov es, ax
mov ax, SEL_KERN_VEDIO
mov gs, ax
mov ax, SEL_KERN_DATA
mov ss, ax
mov esp, 0x7c00 ; 联想到bootsect中的org 0x7c00
; mov the kernel to 0x100000
[extern kernstart]
[extern kernend]
mov eax, kernend
mov ecx, kernstart
sub eax, ecx
mov ecx, eax
mov esi, 0x8000
mov edi, 0x100000
cld
rep movsb
jmp dword SEL_KERN_CODE:go
go:
mov edi, (160*3)+0 ; 160*50 line 3 column 1
mov ah, 00001100b ; red color
mov esi, msg
call print
push 0
jmp os_main ; os entry
jmp $ ; halt
print:
add edi, 160
push edi
cld
loop:
lodsb
cmp al, 0
je outloop
mov [gs:edi], ax
add edi, 2
jmp loop
outloop:
pop edi
ret
msg:
db "=== [ OS ENTRY ] ===", 0
; loader.asm
; jmp to C kernel, achieve some function in asm
;
; kernel code segment selector
SEL_KERN_CODE EQU 0x8
; kernel data segment selector
SEL_KERN_DATA EQU 0x10
; vedio memory
SEL_KERN_VEDIO EQU 0x18
; 用户地址起始
USER_BASE EQU 0xc0000000
align 4
[bits 32]
[section .text]
[extern os_main]
[global start]
start:
xor eax, eax
mov ax, SEL_KERN_DATA
mov ds, ax
mov ax, SEL_KERN_DATA
mov es, ax
mov ax, SEL_KERN_VEDIO
mov gs, ax
mov ax, SEL_KERN_DATA
mov ss, ax
mov esp, 0x7c00 ; 联想到bootsect中的org 0x7c00
; mov the kernel to 0x100000
[extern kernstart]
[extern kernend]
mov eax, kernend
mov ecx, kernstart
sub eax, ecx
mov ecx, eax
mov esi, 0x8000
mov edi, 0x100000
cld
rep movsb
jmp dword SEL_KERN_CODE:go
go:
mov edi, (160*3)+0 ; 160*50 line 3 column 1
mov ah, 00001100b ; red color
mov esi, msg
call print
push 0
jmp os_main ; os entry
jmp $ ; halt
print:
add edi, 160
push edi
cld
loop:
lodsb
cmp al, 0
je outloop
mov [gs:edi], ax
add edi, 2
jmp loop
outloop:
pop edi
ret
msg:
db "=== [ OS ENTRY ] ===", 0
它首先将0x8000处的内核代码复制到0x100000处,然后运行到内核入口os_main。
main.c
#include <type.h>
#include <asm.h>
#include <vga.h>
#include <print.h>
#include <debug.h>
void print_ok(void)
{
putchar('[');
vga_setcolor(VGA_COLOR_GREEN, VGA_COLOR_BLACK);
puts("OK");
vga_setcolor(VGA_COLOR_LIGHTGREY, VGA_COLOR_BLACK);
putchar(']');
}
void init(void)
{
vga_init();
print_ok();
puts(" init vga...
");
}
int os_main(void)
{
init();
vga_setcolor(VGA_COLOR_LIGHTBLUE, VGA_COLOR_BLACK);
puts("
");
puts("Hello world! --- OS by bajdcc
");
puts("
");
LOOP:
hlt();
goto LOOP;
return 0;
}
代码的作用就是打印一堆字符。。
边角料
VGA字符显示
打印字符可以通过中断实现,不过,这里还有一种方式,就是outb利用IO操作加上直接操作显存。
#define VGA_CRT_IC 0x3d4 // vga index register port
#define VGA_CRT_DC 0x3d5 // vga data register port
struct vga_char *vga_mem; /* vga[25][80] at 0xb8000 */
struct vga_char color; /* use vag_char structure to store color */
struct point cur;
static void move_cur()
{
uint16_t tmp;
tmp = cur.y * 80 + cur.x;
/* cursor high port to vga index register */
outb( VGA_CRT_IC, 0xe );
outb( VGA_CRT_DC, tmp >> 8 );
/* cursor low port to vga index register */
outb( VGA_CRT_IC, 0xf );
outb( VGA_CRT_DC, tmp );
}
屏幕上跳动的指针要通过outb设置位置,输出字符只要发动相应显存即可。
编写Makefile
# makefile
.PHONY: init run fs fsck clean
.IGNORE: init
MAKE = make -r
AS = nasm
CC = gcc
DEL = rm -f
QEMU = qemu
LD = ld
OBJCPY = objcopy
GDB = cgdb
IMG = qemu-img
MKFS = mkfs.minix
FSCK = fsck.minix
CFLAGS = -c -O0 -Wall -Werror -nostdinc -fno-builtin -fno-stack-protector -funsigned-char
-finline-functions -finline-small-functions -findirect-inlining
-finline-functions-called-once -Iinclude -m32 -ggdb -gstabs+ -fdump-rtl-expand
ROOTFS = bin/rootfs
OBJS = bin/loader.o bin/main.o bin/asm.o bin/vga.o bin/string.o bin/print.o bin/debug.o
# default task
default: Makefile
$(MAKE) bin/floppy.img
# create a 1.44MB floppy include kernel and bootsector
bin/floppy.img: boot/floppy.asm bin/bootsect.bin bin/kernel
$(AS) -I ./bin/ -f bin -l lst/floppy.s $< -o $@
# bootsector
bin/bootsect.bin: boot/bootsect.asm
$(AS) -I ./boot/ -f bin -l lst/bootsect.s $< -o $@
bin/loader.o : src/kernel/loader.asm
$(AS) -I ./boot/ -f elf32 -g -F stabs -l lst/loader.s $< -o $@
# link loader.o and c objfile
# generate a symbol file(kernel.elf) and a flat binary kernel file(kernel)
bin/kernel: script/link.ld $(OBJS)
$(LD) -T$< -melf_i386 -static -o $@.elf $(OBJS) -M>lst/map.map
$(OBJCPY) -O binary $@.elf $@
# compile c file in all directory
bin/%.o: src/*/%.c
$(CC) $(CFLAGS) -c $^ -o $@
#----------------------------------------
# init
init:
mkdir lst
mkdir bin
mkdir $(ROOTFS)
# make a disk with minix v1 file system
fs:
$(DEL) bin/rootfs.img
$(IMG) create -f raw bin/rootfs.img 10M
$(MKFS) bin/rootfs.img -1 -n14
sudo mount -o loop -t minix bin/rootfs.img $(ROOTFS)
mkdir $(ROOTFS)/bin
mkdir $(ROOTFS)/share
sleep 1
sudo umount $(ROOTFS)
# check root file system
fsck:
$(FSCK) -fsl bin/rootfs.img
# run with qemu
run:
$(QEMU) -S -s
-drive file=bin/floppy.img,if=floppy,format=raw
-drive file=bin/rootfs.img,if=ide,format=raw,cyls=18,heads=2,secs=80
-boot a -m 64 &
sleep 1
$(GDB) -x script/gdbinit
# clean the binary file
clean:
$(DEL) bin/*.lst
$(DEL) bin/*.o
$(DEL) bin/*.bin
$(DEL) bin/*.tmp
$(DEL) bin/kernel
$(DEL) bin/kernel.elf
$(DEL) bin/floppy.img
$(DEL) lst/*
bin/floppy.img: boot/floppy.asm bin/bootsect.bin bin/kernel 这行意思是将后者编译后的代码塞进软盘中
make fs 这功能暂时没用过,后面会用,就是挂个硬盘,里面是编译后的程序
make run 需要熟悉qemu命令行
CGDB
target remote localhost:1234
symbol-file bin/kernel.elf
b os_main
c
这是GDB调试的配置文件,b是break中断的缩写,c是continue缩写。