65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (27投票s)

2018年1月13日

CPOL

7分钟阅读

viewsIcon

177617

downloadIcon

8589

在本文中,我们将创建一个简单的内核,首先打印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有固定的内存量,寻址范围是0xA00000xBFFFF

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.claunch_game()函数以及kernel.c中kernel_entry()函数中的sleep()函数的值,这样它就能正常工作,并且不会太快。我使用了0x2FFFFFFF。

 

有关从零开始构建操作系统、操作系统计算器以及操作系统底层图形的更多信息,请参阅此链接。

参考文献

 

© . All rights reserved.