最近在学习操作系统原理,也想尝试下如何编写一个操作系统,就查阅了相关资料,进行一番总结。
网络上有很多伟大的同志们,帮我们详细的讲解了操作系统从电脑加电后到引导启动,都进行了哪些操作。我在这里将整理的资料一一列举:
最基本的当然是从软盘上加载启动的方式了,最为代表的是国外MikeOS的制作教程,这篇博文是其中的翻译:
怎样写一个简单的操作系统?(原文标题:How to write a simple operating system)
其主要实现方式是解析FAT12软盘文件格式,加载其中的数据文件,实现的引导启动。
同样采用软盘引导的方式的:
大家一起写操作系统系列
这里的资料讲解了基本原理:
操作系统的安装与启动基本原理
还有更为神奇的是,还能用C#开发操作系统了:
打破传统,C#也能开发操作系统!
这个叫Cosmos(译为宇宙)的系统,从名字是就霸气威武,其实现方式也很厉害,通过开发了一款汇编器,将C#中间代码IL,编译成了机器码。而且他集成了C#模块化思想,做成了操作系统开发套件的形式,用户可以直接从VS上组织并编译它们。
下面是重头戏,我最推荐使用的方式,依然使用C语言开发,但使用Grub或isolinux进行引导,这样既避免了引导过程的使用汇编语言繁琐的操作硬件,又能直接为你导入C语言进行开发,体验操作系统中最核心的管理操作。
内核代号101 — 动手写自己的内核
这篇文章强烈推荐,实现简单明了,可能是最简单的内核引导方式,我在Github上也按照其方法搭建了一个简单的项目,大家也可以在上面继续开发:
https://github.com/sunxfancy/MoonOS
用SYSLINUX或ISOLINUX制作启动U盘或光盘
这篇文章重点讲解了启动盘的制作方法,我安装其方法,也成功配置成功了一个iso镜像,放置到了虚拟机中引导自己的内核启动,当然linux下有更简单的方式,用这样一句简单的指令也可以实现测试内核的功能,当然,你得先安装qemu虚拟机:
qemu-system-i386 -kernel kernel.elf
另外再推荐一个重要的资源网站,国外的http://wiki.osdev.org站,最权威的操作系统开发帮助网站。
从操作系统的引导谈起
操作系统,也是由伟大的程序猿们码出来的,首先要坚信这一点。在裸机上你无法进行内存的动态分配,多任务的调度,硬件的协调,这些都是需要操作系统进行管理。但操作系统也不是万能的,他也是需要依赖硬件和体系架构的,没有对应的体系,操作系统也无法工作。
操作系统主要就这几大目标:
- 多任务调度,让只有为数不多的核心的CPU分给多个任务跑,同时执行多个进程或线程。
- 内存管理,Intel x86-64架构下,内存是按照分段和分页的形式组织的,所谓分段,就是管理不同的数据段、代码段、栈段等,分页则是为了将不同的进程的虚拟地址分开,让其不互相影响。
- 硬件管理,设计不同种类的硬件驱动,将硬件抽象成较为方便使用的形式,供操作系统上层程序员使用,方便应用开发。
- 提供用户交互手段,无论是GUI还是控制台,都是交互方式。
- 设计系统调用,将用户态程序和内核态完全分开。
明确的操作系统的任务,那么我们要看,如何进行操作系统的开发呢?
当然首先,要把内核引导启动,并切换到32位模式下,运行操作系统的入口函数kmain()
,然后操作系统根据硬件架构,初始化各个部件,加载驱动,加载图像界面和用户交互。
那么我们从引导开始,我的环境是Ubuntu14.04版,如果手写一个引导程序,首先要安装必要的汇编器,这里我们使用nasm,因为新版的intel汇编语法比较简洁好看。
sudo apt-get install nasm
同时安装qemu虚拟机用来测试:
sudo apt-get install qemu
我们打包好iso文件后,我们可以这样用qemu测试iso文件的引导:
qemu-system-i386 -cdrom test.iso -boot d
-boot的参数一定要选d,a是软盘,c是硬盘,d是光盘
当然,如果你只是想测试内核,并且你的内核支持Multiboot格式,那么你也可以像上面讲的方式一样,用一句话引导内核程序:
qemu-system-i386 -kernel kernel.elf
引导程序是什么原理呢?首先操作系统不能自己把自己拉起来,因为没有加电前,电脑内存中是空的。这个过程叫bootstrap,
bootstrap这个词一直译为”自举”,源于英文中的一个搞笑谚语:”想通过提拉鞋带将自己提升离开地面的可笑
企图”,这说明了操作系统的尴尬情况。这个问题是通过硬件电路解决的,主板bios在上电后,会根据bios中的设置顺序扫描几个储存介质,硬盘、光驱、usb等等,找到第一个有引导区的介质,然后将其第一个扇区内的部分加载到0x7c00的位置,然后初始化寄存器,让指向代码的ip寄存器,指向该位置,然后程序开始运行。
如果要编写可引导的程序呢,就要用nasm汇编了,编写一个简单的在裸机上直接跑的程序。
;bootstrap.asm
BITS 16
start:
jmp main
;*********************************************
; Prints a string
; DS=>SI: 0 terminated string
; Changed Register
; AX, SI
;*********************************************
Print:
lodsb
or al, al
jz PrintDone
mov ah, 0eh
int 10h
jmp Print
PrintDone:
ret
;************************************************
; main function
;************************************************
main:
;------------------------------------------------
; code located at 0000:7c00
; adjust segment registers
;------------------------------------------------
cli
mov ax, 0x07c0 ; setup registers to point to our segment. s*16+off = address
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
;------------------------------------------------
; create stack
;------------------------------------------------
mov ax, 0x0000 ; set the stack
mov ss, ax
mov sp, 0xffff
sti ; restore interrupts
;------------------------------------------------
; display loading message
;------------------------------------------------
mov si, msgLoading ; "Loading Boot Image "
call Print
cli ;block interrupts
hlt ;halt the CPU
;*********************************************
; message data
;*********************************************
msgLoading db 0x0d, 0x0a, "Loading Boot Image ", 0x0d, 0x0a, 0x00
;*********************************************
; bootstrap end
;*********************************************
times 510-($-$$) db 0 ; Pad remainder of boot sector with 0s
dw 0xAA55 ; The standard PC boot signature
然后我们可以这样编译这个部分:
nasm -f bin -o bootstrap.bin bootstrap.asm
用dd工具拷贝其到映像的第一个扇区(实际上也只有第一扇区),并输出到cdiso文件夹中,这个文件夹将会被用来构建iso映像:
dd status=noxfer conv=notrunc if=bootstrap.bin of=cdiso/bootstrap.flp
然后用mkisofs工具生成iso,注意一定要使用 -no-emul-boot参数,否则无法引导:
mkisofs -no-emul-boot -o test.iso -b bootstrap.flp cdiso/
测试一下吧:
qemu-system-i386 -cdrom test.iso -boot d
Multiboot引导协议
之前就一直有提到,Multiboot协议,这个是为了简化操作系统的开发难点,让系统引导从系统内核中分离出来而开发的。支持Multiboot引导协议的,可以用grub或syslinux/isolinux进行引导,非常方便快捷,自动帮你把内核载入内存并切换到32位保护模式,内核直接进行初始化即可。
如何使用这个协议并让其自动引导呢?
首先,我们的bootstrap.asm文件需要写成特殊的引导形式,以用来支持该协议:
;bootstrap.asm
;nasm directive - 32 bit
bits 32
section .text
;multiboot spec
align 4
dd 0x1BADB002 ;magic
dd 0x00 ;flags
dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero
global start
extern kmain ;kmain is defined in the c file
start:
call kmain
cli ;block interrupts
hlt ;halt the CPU
注意,这个文件已经是32位程序了,并且可以导入C语言,call kmain
就是为了调用外部C的内核主函数。
当然,调试阶段,最后我们还是将CPU挂起。
然后我们写一个C函数,导入C语言并写几个基本函数:
/*
* kernel.c
*/
/* Check if the bit BIT in FLAGS is set. */
#define CHECK_FLAG(flags,bit) ((flags) & (1 << (bit)))
/* Some screen stuff. */
/* The number of columns. */
#define COLUMNS 80
/* The number of lines. */
#define LINES 24
/* The attribute of an character. */
#define ATTRIBUTE 0xF0
/* The video memory address. */
#define VIDEO 0xB8000
/* Variables. */
/* Save the X position. */
static int xpos;
/* Save the Y position. */
static int ypos;
/* Point to the video memory. */
static volatile unsigned char *video;
static void cls (void);
static void itoa (char *buf, int base, int d);
static void putchar (char c);
void printf (const char *format, ...);
void init() {
}
void kmain(void)
{
char *str = "my first kernel";
video = (unsigned char *) VIDEO;
cls();
printf("%s\n", str);
init();
return;
}
/* Clear the screen and initialize VIDEO, XPOS and YPOS. */
static void
cls (void)
{
int i;
for (i = 0; i < COLUMNS * LINES * 2; i++)
*(video + i) = 0xFF;
xpos = 0;
ypos = 0;
}
/* Convert the integer D to a string and save the string in BUF. If
BASE is equal to 'd', interpret that D is decimal, and if BASE is
equal to 'x', interpret that D is hexadecimal. */
static void
itoa (char *buf, int base, int d)
{
char *p = buf;
char *p1, *p2;
unsigned long ud = d;
int divisor = 10;
/* If %d is specified and D is minus, put `-' in the head. */
if (base == 'd' && d < 0)
{
*p++ = '-';
buf++;
ud = -d;
}
else if (base == 'x')
divisor = 16;
/* Divide UD by DIVISOR until UD == 0. */
do {
int remainder = ud % divisor;
*p++ = (remainder < 10) ? remainder + '0' : remainder + 'a' - 10;
}
while (ud /= divisor);
/* Terminate BUF. */
*p = 0;
/* Reverse BUF. */
p1 = buf;
p2 = p - 1;
while (p1 < p2)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2--;
}
}
/* Put the character C on the screen. */
static void putchar (char c)
{
if (c == '\n' || c == '\r')
{
newline:
xpos = 0;
ypos++;
if (ypos >= LINES)
ypos = 0;
return;
}
*(video + (xpos + ypos * COLUMNS) * 2) = c;
*(video + (xpos + ypos * COLUMNS) * 2 + 1) = ATTRIBUTE;
xpos++;
if (xpos >= COLUMNS)
goto newline;
}
void printf(const char *format, ...) {
char **arg = (char **) &format;
int c;
char buf[20];
arg++;
while ((c = *format++) != 0)
{
if (c != '%')
putchar(c);
else {
char *p;
c = *format++;
switch (c)
{
case 'd':
case 'u':
case 'x':
itoa (buf, c, *((int *) arg++));
p = buf;
goto make_string;
break;
case 's':
p = *arg++;
if (! p)
p = "(null)";
make_string: while (*p)
putchar (*p++);
break;
default:
putchar (*((int *) arg++));
break;
}
}
}
}
这样我们就可以在屏幕输出调试字符串了
不过这时我们还不能编译整个项目,因为我们的程序格式还很特殊,是由C和nasm汇编混合而成的,我们需要写一个链接脚本,然后让链接器生成正确的二进制程序:
/*
* link.ld
*/
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
. = 0x100000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
}
好的我们可以用下面的指令生成内核了:
nasm -f elf32 bootstrap.asm -o bootstrap.o
gcc -m32 -c kernel.c -o kernel.o
ld -m elf_i386 -T link.ld -o kernel.elf bootstrap.o kernel.o
如何只是简单的想测试一下的话,qemu中已经有Multiboot引导的功能了,执行:
qemu-system-i386 -kernel kernel.elf
如何想打包iso文件,可能还需要费点事,我们下载isolinux的源码,然后从其编译后的二进制文件中找几个: ldlinux.c32
, isolinux.bin
, libcom32.c32
, mboot.c32
然后配置一个isolinux.cfg的脚本文件:
DEFAULT test
label test
kernel /isolinux/mboot.c32
append /kernel.elf
然后将这五个文件都丢到cdiso/isolinux/目录下
执行下面的指令生成iso文件:
mkisofs -o test.iso -b isolinux/isolinux.bin -no-emul-boot -boot-load-size 4 -boot-info-table cdiso/
这里我们使用的是新版协议,由于原理的512启动扇区实在是太小了,后来的逐渐改完4*512个字节,作为光盘的启动标准,这也是isolinux需要使用的引导区大小。
再用iso加载测试一下:
qemu-system-i386 -cdrom test.iso -boot d