本系列为小组作业,参考了很多教程,包括ucore、30天自制操作系统、linux内核设计的艺术等内容、以及最重要的是小组里的几位大佬,本篇文章只是记录自己在学习过程中每一步的脚印,并包含了很多相关知识的补充。本系列大约会有五篇文章,可以完全从零基础编写一个十分简单的操作系统内核。
对应labOS版本v1.0
安装qemu
qemu用来模拟i386的硬件cpu环境
sudo apt install qemu-system-i386
安装好之后就可以用qemu-system-i386命令来模拟硬件环境来运行img镜像文件
补充:apt install 直接通过安装包安装的命令文件默认文件位置在/usr/bin ,而通过下载源码文件手动编译生成的命令文件默认位置在/usr/local/bin
安装nasm
nasm是为可移植性与模块化而设计的一个80x86的汇编器
sudo apt install nasm
补充nasm用法
从编写hello.asm到nasm汇编成hello.o文件到ld链接成hello可执行文件
编写hello.asm文件
section .data
hello: db 'hello world!', 10
len: equ 13 ; hello world!\n
section .text
global _start
_start:
mov eax, 4 ; sys_write
mov ebx, 1 ; stdout
mov ecx, hello
mov edx, len
int 80h
mov eax, 1
mov ebx, 0
int 80h
nasm -f elf64 hello.asm
ld hello.o -o hello
./hello
补充Makefile语法
-
命令前面加一个$@ 时,使用make 编译时不会显示此条命令,而如果使用make "V=" 还是会显示出来词条命令。如果命令前面没有$@ 时,即使只用make 编译也会显示出命令信息。 -
$@ 表示目标文件 $^ 表示所有的依赖文件 $< 目标依赖列表中的第一个依赖 $? 所有目标依赖中被修改过的文件 这些符号表示的都是在命令当前代码块第一行用冒号等声明出来的各种类型的文件。 -
dd时使用conv=notrunc 不截断输出文件 -
seek=1 时从文件开头跳过1个快后再开始复制,效果会受conv=notrunc 选项的影响。 -
makefile当生成一个目标文件后就停止了,如果要生成多个目标文件,需要在最开始添加一个标签,比如all 。
补充汇编语法
记录当前代码地址,
记录当前代码地址,
记录当前代码地址,
记录节的开始地址,
记录节的开始地址,
记录节的开始地址,-$$表示当前位置距离开始的相对距离
times 重复汇编,在times后跟着的表达式会被重复汇编指定次数,语法示例times 510-($-$$) db 0
到此环境搭建完成,先创建一个目录labOS,以后都在这个目录里干活了。
编写引导扇区文件boot.s
补充:代码说明
- BITS指令指定nasm产生的代码是被设计运行在16位模式的处理器上还是运行在32位模式的处理器上。
- org指令是告诉汇编程序,在开始执行的时候,将某段机器语言装载到内存中的哪个地址,按道理说bios会主动加载引导扇区的内容到0x7c00,但是如果这里不加org 0x7c00就会报错。
- BIOS是一组固化到计算机内主板上一个ROM芯片上的程序,因此我们写操作系统都是从bios之后的程序开始写,而bios最后的工作就是将磁盘第一扇区加载到0x7c00处,再从7c00处开始执行,因此我们的工作从第一扇区开始,此后我们编写的程序就完全可以不按照正常的内核的工作模式来写(正常的工作模式是bootsect加载setup,然后setup覆盖bootsec等等),而是按照我们自己规定的方式工作。同时需要注意的是,如果我们不覆盖bios在内存低地址初始化好的bios中断,那就可以用他的中断来做一些事情了。
- 另外,bios加载引导扇区到0x7c00时会检测扇区最后是否以0x55\0xaa结尾,这里我们最后要设置一下。
- 还有,我们在“随意”编写引导扇区代码时,可以直接将0x7c00作为sp,因为此时0x7c00一下一段空间里都没有东西,所以可以随便压栈操作。
; boot.s
BITS 16
; this is designed to be the first sector, it's code must be lower than 512B
; cs:ip=0000:7c00
org 0x7c00
; init segment registers
xor ax, ax
mov ds, ax
mov ss, ax
mov es, ax
mov sp, 0x7c00
; print some message, use bios's intrupt vector fucntion
printStr:
mov ax, 0x1301
mov bx, 0x0002
mov cx, len
mov dx, 0x0101
mov bp, msg
int 0x10
msg: db "oh~ hello labOS!", 10, 0
len: equ 18
; padding with 0 to 510B
times 510-($-$$) db 0
; padding with 0x55aa, it represent gurb sector
dw 0xaa55
编写Makefile文件
V := @
OSIMG := labOS.img
all: $(OSIMG)
boot.o: boot.s
$(V)nasm $< -o $@
$(OSIMG): boot.o
$(V)dd if=/dev/zero of=$@ count=100
$(V)dd if=$< of=$@ conv=notrunc
clean:
$(V)rm *.img
$(V)rm *.o
补充:代码说明
创建镜像的过程参考ucore,先创建一个100B大小文件块,在往里面复制引导等等文件。
编译并运行
make
qemu-system-i386 -drive file=labOS.img,format=raw,media=disk -monitor stdio
到此说明引导扇区制作的没有错误,接下来继续制作实模式转保护模式
补充:代码说明
-
因为boot.s这个文件大小不能超过512B,而且这里假设这个文件只能写上面那一点数据,所以我们需要把接下来的一些扇区的东西放到另一个文件里,然后让boot.s中的代码将这些扇区数据读入内存中,这里规定让他读到内存的0x8000处。 -
当打开A20进入保护模式后,寻址方式立刻发生改变,因此在打开指令后需要用jump设置一下cs:ip,因为其中包含了rpl等一些信息,同时寻址方式的改变导致了bios的中断寻址无法正常寻址。 -
jmp dword 0x8:protect 中的0x8就是保护模式下代码段选择子 -
GDT中第一个条目不被使用,这个条目占了8个字节,索引总个数为sizeof(gdt)/8-1。 -
而段描述符中有4bit是type字段,在setup.s代码中定义为不同的宏 %define STA_X 0x8 ; Executable segment
%define STA_E 0x4 ; Expand down (non-executable segments)
%define STA_C 0x4 ; Conforming code segment (executable only)
%define STA_W 0x2 ; Writeable (non-executable segments)
%define STA_R 0x2 ; Readable (executable segments)
%define STA_A 0x1 ; Accessed
-
mov ebp, 0x0 是因为初始的栈帧ebp是多少无所谓,在调用函数时,会有mov ebp, esp -
ld -nostdlib 不连接系统标准启动文件和标准库文件,只把指定的文件传递给连接器。这个选项常用于编译内核、bootloader等程序,它们不需要启动文件、标准库文件。 -
ld -T file 将file作为链接脚本
重新修改boot.s文件
BITS 16
; this is designed to be the first sector, it's code must be lower than 512B
; cs:ip=0000:7c00
org 0x7c00
; init segment registers
xor ax, ax
mov ds, ax
mov ss, ax
mov es, ax
mov sp, 0x7c00
; print some message, use bios's intrupt vector fucntion
printStr:
mov ax, 0x1301
mov bx, 0x0002
mov cx, len
mov dx, 0x0101
mov bp, msg
int 0x10
; read sector to 0x8000
; buffer address => es:bx = es<<4 + bx
next:
mov ax, 0800h
mov es, ax
mov bx, 0
mov ch, 0 ; Cylinder
mov dh, 0 ; Heads
mov cl, 2 ; Sector start number
mov ah, 2 ; read function
mov al, 3 ; read 3 sectors
mov dl, 80h ;
int 13h
; jump to setup.s
jmp word 0000:8000h
; data to print
msg: db "oh~ hello labOS!", 10, 0
len: equ 18
; padding with 0 to 510B
times 510-($-$$) db 0
; padding with 0x55aa, it represent gurb sector
dw 0xaa55
编写boot.s跳转到的setup.s文件
补充:
0x8000~0x81ff留给启动扇区,这里也算启动扇区,所以读到这里完全可以
BITS 16
org 0x8000
; define main function
; main function is in the third sector, 0x8000+512=0x8200
%define c_main 8200h
; define gdt
%define seg_null times 8 db 0
; set_seg(%1, %2, %3), type:, base, lim
%macro set_seg 3
dw (((%3) >> 12) & 0xffff), ((%2) & 0xffff)
db (((%2) >> 16) & 0xff), (0x90 | (%1)), \
(0xC0 | (((%3) >> 28) & 0xf)), (((%2) >> 24) & 0xff)
%endmacro
; macro set_seg's %1 => type
%define STA_X 0x8 ; Executable segment
%define STA_E 0x4 ; Expand down (non-executable segments)
%define STA_C 0x4 ; Conforming code segment (executable only)
%define STA_W 0x2 ; Writeable (non-executable segments)
%define STA_R 0x2 ; Readable (executable segments)
%define STA_A 0x1 ; Accessed
; close intrupt
cli
wait1:
in al, 64h ; 64 port is 8042's status register
test al, 02h
jnz wait1 ; input buffer has data
mov al, 0xd1 ; 0xd1 => port 0x64
out 64h, al ;
wait2:
in al, 64h
test al, 02h
jnz wait2
mov al, 0xdf ; 0xdf => port 0x60
out 60h, al ; 0xdf = 11011111, open A20 gate
; load GDT
lgdt [temp_gdtdesc]
; cr0[0](PE) => 1, enter protect mode
mov eax, cr0
or eax, 1
mov cr0, eax
jmp dword 0x8:protect ; jmpi 0x08 => gdt+1, eip=protect, 0x8 is PROT_MODE_CSEG
align 4 ; 4B align
temp_gdt:
seg_null
set_seg STA_X|STA_R, 0x0, 0xffffffff ; code segment
set_seg STA_W, 0x0, 0xffffffff ; data segment
temp_gdtdesc:
dw 0x17 ; three segments, 3x8-1 = 23 = 0x17 = sizeof(gdt)-1
dd temp_gdt ; gdt start address
BITS 32
protect:
; init data segment registers
mov ax, 10h ; 0x10 is PROT_MODE_DSEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov ebp, 0x0
mov esp, 0x7c00
jmp c_main
补充:代码说明
setup.s代码先关闭中断,然后等待缓冲区数据结束,之后打开了A20,加载了GDT,接着就jmp修改cs段寄存器,这时就完全到了保护模式的32位寻址模式。然后初始化了一下各个段寄存器就跳转到c语言编写的c_main函数中去了
编写main.c文件
void _hlt();
void main(){
while (1){
_hlt();
}
}
void _hlt(){
asm("hlt");
}
编写main.c的链接脚本main.ld
OUTPUT_FORMAT("binary")
OUTPUT_ARCH(i386)
ENTRY(main)
SECTIONS {
. = 0x8200;
.text : {
*(.text .rel.text)
}
.data : {
*(.data)
}
}
编写Makefile文件
V := @
OSIMG := labOS.img
all: $(OSIMG)
boot.o: boot.s
$(V)nasm $< -o $@
setup.o: setup.s
$(V)nasm $< -o $@
main.o: main.c
$(V)gcc main.c -march=i386 -m32 -fno-builtin -fno-PIC -Wall -nostdinc -fno-stack-protector -ffreestanding -c -o main.o
main.out: main.o
$(V)ld -nostdlib -T main.ld $< -o $@
$(OSIMG): boot.o setup.o main.out
$(V)dd if=/dev/zero of=$@ count=100
$(V)dd if=$< of=$@ conv=notrunc
$(V)dd if=setup.o of=$@ seek=1 conv=notrunc
$(V)dd if=main.out of=$@ seek=2 conv=notrunc
clean:
$(V)rm *.img
$(V)rm *.o
$(V)rm *.out
之后就可以编译
make
qemu启动
qemu-system-i386 -drive file=labOS.img,format=raw,media=disk -monitor stdio
这样就结束了,也可以
用gdb调试一下
qemu-system-i386 -s -S -drive file=labOS.img,format=raw,media=disk -monitor stdio
gdb
target remote localhost:1234
set architecture i8086
set disassembly-flavor intel
b *0x8000
c
x/20i $pc
b *0x8200
c
x/20i $pc
|