注:与本系列博客同时同步的还有后面需要学习和研究的FreeRTOS和linux0.11-linux1.0内核代码VV的Linux操作系统内核笔记系列,即使笔者已经自己写了个操作系统了,但是为了能够使博客能读懂,笔者需要把每一个lab和代码打出来做出解释同时笔者也有自己繁重的学习和工作(本科狗),所以进度会非常非常慢
准备工作
Ubuntu16.04-i386 32位操作系统镜像
话不多说,迅雷下载下载地址
- 安装镜像到到虚拟机
安装过程不多赘述,安装完成如图所示:
- 安装ubuntu的一些软件和包
apt-get install docker docker.io docker-compose qemu virtualbox
- 安装IDE
Eclipse的CDT原生支持Makefile工程,而且虚拟机内存占用较小,所以这里我们就用Eclipse CDT,注意是32位的Eclipse表问我为啥安装Eclipse Indigo这种老玩意,因为最新版本的EC并不支持32位。
在我们编写内核的过程中,我们使用GRUB来启动我们的内核。 至于为什么用GRUB,因为它可以设置多系统共存,这样的话你就可以打包多个系统内核同时存在并且启动的镜像文件。
操作系统启动流程
为了直观和形象,我们直接上图
- BIOS(Basic Input/Output System),基本输入输出系统,该系统存储于主板的ROM芯片上,计算机在开机时,会最先读取该系统,然后会有一个加电自检过程,这个过程其实就是检查CPU和内存,计算机最基本的组成单元(控制器、运算器和存储器),还会检查其他硬件,若没有异常就开始加载BIOS程序到内存当中。详细的BIOS功能,这边就不说了,BIOS主要的一个功能就是存储了磁盘的启动顺序,BIOS会按照启动顺序去查找第一个磁盘头的MBR信息,并加载和执行MBR中的Bootloader程序,若第一个磁盘不存在MBR,则会继续查找第二个磁盘(PS:启动顺序可以在BIOS的界面中进行设置),一旦BootLoader程序被检测并加载内存中,BIOS就将控制权交接给了BootLoader程序。
- MBR(Master Boot Record),主引导记录,MBR存储于磁盘的头部,大小为512bytes,其中,446bytes用于存储BootLoader程序,64bytes用于存储分区表信息,最后2bytes用于MBR的有效性检查。
- GRUB(Grand Unified Bootloader),多系统启动程序,其执行过程可分为三个步骤:
- Stage1:这个其实就是MBR,它的主要工作就是查找并加载第二段Bootloader程序(stage2),但系统在没启动时,MBR根本找不到文件系统,也就找不到stage2所存放的位置,因此,就有了stage1_5
- Stage1_5:该步骤就是为了识别文件系统
- Stage2:GRUB程序会根据/boot/grub/grub.conf文件查找Kernel的信息,然后开始加载Kernel程序,当Kernel程序被检测并在加载到内存中,GRUB就将控制权交接给了Kernel程序。
注意!现代操作系统使用了UEFI启动,但是我们现在不说UEFI,请自行忽略
但是这样也需要我们的Boot程序按照Mutileboot 规范来编译内核,才可以被GRUB引导。 按照Mutileboot规范,内核必须在起始的8KB中的(512字节)包含这一个多引导项头(Multiboot header)。 而且,这个多引导项头里面必须有3个4字节对齐的块。
代码语言:javascript复制一个魔术块:包含了魔数[0x1BADB002],是多引导项头结构的定义值。
一个标志块:我们不关心这个块的内容,我们简单设定为0。
一个校检块:校检块,魔术块和标志块的数值的总和必须是0。
我的内核启动代码如下: boot.s
代码语言:javascript复制.set MAGIC, 0x1badb002;GRUB魔术块
.set FLAGS, (1<<0 | 1<<1);GRUB标志块
.set CHECKSUM, -(MAGIC FLAGS);校验块
.section .multboot
.long MAGIC
.long FLAGS
.long CHECKSUM
.section .text
.extern kernel_main;导入kernel_main
.extern system_constructors;导入系统构造函数
.global laoder
loader:
mov $kernel_stack, %esp
call system_constructors
push �x
push �x
call kernel_main
stop:
cli
hlt
jmp stop
.section .bss
.space 2*1024*1024
kernel_stack:
一些code解释:
- CLI:将IF置0,屏蔽掉“可屏蔽中断”,当可屏蔽中断到来时CPU不响应,继续执行原指令
- STI:将IF置1,允许“可屏蔽中断”,中断到来转而处理中断
- HLT:本指令是处理器“暂停”指令。
- JMP:命令跳转指令
- .global .global 用来让一个符号对链接器可见,可以供其他链接对象模块使用。 .global boot 让_start符号成为可见的标示符,这样链接器就知道跳转到程序中的什么地方并开始执行。linux寻找这个 bootbootbootstart标签作为程序的默认进入点。 在汇编和C混合编程中,汇编程序中要使用.global伪操作声明汇编程序为全局的函数,意即可被外部函数调用,同时C程序中要使用extern声明要调用的汇编语言程序。
- .extern .extern XXXX 说明xxxx为外部函数,调用的时候可以遍访所有文件找到该函数并且使用它。
- .long MAGIG .long指示声明变量占用空间,占32位
- .set 给一个全局变量或局部变量赋值
现在建立符号链接来Link我们的所有object文件 linker.ld
代码语言:javascript复制ENTRY(boot)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)
SECTIONS {
. = 0x0100000;
.text :{
*(.muiltboot)
*(.text*)
*(.rodata)
}
.data :
{
start_ctors = .;
KEEP(*(.init_array ));
KEEP(*(SORT_BY_INIT_PRIORITY( .init_array.* )));
end_ctors = .;
*(.data)
}
.bss :
{
*(.bss)
}
/DISCARD/ : {
*(.fini_array*) *(.comment)
}
}
Makefile 没什么好说的,Makefile负责C/C 的 编译依赖过程
代码语言:javascript复制GCCPARAMS = -m32 -W -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-builtin -fno-rtti -fno-exceptions -fno-leading-underscore
ASPPARAMS = --32
LDPARAMS = -melf_i386
GCC = g
ASM = as
LINKER = ld
CFLAGS = -o $@ -c $<
ASMFLAGS = -o $@ $<
LINKERFLAGS = -T $< -o $@
objects = boot.o kernel.o
%.o: %.c
$(GCC) $(GCCPARAMS) $(CFLAGS)
%.o: %.s
$(ASM) $(ASPPARAMS) $(ASMFLAGS)
kernel_lab.bin: linker.ld $(objects)
$(LINKER) $(LINKERFLAGS) $(objects)
all: kernel_lab.bin
echo "build successed"
clean:
rm -rf *.o
rm -rf *.out
rm -rf iso
rm -rf *.iso
rm -rf *.bin
rebuild: clean all
echo "rebuild"
install: kernel_lab.bin
sudo cp $< /boot/kernel_lab.bin
kernel_lab.iso: rebuild
mkdir iso
mkdir iso/boot
mkdir iso/boot/grub
cp kernel_lab.bin iso/boot/
cp boot/grub.cfg iso/boot/grub/grub.cfg
grub-mkrescue -o $@ iso
rm -rf iso
kernel_vm: kernel_lab.iso
(killall virtualbox) || true
virtualbox -startvm "kernel_lab" &
下面是操作系统的主要程序,我们由C 编写,用extern "C"导出我们的函数符号 kernel.cpp
代码语言:javascript复制#include "kernel.h"
//因为我们的操作系统没有TTY IO,所以我们需要重新写一个printf函数
extern "C" void printf(char *str){
u_short *monitor_io_memory=(u_short *)0xb8000;//注意!重点来啦!0xb8000内存地址是显示器地址,往这里写数据就直接能够输出到屏幕上
for(int i=0;str[i]!='