好吧,您已经知道内核是什么 内核_百度百科
编写操作系统的第一部分是以 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 具有固定数量的内存,地址为0xA0000到0xBFFFF。
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 内核代码。

- #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