开始自己写内核

根据前面的基础,写一个简单的内核:打印字符然后驻留。

《操作系统真相还原》给出了源代码,我力图自己实践理解,确保有代码,也有运行截图(需要注意运行环境)。

我们需要理解磁盘上文件是如何安排的,1M的物理内存如何使用的,4G的虚拟内存如何使用的?

程序编码,虚拟机运行

  • 代码目录结构

  • 注意上述文件的编译运行在32bit的linux环境下,因为要解析ELF文件,需要保持一致(32位和64位的ELF文件格式,有些数据结构大小,字段可能不同,所以解析会不同)

  • Makefile

.PHONY:build image clean

img=/home/john/os/hd30M.img

mbr_src=mbr.S
loader_src=loader.S

mbr=mbr.bin
loader=loader.bin

mbr_loader:
    nasm -I boot/include/ -o boot/${mbr} boot/${mbr_src}
    nasm -I boot/include/ -o boot/${loader} boot/${loader_src}

build:
    nasm -f elf -o lib/kernel/print.o lib/kernel/print.S
    gcc -I lib/kernel -c -o kernel/main.o  kernel/main.c
    ld -Ttext 0xc0001500 -e main -o kernel/kernel.bin kernel/main.o lib/kernel/print.o

image:
    @-rm -rf $(img)
    bximage -hd -mode="flat" -size=30 -q $(img)
    dd if=./boot/mbr.bin of=$(img) bs=512 count=1 conv=notrunc
    dd if=./boot/loader.bin of=$(img) bs=512 seek=2 count=3 conv=notrunc
    dd if=./kernel/kernel.bin of=$(img) bs=512 seek=9 count=200 conv=notrunc

clean:
    @-rm -rf boot/*.img boot/*.bin boot/*.o /boot/*~
    @-rm -rf lib/*.img lib/*.bin lib/*.o lib/*~
    @-rm -rf lib/kernel/*.img lib/kernel/*.bin lib/kernel/*.o lib/kernel/*~
    @-rm -rf kernel/*.img kernel/*.bin kernel/*.o kernel/*~
    @-rm -rf *.o *.bin *.img *~
  • main.c(kernel)
#include "print.h"
void main(void) {
   put_char('k');
   put_char('e');
   put_char('r');
   put_char('n');
   put_char('e');
   put_char('l');
   put_char('\n');
   put_char('1');
   put_char('2');
   put_char('\b');
   put_char('3');
   while(1);
}

其中\n表示换行,\b表示删除前一字符,最后bochs运行结果图

操作系统说明

磁盘结构,虚拟地址

mbr

被加载到物理地址0x7c00,有BIOS读取磁盘的mbr分区(即磁盘的第一个扇区-512字节)

mbr负责读取磁盘2-4扇区的loader内容,加载在物理内存 可用区域,我们选择了0x9000,mbr结束自己,跳转到loader入口地址

loader

loader建立分段,分页机制等,并读取内核所在的磁盘区域,把内核加载到内存,然后跳转到内核入口处,结束自己

内核代码

内核的kernel.bin有两个地址,一个是ELF文件地址,这个被加载到物理内存可用区域的0x70000;另一个是解析ELF后的内核映像,这个加载到了0x1500(loader设计的内存大小不超过2000字节,0x9000+2000字节 = 0x1d10,然后空了些,内核选择了0x1500开始后的物理内存)

页表中设置了内核低端1MB的虚拟内存与物理内存一一对应

所以,物理地址0x1500对应到内核虚拟地址就是0xc0001500,这也就是内核入口的虚拟地址(Makefile中编译出kernel.bin就设置的这个地址)

文件加载,内核运行

内核文件目前就是main.o和print.o链接的,print使用汇编写的操作了显存,这样可以将文本输出到屏幕上,main直接调用就行了。

print涉及到光标和单字符的显示,这个是基础的显存操作,后面打印字符串,打印整数可以在此函数上基础上封装。

采用了函数调用方式(参数传递,栈变化,函数执行返回等这些需要理解)

打印函数

  • put_str

不断的调用put_char函数,实现打印字符串的功能

;--------------------------------------------
;put_str 通过put_char来打印以0字符结尾的字符串
;--------------------------------------------
;输入:栈中参数为打印的字符串
;输出:无

global put_str
put_str:
;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
   push ebx
   push ecx
   xor ecx, ecx              ; 准备用ecx存储参数,清空
   mov ebx, [esp + 12]          ; 从栈中得到待打印的字符串地址 
.goon:
   mov cl, [ebx]
   cmp cl, 0              ; 如果处理到了字符串尾,跳到结束处返回
   jz .str_over
   push ecx              ; 为put_char函数传递参数
   call put_char
   add esp, 4              ; 回收参数所占的栈空间
   inc ebx              ; 使ebx指向下一个字符
   jmp .goon
.str_over:
   pop ecx
   pop ebx
   ret
  • put_int

put_int 将数字转成字符,存到一个字符串buffer中,然后put_char 当然并未输出10进制的数,而是打印每个字节内容

  • mian.c调用
#include "print.h"
void main(void) {
   put_str("I am kernel\n");
   put_int(0);
   put_char('\n');
   put_int(9);
   put_char('\n');
   put_int(0x00021a3f);
   put_char('\n');
   put_int(0x12345678);
   put_char('\n');
   put_int(0x00000000);
   while(1);
}
  • 结果图

Copyright @doctording all right reserved,powered by Gitbookupdate at: 2020-08-29 14:58:51

results matching ""

    No results matching ""