用C语言编写你自己内核

介绍

好吧,您已经知道内核是什么 内核_百度百科

编写操作系统的第一部分是以 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 项目的 GNU Grand Unified Bootloader 包,用于类 Unix 系统。

                 https://en.wikipedia.org/wiki/GNU_GRUB

我们将使用 GNU GRUB 来加载我们的内核,因为它支持许多操作系统的多重引导。

 

要求

GNU/Linux :-  任何发行版(Ubuntu/Debian/RedHat 等)。
Assembler :-  GNU Assembler( gas ) 用于组装汇编语言文件。
GCC :-   GNU 编译器集合,C 编译器。任何版本 4、5、6、7、8 等
Xorriso :-  创建、加载、操作 ISO 9660 文件系统映像的包。(man xorriso)
grub-mkrescue :-  制作 GRUB 救援映像,此包在内部调用 xorriso构建iso映像的功能。
QEMU :-  快速 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 :-如果设置了 flags 字中的位 0,则与操作系统一起加载的所有引导模块必须在页面 (4KB) 边界上对齐。
校验和:-引导加载程序用于特殊用途,其值必须是魔术编号和标志的总和。

我们不需要其他信息, 
但更多详细信息  https://www.gnu.org/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)。

如您所知,每个流程都由不同的部分组成,例如数据、bss、rodata 和文本。
您可以通过编译源代码而不组装它来查看每个部分。

例如:运行以下命令
        gcc -S kernel.c
      并查看 kernel.S 文件。

而这部分需要一个内存来存储它们,这个内存大小由链接器映像文件提供。
每个内存都与每个块的大小对齐。
它主要需要将所有目标文件链接在一起以形成最终的内核映像。
链接器图像文件提供了应该为每个部分分配多少大小。
信息存储在最终的内核映像中。
如果您在 hexeditor 中打开最终的内核映像(.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.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

Segment : 0xB800 & Offset : 0(our index variable(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 和输入/输出指令。从键盘下载 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 字符。

 

制图图形用户界面:-

下载 DOSBox 等旧系统中使用的绘图框的 kernel_source (kernel_source/GUI/)

 

井字游戏:-

我们有打印代码、键盘 I/O 处理和使用绘图字符的 GUI。所以让我们在内核中编写一个简单的井字游戏,可以在任何 PC 上运行。

载 kernel_source 代码,kernel_source/Tic-Tac-Toe。

怎么玩 :

使用箭头键(上、下、左、右)在单元格之间移动白框,然后按空格键选择该单元格。

玩家 1 的盒子为红色,玩家 2 的盒子为蓝色。

请参阅轮到哪个玩家轮到选择单元格。(轮到:玩家 1)

如果您在实际硬件上运行此程序,则增加 tic_tac_toe.c 中的 launch_game() 和 kernel.c 中的 kernel_entry() 中的 sleep() 函数的值,以便正常工作不会太快。我使用了 0x2FFFFFFF。

 

有关从头开始的操作系统、操作系统计算器和操作系统中的低级图形的更多信息。

源代码链接https://download.csdn.net/download/qq_20173195/86500517

参考

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Meta.Qing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值