write in front
Project address: https://github.com/bajdcc/MiniOS
I remember that I modified (actually copied) a piece of SilverRainZ/OS67 code last year. It is no problem to run it with bochs, but it always reports an error with qemu. The key is that qemu is easy to debug. Therefore, I deleted the code in anger, kept the most concise part, and then added functions slowly. During the coding process, I also found some comment problems in OS67. I directly pasted the data URL in the code comments for easy reading.
So what does this minimal kernel look like?
Introduction to the kernel
The directory is divided into:
- boot boot file
- include header files
- script script
- src/kernel kernel code
- usr/logo.txt system logo
- * makefile
The parts in bold are more important.
All codes are stored in a floppy disk. The format of the floppy disk after compilation is: [0-200H] boot item, [200H-] kernel, and the rest is filled with zeros.
Boot section
The system kernel we compiled comes with a boot file, which is ultimately stored in a floppy disk (Floppy) in binary form. When qemu starts, it reads the floppy disk and executes the code.
Boot files are written in assembly.
; 引导扇区 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)是'\r \n'
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
The above code does a few things:
- The code above at times 18 db 0: mainly the parameters to fill the floppy disk
- _print16: used to output
- Then enter the entry
- 10H interrupt: set VGA display mode to 16-color 80x25 matrix
- Then 13H interrupt: read the floppy disk data ( read only the 2-20th sector, that is, the kernel part, and the boot area is located in the 1st sector ) to the memory 0x8000. Floppy disk size: 80 (track) x 18 (sector) x 512 bytes (sector size) x 2 (double-sided) = 1440 x 1024 bytes = 1440 KB = 1.44MB
- Then set the global descriptor
- Enable A20 address line
- switch to protected mode
- Jump to 0x8000, which is the beginning of the kernel code, the kernel is the compiled code of the c file in the src directory
kernel part
There are several files under src/kernel, all very simple.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
It first copies the kernel code at 0x8000 to 0x100000, and then runs to the kernel entry 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...\n");
}
int os_main(void)
{
init();
vga_setcolor(VGA_COLOR_LIGHTBLUE, VGA_COLOR_BLACK);
puts("\n");
puts("Hello world! --- OS by bajdcc \n");
puts("\n");
LOOP:
hlt();
goto LOOP;
return 0;
}
What the code does is print a bunch of characters. .
Leftovers
VGA character display
Printing characters can be achieved through interrupts, but there is another way here, that is, outb uses IO operations plus direct operation of video memory.
#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 );
}
The beating pointer on the screen should be set through outb, and the output character only needs to activate the corresponding video memory.
Write Makefiles
# 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 [email protected] $(OBJS) -M>lst/map.map
$(OBJCPY) -O binary [email protected] $@
# 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 This line means to put the compiled code of the latter into the floppy disk
make fs This function has not been used for the time being, and will be used later, just hang it Hard disk, which is the compiled program
make run need to be familiar with qemu command line
CGDB
target remote localhost:1234
symbol-file bin/kernel.elf
b os_main
c
This is the configuration file for GDB debugging, b is the abbreviation for break, and c is the abbreviation for continue.
Backed up by https://zhuanlan.zhihu.com/p/25819125 .