自己动手写操作系统系列第1篇,从开机加电到切换保护模式

本系列为小组作业,参考了很多教程,包括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

在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
本书在详细分析操作系统原理的基础上,用丰富的实例代码,一步一步地指导读者用C语言和汇编语言编出一个具备操作系统基本功能的操作系统框架。本书不同于其他的理论型书籍,而是提供给读者一个动手实践的路线图。书中讲解了大量在开发操作系统中需注意的细节问题,这些细节不仅能使读者更深刻地认识操作系统的核心原理,而且使整个开发过程少走弯路。全书共分7章。 本书适合各类程序员、程序开发爱好者阅读,也可作为高等院校操作系统课程的实践参考书。 折叠 作品目录 第1章 马上动手一个最小的"操作系统"1 1.1 准备工作1 1.2 10分钟完成的操作系统1 1.3 Boot Sector3 1.4 代码解释3 1.5 水面下的冰山5 1.6 回顾6 第2章 搭建你的工作环境7 2.1 虚拟计算机(Virtual PC)7 2.1.1 Virtual PC初体验8 2.1.2 创建你的第一个Virtual PC9 2.1.3 虚拟软盘研究12 2.1.4 虚拟软盘实战14 2.2 编译器(NASM & GCC)18 2.3 安装虚拟Linux19 2.4 在虚拟Linux上访问Windows文件夹26 2.5 安装虚拟PCDOS26 2.6 其他要素29 2.7 Bochs29 2.7.1 Bochs vs. Virtual PC vs. VMware30 2.7.2 Bochs的使用方法31 2.7.3 用Bochs进行调试33 2.7.4 在Linux上开发34 2.8 总结与回顾36 第3章 保护模式(Protect Mode)37 3.1 认识保护模式37 3.1.1 GDT(Global Descriptor Table) 42 3.1.2 实模式到保护模式,不一般的jmp45 3.1.3 描述符属性47 3.2 保护模式进阶50 3.2.1 海阔凭鱼跃50 3.2.2 LDT(Local Descriptor Table)58 3.2.3 特权级62 3.3 页式存储82 3.3.1 分页机制概述83 3.3.2 编代码启动分页机制84 3.3.3 PDE和PTE85 3.3.4 cr388 3.3.5 回头看代码88 3.3.6 克勤克俭用内存90 3.3.7 进一步体会分页机制100 3.4 中断和异常107 3.4.1 中断和异常机制109 3.4.2 外部中断111 3.4.3 编程操作8259A113 3.4.4 建立IDT116 3.4.5 实现一个中断117 3.4.6 时钟中断试验119 3.4.7 几点额外说明121 3.5 保护模式下的I/O122 3.5.1 IOPL122 3.5.2 I/O许可位图(I/O Permission Bitmap)123 3.6 保护模式小结123 第4章 让操作系统走进保护模式125 4.1 突破512字节的限制125 4.1.1 FAT12126 4.1.2 DOS可以识别的引导盘131 4.1.3 一个最简单的Loader132 4.1.4 加载Loader入内存133 4.1.5 向Loader交出控制权142 4.1.6 整理boot.asm142 4.2 保护模式下的"操作系统"144 第5章 内核雏形146 5.1 用NASM在LinuxHello World146 5.2 再进一步,汇编和C同步使用148 5.3 ELF(Executable and Linkable Format)150 5.4 从Loader到内核155 5.4.1 用Loader加载ELF155 5.4.2 跳入保护模式161 5.4.3 重新放置内核170 5.4.4 向内核交出控制权175 5.4.5 操作系统的调试方法176 5.5 扩充内核184 5.5.1 切换堆栈和GDT184 5.5.2 整理我们的文件夹191 5.5.3 Makefile191 5.5.4 添加中断处理200 5.5.5 两点说明218 5.6 小结219 第6章 进程221 6.1 迟到的进程221 6.2 概述222 6.2.1 进程介绍222 6.2.2 未雨绸缪--形成进程的必要考虑222 6.2.3 参考的代码224 6.3 最简单的进程224 6.3.1 简单进程的关键技术预测225 6.3.2 第一步--ring0→ring1227 6.3.3 第二步--丰富中断处理程序243 6.3.4 进程体设计技巧254 6.4 多进程256 6.4.1 添加一个进程体256 6.4.2 相关的变量和宏257 6.4.3 进程表初始化代码扩充258 6.4.4 LDT260 6.4.5 修改中断处理程序261 6.4.6 添加一个任务的步骤总结263 6.4.7 号外:Minix的中断处理265 6.4.8 代码回顾与整理269 6.5 系统调用280 6.5.1 实现一个简单的系统调用280 6.5.2 get_ticks的应用286 6.6 进程调度292 6.6.1 避免对称--进程的节奏感292 6.6.2 优先级调度总结300 第7章 输入/输出系统302 7.1 键盘302 7.1.1 从中断开始--键盘初体验302 7.1.2 AT、PS/2键盘304 7.1.3 键盘敲击的过程304 7.1.4 解析扫描码309 7.2 显示器325 7.2.1 初识TTY325 7.2.2 基本概念326 7.2.3 寄存器328 7.3 TTY任务332 7.3.1 TTY任务框架的搭建334 7.3.2 多控制台340 7.3.3 完善键盘处理346 7.3.4 TTY任务总结354 7.4 区分任务和用户进程354 7.5 printf357 7.5.1 为进程指定TTY357 7.5.2 printf()的实现358 7.5.3 系统调用write()361 7.5.4 使用printf()363 后记366
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

H4ppyD0g

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值