1.基础-环境配置与开发工具
本机环境:Ubuntu22.04
需要的开发工具
编译器:gcc
链接器:ld
汇编编译器:nasm
虚拟机:qemu
参考:
hurley25/hurlex-doc: hurlex 小内核分章节代码和文档 (github.com)
1.1qemu qemu安装:我之前安装了qemu8.0.0 故就不重新安装了
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
这个意思是后面直接可以用qemu代替 qemu-system-i386,会方便一些 但我觉得没有必要
1.2 Makefile 文件
给出详细的注释,项目基本上以这个Makefile为主
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 C_SOURCES = $(shell find . -name "*.c") C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES) ) S_SOURCES = $(shell find . -name "*.s") S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES) ) CC = gcc LD = ld ASM = nasm C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector -I include LD_FLAGS = -T scripts/kernel.ld -m elf_i386 -nostdlib ASM_FLAGS = -f elf -g -F stabs all: $(S_OBJECTS) $(C_OBJECTS) link update_image .c.o: @echo 编译代码文件 $< ... $(CC) $(C_FLAGS) $< -o $@ .s.o: @echo 编译汇编文件 $< ... $(ASM) $(ASM_FLAGS) $< link: @echo 链接内核文件... $(LD) $(LD_FLAGS) $(S_OBJECTS) $(C_OBJECTS) -o hx_kernel .PHONY :cleanclean: $(RM) $(S_OBJECTS) $(C_OBJECTS) hx_kernel .PHONY :update_imageupdate_image: sudo mount floppy.img /mnt/kernel sudo cp hx_kernel /mnt/kernel/hx_kernel sleep 1 sudo umount /mnt/kernel .PHONY :mount_imagemount_image: sudo mount floppy.img /mnt/kernel .PHONY :umount_imageumount_image: sudo umount /mnt/kernel .PHONY :qemuqemu: qemu -fda floppy.img -boot a .PHONY :bochsbochs: bochs -f scripts/bochsrc.txt .PHONY :debugdebug: qemu -S -s -fda floppy.img -boot a & sleep 1 cgdb -x scripts/gdbinit
1.3 kernel.ld 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 ENTRY(start) SECTIONS { . = 0x100000 ; .text : { *(.text) . = ALIGN(4096 ); } .data : { *(.data) *(.rodata) . = ALIGN(4096 ); } .bss : { *(.bss) . = ALIGN(4096 ); } .stab : { *(.stab) . = ALIGN(4096 ); } .stabstr : { *(.stabstr) . = ALIGN(4096 ); } /DISCARD/ : { *(.comment) *(.eh_frame) } }
复习一下
.text 代码段
.data 已初始化数据段
.bss 未初始化数据段
.stab 调试符号表
.stabstr 字符串表
2.启动过程 grub 和 multiboot
模拟内核 是 32位的 地址寻址线也为32位 可以寻址 2的32次方 4GB地址空间
启动过程,在bios 初始化设备完成后,读取存储设备的第一个扇区如果第一个扇区512个字节的最后两个字节是0x55 和 0xAA 那么该存储设备就是可以启动的
为什么 要使用 grub 而不编写 bootloader呢 因为打算将这个内核与其他win Linux系统共存 所以使用grub (GRand Unified Bootloader) 从而需要了解 multiloader
3.Hello OS Kernel 3.1 编译参数 解释
C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector -I include
-m32 生成32位代码
-ggdb 和 -gstabs+ 添加相关的调试信息
-nostdinc 不包含C语言的标准库的头文件
-fno-builtin gcc 不主动使用自己的内建函数 除非显示的声明
-fno-stack-protector 不使用栈保护
LD_FLAGS = -T scripts/kernel.ld -m elf_i386 -nostdlib
-T… 使用我们自己的链接器脚本
-m elf_i386 生成i386平台下的elf 格式的 可执行文件,这是Linux 下的可执行文件格式
-nostdlib 不使用 c语言标准库
3.2 启动镜像制作
作者使用 软盘制作,相对于硬盘 比较简单
并且使用FAT12 用作文件系统
3.3 boot.s 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 ; ; boot.s ; MBOOT_HEADER_MAGIC equ 0x1BADB002 ; Multiboot 魔数,由规范决定的 MBOOT_PAGE_ALIGN equ 1 << 0 ; 0 号位表示所有的引导模块将按页(4KB)边界对齐 MBOOT_MEM_INFO equ 1 << 1 ; 1 号位通过 Multiboot 信息结构的 mem_* 域包括可用内存的信息 ; 定义我们使用的 Multiboot 的标记 MBOOT_HEADER_FLAGS equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO ; 域checksum是一个32位的无符号值,当与其他的magic域(也就是magic和flags)相加时, ; 要求其结果必须是32位的无符号值 0 (即magic + flags + checksum = 0) MBOOT_CHECKSUM equ - (MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS) ; 符合Multiboot规范的 OS 映象需要这样一个 magic Multiboot 头 ; Multiboot 头的分布必须如下表所示: ; ---------------------------------------------------------- ; 偏移量 类型 域名 备注 ; ; 0 u32 magic 必需 ; 4 u32 flags 必需 ; 8 u32 checksum 必需 ; ;-------------------------------- ; 代码真正开始 ;----------------- [BITS 32] ; 所有代码以 32-bit 的方式编译 section .text ; 代码段从这里开始 ; 在代码段的起始位置设置符合 Multiboot 规范的标记 dd MBOOT_HEADER_MAGIC ; GRUB 会通过这个魔数判断该映像是否支持 dd MBOOT_HEADER_FLAGS ; GRUB 的一些加载时选项,其详细注释在定义处 dd MBOOT_CHECKSUM ; 检测数值,其含义在定义处 [GLOBAL start] ; 内核代码入口,此处提供该声明给 ld 链接器 [GLOBAL glb_mboot_ptr] ; 全局的 struct multiboot * 变量 [EXTERN kern_entry] ; 声明内核 C 代码的入口函数 start: cli ; 此时还没有设置好保护模式的中断处理,要关闭中断 ; 所以必须关闭中断 mov esp, STACK_TOP ; 设置内核栈地址 mov ebp, 0 ; 帧指针修改为 0 and esp, 0FFFFFFF0H ; 栈地址按照16字节对齐 mov [glb_mboot_ptr], ebx ; 将 ebx 中存储的指针存入全局变量 call kern_entry ; 调用内核入口函数 stop: hlt ; 停机指令,什么也不做,可以降低 CPU 功耗 jmp stop ; 到这里结束,关机什么的后面再说 ;----------------------------------------------------------------------------- section .bss ; 未初始化的数据段从这里开始 stack: resb 32768 ; 这里作为内核栈 glb_mboot_ptr: ; 全局的 multiboot 结构体指针 resb 4 STACK_TOP equ $-stack-1 ; 内核栈顶,$ 符指代是当前地址 ;-----------------------------------------------------------------------------
multiboot 的头部 必须包含以下三个:
magic number : 必须包含一个固定的数 – 0xABADB002 用于引导程序识别兼容multi内核
flags :标志引导程序需要提供哪些额外的信息 是否需要内核对齐 是否需要内存信息等
checksum:校验和 用来确保 要求其结果必须是32位的无符号值 0 (即magic + flags + checksum = 0)
3.4 入口函数实现
init/entry.c
1 2 3 int kern_entry () { return 0 ; }
inlcude/types.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ifndef INCLUDE_TYPES_H_ #define INCLUDE_TYPES_H_ #ifndef NULL #define NULL 0 #endif #ifndef TRUE #define TRUE 1 #define FALSE 0 #endif typedef unsigned int uint32_t ;typedef int int32_t ;typedef unsigned short uint16_t ;typedef short int16_t ;typedef unsigned char uint8_t ;typedef char int8_t ;#endif
首先make
然后执行 make qemu
make 的时候 可能会报错 sudo mount floppy.img /mnt/kernel mount: /mnt/kernel: mount point does not exist.
这个时候直接去 /mnt 下创建kernel文件夹就行 提示权限不够就用sudo mkdir kernel
此时出现的是空白
执行 make qemu
注意!!! 在vscode 下终端执行会报错
需要去终端执行
解决方案见:qemu-system-i386 库文件libpthread.so.0未定义符号_undefined symbol:libc_pthread_init-CSDN博客
在entry中添加下面代码会显示 hello OS kernel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include "typers.h" int kern_entry () { uint8_t *input = (uint8_t *)0xB8000 ; uint8_t color = (0 << 4 ) | (15 & 0x0F ); *input++ = 'H' ; *input++ = color; *input++ = 'e' ; *input++ = color; *input++ = 'l' ; *input++ = color; *input++ = 'l' ; *input++ = color; *input++ = 'o' ; *input++ = color; *input++ = ',' ; *input++ = color; *input++ = ' ' ; *input++ = color; *input++ = 'O' ; *input++ = color; *input++ = 'S' ; *input++ = color; *input++ = ' ' ; *input++ = color; *input++ = 'K' ; *input++ = color; *input++ = 'e' ; *input++ = color; *input++ = 'r' ; *input++ = color; *input++ = 'n' ; *input++ = color; *input++ = 'e' ; *input++ = color; *input++ = 'l' ; *input++ = color; *input++ = '!' ; *input++ = color; return 0 ; }
4.字符模式下的显卡驱动
所有在PC上工作的显卡 在加点初始化之后都会自动初始化到 80*25 文本模式,表示屏幕被划分成了25行,每行可以显示80个字符
所以一屏可以显示2000个字符 0xB8000 - 0xBFFFF这个地址段 便是映射到文本模式的显存的
内码:定义了字符在内存中存储的形式
对应关系从0xB8000 开始 每两个字节表示屏幕上显示一个字符。这两个字节前一个字节是显示字符串的ASCII 码,后一个是控制这个字符颜色和属性的控制信息
4.1 端口读写函数的实现
libs/common.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "common.h" inline void outb (uint16_t port, uint8_t value) { asm volatile ("outb %1, %0" :: "dN" (port), "a" (value)) ; } inline uint8_t inb (uint16_t port) { uint8_t ret; asm volatile ("inb %1, &0" : "=a" (ret) : "dN" (port)) ; return ret; } inline uint16_t inw (uint16_t port) { uint16_t ret; asm volatile ("inw %1, %0" : "=a" (ret) : "dN" (port)) ; return ret; }
采用c语言配合汇编代码编写
outb %1, %0
输出指令 像端口发送数据 %1 和 %0 是操作占位符,对应后面的输入输出操作列表
dN
(port):告诉编译器 使用dx寄存器 或者一个立即数来存放端口号 port
a (value)
:约束使用指定寄存器ax 存放要输出的值 value
GCC 内嵌汇编语法
1 asm volatile ( AssemblerTemplate : OutputOperands : InputOperands : Clobbers ) ;
AssemblerTemplate
是实际的汇编指令。
OutputOperands
是输出操作数,指示汇编代码写入的变量。
InputOperands
是输入操作数,指示汇编代码读取的变量。
Clobbers
告诉编译器这段汇编代码可能会修改的寄存器或内存。
inline 用于提示编译器将函数的定义直接插入到函数调用的位置,而不是通过函数调用的 方式执行 好处:
减少函数调用的开销 — 程序本身较小的时候 省略调用过程减少开销
优化程序性能 — 函数体直接插入调用位置,编译器更容易优化
避免链接问题
提升模块化 — 小的 频繁调用的函数定义为inline
支持递归
宏定义 没有参数类型检查喔 inline 有
include/common.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #ifndef INCLUDE_COMMON_H_ #define INCLUDE_COMMON_H_ #include "typers.h" void outb (uint16_t port, uint8_t value) ;uint8_t inb (uint16_t port) ;uint16_t inw (uint16_t port) ; #endif
libs/common.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "common.h" inline void outb (uint16_t port, uint8_t value) { asm volatile ("outb %1, %0" :: "dN" (port), "a" (value)) ; } inline uint8_t inb (uint16_t port) { uint8_t ret; asm volatile ("inb %1, &0" : "=a" (ret) : "dN" (port)) ; return ret; } inline uint16_t inw (uint16_t port) { uint16_t ret; asm volatile ("inw %1, %0" : "=a" (ret) : "dN" (port)) ; return ret; }
4.2 颜色的枚举定义和屏幕操作函数实现
include/console.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #ifndef INCLUDE_CONSOLE_H_ #define INCLUDE_CONSOLE_H_ #include "typers.h" typedef enum real_color { rc_black = 0 , rc_blue = 1 , rc_green = 2 , rc_cyan = 3 , rc_red = 4 , rc_magenta = 5 , rc_brown = 6 , rc_light_grey = 7 , rc_dark_grey = 8 , rc_light_blue = 9 , rc_light_green = 10 , rc_light_cyan = 11 , rc_light_red = 12 , rc_light_magenta = 13 , rc_light_brown = 14 , rc_white = 15 }real_color_t ; void console_clear () ;void console_putc_color (char c, real_color_t back, real_color_t fore) ;void console_write (char *cstr) ;void console_write_color (char *cstr, real_color_t back, real_color_t fore) ;void console_write_hex (uint32_t n, real_color_t back, real_color_t fore) ;void console_write_dec (uint32_t n, real_color_t back, real_color_t fore) ;#endif
使用枚举的好处
提高代码可读性 :使用枚举可以使代码更易读和维护。
减少错误 :枚举限制了变量可以接受的值,减少了错误的可能性 。
便于比较 :枚举项可以很容易地进行比较和排序。
方便维护 :如果未来需要添加或修改值,枚举使得这些变更更加集中和统一。
driver/console.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include "console.h" #include "common.h" static uint16_t *video_memory = (uint16_t *)0xB8000 ;static uint8_t cursor_x = 0 ; static uint8_t cursor_y = 0 ; static void move_cursor () { uint16_t cursorLocation = cursor_y * 80 + cursor_x; outb (0x3D4 , 14 ); outb (0x3D5 , cursorLocation >> 8 ); outb (0x3D4 , 15 ); outb (0x3D5 , cursorLocation); }
变量定义
static uint16_t *video_memory = (uint16_t *)0xB8000;
这是一个指向视频内存开始地址的指针,VGA文本模式通常从物理地址 0xB8000
开始。这块内存用于存放屏幕上显示的字符及其属性(如颜色)。
uint16_t
表示每个内存位置可以存储16位数据,通常前8位是字符的ASCII码,后8位是字符的属性(前景色和背景色) 。
move_cursor
函数
这个函数用于移动屏幕光标到由 cursor_x
和 cursor_y
指定的位置。
uint16_t cursorLocation = cursor_y * 80 + cursor_x;
这行代码计算光标在video内存中的位置索引。因为每行有80个字符,所以每增加一行,索引增加80。
outb(0x3D4, 14);
调用 outb
函数向端口 0x3D4
写入值 14
。0x3D4
是VGA端口地址,用于指定接下来要设置的是光标位置的高字节。
outb(0x3D5, cursorLocation >> 8);
将光标位置的高8位(通过右移8位获得)写入VGA数据寄存器 0x3D5
。
outb(0x3D4, 15);
再次向端口 0x3D4
写入值 15
,指定接下来要设置的是光标位置的低字节。
outb(0x3D5, cursorLocation);
将光标位置的低8位直接写入VGA数据寄存器 0x3D5
。
清屏操作
1 2 3 4 5 6 7 8 9 10 11 12 13 void console_clear () { uint8_t attribute_byte = (0 << 4 ) | (15 & 0x0F ); uint16_t blank = 0x20 | (attribute_byte << 8 ); for (int i = 0 ; i < 80 * 25 ; i++) { video_memory[i] = blank; } cursor_x = 0 ; cursor_y = 0 ; move_cursor(); }
uint8_t attribute_byte = (0 << 4) | (15 & 0x0F);
这行代码定义了字符的显示属性。VGA文本模式中每个字符占用两个字节:一个字节表示字符的ASCII码,另一个字节定义字符的显示属性(包括前景色和背景色)。
(0 << 4)
设置背景色为黑色(颜色代码0)。0 左移 4 位是因为背景色占据属性字节的高4位。
(15 & 0x0F)
设置前景色为白色(颜色代码15)。& 0x0F
确保颜色代码不超过15,因为前景色占据低4位。
uint16_t blank = 0x20 | (attribute_byte << 8);
将空格字符(ASCII码 0x20)和属性字节组合成一个16位的值。属性字节通过左移8位移到高字节位置,然后与空格字符的ASCII码合并。这样,blank
变量代表一个带有指定颜色属性的空格字符。
屏幕滚动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void scroll () { uint8_t attribute_byte = (0 << 4 ) | (15 & 0x0F ); uint16_t blank = 0x20 | (attribute_byte << 8 ); if (cursor_y >= 25 ) { for (int i = 0 * 80 ; i < 24 * 80 ; i++) { video_memory[i] = video_memory[i + 80 ]; } for (int i = 24 * 80 ; i < 25 * 80 ; i++) { video_memory[i] = blank; } cursor_y = 24 ; } }
以上同理
显示字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void console_putc_color (char c, real_color_t back, real_color_t fore) { uint8_t back_color = (uint8_t )back; uint8_t fore_color = (uint8_t )fore; uint8_t attribute_byte = (back_color << 4 ) | (fore_color & 0x0F ); uint16_t attribute = attribute_byte << 8 ; if (c == 0x08 && cursor_x) { cursor_x--; } else if (c == 0x09 ) { cursor_x = (cursor_x + 8 ) & ~(8 - 1 ); } else if (c == '\r' ) { cursor_x = 0 ; } else if (c == '\n' ) { cursor_x = 0 ; cursor_y++; } else if (c >= ' ' ) { video_memory[cursor_y* 80 + cursor_x] = c | attribute; cursor_x++; } if (cursor_x >= 80 ) { cursor_x = 0 ; cursor_y++; } scroll(); move_cursor(); }
字符串输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void console_write (char *cstr) { while (*cstr) { console_putc_color(*cstr++, rc_black, rc_white); } } void console_write_color (char *cstr, real_color_t back, real_color_t fore) { while (*cstr) { console_putc_color(*cstr++, back, fore); } }
16进制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void console_write_hex (uint32_t n, real_color_t back, real_color_t fore) { int tmp; char noZeroes = 1 ; console_write_color("0x" , back, fore); for (int i = 28 ; i >= 0 ; i -= 4 ) { tmp = (n >> i) & 0xF ; if (tmp == 0 && noZeroes != 0 ) { continue ; } noZeroes = 0 ; if (tmp >= 0xA ) { console_putc_color(tmp-0xA +'a' , back, fore); } else { console_putc_color(tmp+'0' , back, fore); } } }
循环从28位开始,每次右移4位,直到0位。由于十六进制是基于4位二进制,这种方式可以每次处理一个十六进制数字。
for (int i = 28; i >= 0; i -= 4) {
1 2 3 - ``` tmp = (n >> i) & 0xF;
- 通过右移和与操作,每次提取4位,转换为一个十六进制的数字(0-15)。
- ```
if (tmp == 0 && noZeroes != 0) { continue; }
1 2 3 4 5 - 如果提取的数字为0,并且还未遇到任何非零数字,则跳过当前循环迭代,不输出该零。 - ``` noZeroes = 0;
- 一旦输出了第一个非零数字,将 `noZeroes` 设为0,以允许后续的零被输出。
10进制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void console_write_dec (uint32_t n, real_color_t back, real_color_t fore) { if (n == 0 ) { console_putc_color('0' , back, fore); return ; } uint32_t acc = n; char c[32 ]; int i = 0 ; while (acc > 0 ) { c[i] = '0' + acc % 10 ; acc /= 10 ; i++; } c[i] = 0 ; char c2[32 ]; c2[i--] = 0 ; int j = 0 ; while (i >= 0 ) { c2[i--] = c[j++]; } console_write_color(c2, back, fore); }
执行make clean
make
make qemu