用 C 语言创建你自己的内核






4.96/5 (27投票s)
在本文中,我们将创建一个简单的内核,首先打印HelloWorld,然后编写用于在C语言内核中打印数字、键盘I/O、绘图GUI以及井字游戏的功能。
引言
好的,你已经知道什么是内核了 https://en.wikipedia.org/wiki/Kernel_(operating_system)
编写操作系统第一步是使用16位汇编(实模式)编写引导加载程序。
引导加载程序是在任何操作系统运行之前运行的一段程序。
它用于引导其他操作系统,通常每个操作系统都有其特定的引导加载程序集。
请访问以下链接,使用16位汇编创建你自己的引导加载程序
https://createyourownos.blogspot.in/
引导加载程序通常会选择一个特定的操作系统并启动其进程,然后操作系统将自身加载到内存中。
如果你正在编写自己的引导加载程序来加载内核,你需要了解内存的整体寻址/中断以及BIOS。
大多数操作系统都有特定的引导加载程序。
网上有很多可用的引导加载程序。
但是也有一些专有引导加载程序,例如Windows操作系统的Windows Boot Manager或Apple操作系统的BootX。
但是有很多免费和开源的引导加载程序。请参阅比较,
https://en.wikipedia.org/wiki/Comparison_of_boot_loaders
其中最著名的是GNU GRUB - GNU项目为类Unix系统提供的GNU Grand Unified Bootloader软件包。
https://en.wikipedia.org/wiki/GNU_GRUB
我们将使用GNU GRUB来加载我们的内核,因为它支持多操作系统启动。
要求
GNU/Linux :- 任何发行版(Ubuntu/Debian/RedHat 等)。
汇编器:- GNU Assembler (gas),用于汇编汇编语言文件。
GCC:- GNU Compiler Collection,C编译器。任何版本4、5、6、7、8等。
Xorriso:- 一个创建、加载、操作ISO 9660文件系统映像的软件包。(man xorriso)
grub-mkrescue:- 创建一个GRUB救援映像,此软件包内部调用xorriso功能来构建iso映像。
QEMU:- Quick EMUlator,用于在虚拟机中启动我们的内核,而无需重新启动主系统。
使用代码
好了,从头开始编写内核就是要在屏幕上显示一些内容。
所以我们有一个VGA(Visual Graphics Array),一个控制显示的硬件系统。
https://en.wikipedia.org/wiki/Video_Graphics_Array
VGA有固定的内存量,寻址范围是0xA0000到0xBFFFF。
0xA0000用于EGA/VGA图形模式(64 KB)
0xB0000用于单色文本模式(32 KB)
0xB8000用于彩色文本模式和CGA兼容图形模式(32 KB)
首先,你需要一个多引导加载程序文件,它指示GRUB加载它。
必须定义以下字段。
Magic:- 引导加载程序用于识别要加载的内核的头部(起始点)的固定十六进制数字。
flags:- 如果标志字中的位0被设置,那么与操作系统一起加载的所有引导模块必须对齐到页面(4KB)边界。
checksum:- 由引导加载程序专用,其值必须是magic number和flags的总和。
我们不需要其他信息,
但更多细节请访问 https://gnu.ac.cn/software/grub/manual/multiboot/multiboot.pdf
好的,让我们根据以上信息编写一个GAS汇编代码。
我们不需要上面图像中显示的一些字段。
boot.S
# set magic number to 0x1BADB002 to identified by bootloader
.set MAGIC, 0x1BADB002
# set flags to 0
.set FLAGS, 0
# set the checksum
.set CHECKSUM, -(MAGIC + FLAGS)
# set multiboot enabled
.section .multiboot
# define type to long for each data defined as above
.long MAGIC
.long FLAGS
.long CHECKSUM
# set the stack bottom
stackBottom:
# define the maximum size of stack to 512 bytes
.skip 1024
# set the stack top which grows from higher to lower
stackTop:
.section .text
.global _start
.type _start, @function
_start:
# assign current stack pointer location to stackTop
mov $stackTop, %esp
# call the kernel main source
call kernel_entry
cli
# put system in infinite loop
hltLoop:
hlt
jmp hltLoop
.size _start, . - _start
我们定义了一个大小为1024字节的堆栈,由stackBottom和stackTop标识符管理。
然后在_start中,我们存储当前的堆栈指针,并调用内核的主函数(kernel_entry)。
如你所知,每个进程都包含不同的节,如data、bss、rodata和text。
你可以通过在不汇编的情况下编译源代码来查看每个节。
例如:运行以下命令
gcc -S kernel.c
并查看kernel.S文件。
这些节需要内存来存储它们,这个内存大小由链接器映像文件提供。
每段内存都按块大小对齐。
这通常需要将所有对象文件链接在一起形成最终的内核映像。
链接器映像文件提供了为每个节分配多少大小。
信息存储在最终的内核映像中。
如果你在十六进制编辑器中打开最终的内核映像(.bin文件),你会看到很多00字节。
链接器映像文件包含一个入口点(在本例中是我们定义在boot.S文件中的_start)以及具有在BLOCK关键字中定义的大小并按一定间隔对齐的节。
linker.ld
/* entry point of our kernel */
ENTRY(_start)
SECTIONS
{
/* we need 1MB of space atleast */
. = 1M;
/* text section */
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
*(.text)
}
/* read only data section */
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
/* data section */
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
/* bss section */
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON)
*(.bss)
}
}
现在你需要一个配置文件,指示grub加载带有关联映像文件的菜单
grub.cfg
menuentry "MyOS" {
multiboot /boot/MyOS.bin
}
现在让我们编写一个简单的HelloWorld内核代码。
简单:-
kernel_1:-
kernel.h
#ifndef KERNEL_H
#define KERNEL_H
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
#define VGA_ADDRESS 0xB8000
#define BUFSIZE 2200
uint16* vga_buffer;
#define NULL 0
enum vga_color {
BLACK,
BLUE,
GREEN,
CYAN,
RED,
MAGENTA,
BROWN,
GREY,
DARK_GREY,
BRIGHT_BLUE,
BRIGHT_GREEN,
BRIGHT_CYAN,
BRIGHT_RED,
BRIGHT_MAGENTA,
YELLOW,
WHITE,
};
#endif
这里我们使用的是16位VGA缓冲区,在我机器上VGA地址从0xB8000开始,32位从0xA0000开始。
一个指向VGA地址的无符号16位类型终端缓冲区指针。
它有8*16像素的字体大小。
参见上图。
kernel.c
#include "kernel.h"
/*
16 bit video buffer elements(register ax)
8 bits(ah) higher :
lower 4 bits - forec olor
higher 4 bits - back color
8 bits(al) lower :
8 bits : ASCII character to print
*/
uint16 vga_entry(unsigned char ch, uint8 fore_color, uint8 back_color)
{
uint16 ax = 0;
uint8 ah = 0, al = 0;
ah = back_color;
ah <<= 4;
ah |= fore_color;
ax = ah;
ax <<= 8;
al = ch;
ax |= al;
return ax;
}
//clear video buffer array
void clear_vga_buffer(uint16 **buffer, uint8 fore_color, uint8 back_color)
{
uint32 i;
for(i = 0; i < BUFSIZE; i++){
(*buffer)[i] = vga_entry(NULL, fore_color, back_color);
}
}
//initialize vga buffer
void init_vga(uint8 fore_color, uint8 back_color)
{
vga_buffer = (uint16*)VGA_ADDRESS; //point vga_buffer pointer to VGA_ADDRESS
clear_vga_buffer(&vga_buffer, fore_color, back_color); //clear buffer
}
void kernel_entry()
{
//first init vga with fore & back colors
init_vga(WHITE, BLACK);
//assign each ASCII character to video buffer
//you can change colors here
vga_buffer[0] = vga_entry('H', WHITE, BLACK);
vga_buffer[1] = vga_entry('e', WHITE, BLACK);
vga_buffer[2] = vga_entry('l', WHITE, BLACK);
vga_buffer[3] = vga_entry('l', WHITE, BLACK);
vga_buffer[4] = vga_entry('o', WHITE, BLACK);
vga_buffer[5] = vga_entry(' ', WHITE, BLACK);
vga_buffer[6] = vga_entry('W', WHITE, BLACK);
vga_buffer[7] = vga_entry('o', WHITE, BLACK);
vga_buffer[8] = vga_entry('r', WHITE, BLACK);
vga_buffer[9] = vga_entry('l', WHITE, BLACK);
vga_buffer[10] = vga_entry('d', WHITE, BLACK);
}
vga_entry()函数返回的值是uint16类型,通过高亮显示字符以彩色打印。
该值存储在缓冲区中,以在屏幕上显示字符。
首先,让我们将指针vga_buffer指向VGA地址0xB8000。
段:0xB800 & 偏移:0(我们的索引变量(vga_index))
现在你有一个VGA数组,你只需要根据屏幕上要打印的内容为数组的每个索引分配特定值,就像我们通常为数组赋值一样。
查看上面打印HelloWorld每个字符到屏幕的代码。
好的,让我们编译源代码。
在终端输入sh run.sh命令。
run.sh
#assemble boot.s file
as --32 boot.s -o boot.o
#compile kernel.c file
gcc -m32 -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra
#linking the kernel with kernel.o and boot.o files
ld -m elf_i386 -T linker.ld kernel.o boot.o -o MyOS.bin -nostdlib
#check MyOS.bin file is x86 multiboot file or not
grub-file --is-x86-multiboot MyOS.bin
#building the iso file
mkdir -p isodir/boot/grub
cp MyOS.bin isodir/boot/MyOS.bin
cp grub.cfg isodir/boot/grub/grub.cfg
grub-mkrescue -o MyOS.iso isodir
#run it in qemu
qemu-system-x86_64 -cdrom MyOS.iso
请确保你已安装构建内核所需的所有软件包。
输出是:-
正如你所看到的,为VGA缓冲区逐个赋值是很繁琐的,所以我们可以编写一个函数来实现这一点,该函数可以将我们的字符串打印到屏幕上(意味着将字符串中的每个字符值分配给VGA缓冲区)。
kernel_2 :-
kernel.h
#ifndef KERNEL_H
#define KERNEL_H
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
#define VGA_ADDRESS 0xB8000
#define BUFSIZE 2200
uint16* vga_buffer;
#define NULL 0
enum vga_color {
BLACK,
BLUE,
GREEN,
CYAN,
RED,
MAGENTA,
BROWN,
GREY,
DARK_GREY,
BRIGHT_BLUE,
BRIGHT_GREEN,
BRIGHT_CYAN,
BRIGHT_RED,
BRIGHT_MAGENTA,
YELLOW,
WHITE,
};
#endif
digit_ascii_codes 是字符0到9的十六进制值。当我们想在屏幕上打印它们时需要它们。vga_index是我们的VGA数组索引。当值被分配给该索引时,vga_index会增加。要打印一个32位整数,首先需要将其转换为字符串,然后打印字符串。
BUFSIZE是我们的VGA限制。要打印新行,您必须根据像素字体大小跳过VGA指针(vga_buffer)中的一些字节。
为此,我们需要另一个变量来存储当前行索引(next_line_index)。
#include "kernel.h"
//index for video buffer array
uint32 vga_index;
//counter to store new lines
static uint32 next_line_index = 1;
//fore & back color values
uint8 g_fore_color = WHITE, g_back_color = BLUE;
//digit ascii code for printing integers
int digit_ascii_codes[10] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};
/*
16 bit video buffer elements(register ax)
8 bits(ah) higher :
lower 4 bits - forec olor
higher 4 bits - back color
8 bits(al) lower :
8 bits : ASCII character to print
*/
uint16 vga_entry(unsigned char ch, uint8 fore_color, uint8 back_color)
{
uint16 ax = 0;
uint8 ah = 0, al = 0;
ah = back_color;
ah <<= 4;
ah |= fore_color;
ax = ah;
ax <<= 8;
al = ch;
ax |= al;
return ax;
}
//clear video buffer array
void clear_vga_buffer(uint16 **buffer, uint8 fore_color, uint8 back_color)
{
uint32 i;
for(i = 0; i < BUFSIZE; i++){
(*buffer)[i] = vga_entry(NULL, fore_color, back_color);
}
next_line_index = 1;
vga_index = 0;
}
//initialize vga buffer
void init_vga(uint8 fore_color, uint8 back_color)
{
vga_buffer = (uint16*)VGA_ADDRESS;
clear_vga_buffer(&vga_buffer, fore_color, back_color);
g_fore_color = fore_color;
g_back_color = back_color;
}
/*
increase vga_index by width of row(80)
*/
void print_new_line()
{
if(next_line_index >= 55){
next_line_index = 0;
clear_vga_buffer(&vga_buffer, g_fore_color, g_back_color);
}
vga_index = 80*next_line_index;
next_line_index++;
}
//assign ascii character to video buffer
void print_char(char ch)
{
vga_buffer[vga_index] = vga_entry(ch, g_fore_color, g_back_color);
vga_index++;
}
uint32 strlen(const char* str)
{
uint32 length = 0;
while(str[length])
length++;
return length;
}
uint32 digit_count(int num)
{
uint32 count = 0;
if(num == 0)
return 1;
while(num > 0){
count++;
num = num/10;
}
return count;
}
void itoa(int num, char *number)
{
int dgcount = digit_count(num);
int index = dgcount - 1;
char x;
if(num == 0 && dgcount == 1){
number[0] = '0';
number[1] = '\0';
}else{
while(num != 0){
x = num % 10;
number[index] = x + '0';
index--;
num = num / 10;
}
number[dgcount] = '\0';
}
}
//print string by calling print_char
void print_string(char *str)
{
uint32 index = 0;
while(str[index]){
print_char(str[index]);
index++;
}
}
//print int by converting it into string
//& then printing string
void print_int(int num)
{
char str_num[digit_count(num)+1];
itoa(num, str_num);
print_string(str_num);
}
void kernel_entry()
{
//first init vga with fore & back colors
init_vga(WHITE, BLACK);
/*call above function to print something
here to change the fore & back color
assign g_fore_color & g_back_color to color values
g_fore_color = BRIGHT_RED;
*/
print_string("Hello World!");
print_new_line();
print_int(123456789);
print_new_line();
print_string("Goodbye World!");
}
正如你所看到的,为每个值调用每个函数来显示值是很繁琐的,这就是为什么C编程提供了一个printf()函数,它带有格式说明符,可以在每个说明符和文字(如\n、\t、\r等)处将特定值打印/设置到标准输出设备。
键盘:-
对于键盘I/O,使用端口号0x60,并配合in/out指令。下载键盘的kernel_source代码。它从用户读取按键,并将它们显示在屏幕上。
#ifndef KEYBOARD_H
#define KEYBOARD_H
#define KEYBOARD_PORT 0x60
#define KEY_A 0x1E
#define KEY_B 0x30
#define KEY_C 0x2E
#define KEY_D 0x20
#define KEY_E 0x12
#define KEY_F 0x21
#define KEY_G 0x22
#define KEY_H 0x23
#define KEY_I 0x17
#define KEY_J 0x24
#define KEY_K 0x25
#define KEY_L 0x26
#define KEY_M 0x32
#define KEY_N 0x31
#define KEY_O 0x18
#define KEY_P 0x19
#define KEY_Q 0x10
#define KEY_R 0x13
#define KEY_S 0x1F
#define KEY_T 0x14
#define KEY_U 0x16
#define KEY_V 0x2F
#define KEY_W 0x11
#define KEY_X 0x2D
#define KEY_Y 0x15
#define KEY_Z 0x2C
#define KEY_1 0x02
#define KEY_2 0x03
#define KEY_3 0x04
#define KEY_4 0x05
#define KEY_5 0x06
#define KEY_6 0x07
#define KEY_7 0x08
#define KEY_8 0x09
#define KEY_9 0x0A
#define KEY_0 0x0B
#define KEY_MINUS 0x0C
#define KEY_EQUAL 0x0D
#define KEY_SQUARE_OPEN_BRACKET 0x1A
#define KEY_SQUARE_CLOSE_BRACKET 0x1B
#define KEY_SEMICOLON 0x27
#define KEY_BACKSLASH 0x2B
#define KEY_COMMA 0x33
#define KEY_DOT 0x34
#define KEY_FORESLHASH 0x35
#define KEY_F1 0x3B
#define KEY_F2 0x3C
#define KEY_F3 0x3D
#define KEY_F4 0x3E
#define KEY_F5 0x3F
#define KEY_F6 0x40
#define KEY_F7 0x41
#define KEY_F8 0x42
#define KEY_F9 0x43
#define KEY_F10 0x44
#define KEY_F11 0x85
#define KEY_F12 0x86
#define KEY_BACKSPACE 0x0E
#define KEY_DELETE 0x53
#define KEY_DOWN 0x50
#define KEY_END 0x4F
#define KEY_ENTER 0x1C
#define KEY_ESC 0x01
#define KEY_HOME 0x47
#define KEY_INSERT 0x52
#define KEY_KEYPAD_5 0x4C
#define KEY_KEYPAD_MUL 0x37
#define KEY_KEYPAD_Minus 0x4A
#define KEY_KEYPAD_PLUS 0x4E
#define KEY_KEYPAD_DIV 0x35
#define KEY_LEFT 0x4B
#define KEY_PAGE_DOWN 0x51
#define KEY_PAGE_UP 0x49
#define KEY_PRINT_SCREEN 0x37
#define KEY_RIGHT 0x4D
#define KEY_SPACE 0x39
#define KEY_TAB 0x0F
#define KEY_UP 0x48
#endif
inb()从指定端口接收字节并返回。
outb()将字节发送到指定端口。
uint8 inb(uint16 port)
{
uint8 ret;
asm volatile("inb %1, %0" : "=a"(ret) : "d"(port));
return ret;
}
void outb(uint16 port, uint8 data)
{
asm volatile("outb %0, %1" : "=a"(data) : "d"(port));
}
char get_input_keycode()
{
char ch = 0;
while((ch = inb(KEYBOARD_PORT)) != 0){
if(ch > 0)
return ch;
}
return ch;
}
/*
keep the cpu busy for doing nothing(nop)
so that io port will not be processed by cpu
here timer can also be used, but lets do this in looping counter
*/
void wait_for_io(uint32 timer_count)
{
while(1){
asm volatile("nop");
timer_count--;
if(timer_count <= 0)
break;
}
}
void sleep(uint32 timer_count)
{
wait_for_io(timer_count);
}
void test_input()
{
char ch = 0;
char keycode = 0;
do{
keycode = get_input_keycode();
if(keycode == KEY_ENTER){
print_new_line();
}else{
ch = get_ascii_char(keycode);
print_char(ch);
}
sleep(0x02FFFFFF);
}while(ch > 0);
}
void kernel_entry()
{
init_vga(WHITE, BLUE);
print_string("Type here, one key per second, ENTER to go to next line");
print_new_line();
test_input();
}
每个键码都通过get_ascii_char()函数转换为其ASCII字符。
绘图GUI:-
下载用于绘制旧系统(如DOSBox等)中使用的框的内核源代码(kernel_source/GUI/)
井字游戏:-
我们有打印代码、键盘I/O处理和使用框绘图字符的GUI。所以让我们在内核中编写一个简单的井字游戏,可以在任何PC上运行。
下载内核源代码,kernel_source/Tic-Tac-Toe。
如何玩
使用箭头键(上、下、左、右)移动白框在单元格之间,然后按空格键选择该单元格。
红色代表玩家1的框,蓝色代表玩家2的框。
查看“轮到谁”(Turn)以了解哪个玩家轮流选择单元格。(Turn: Player 1)
如果你在实际硬件上运行此程序,请增加tic_tac_toe.c中launch_game()函数以及kernel.c中kernel_entry()函数中的sleep()函数的值,这样它就能正常工作,并且不会太快。我使用了0x2FFFFFFF。
有关从零开始构建操作系统、操作系统计算器以及操作系统底层图形的更多信息,请参阅此链接。
参考文献
- http://wiki.osdev.org/Expanded_Main_Page
- http://www.brokenthorn.com/
- http://mikeos.sourceforge.net/
- https://github.com/pritamzope/OS