1.基础-环境配置与开发工具

本机环境:Ubuntu22.04

需要的开发工具

编译器:gcc

链接器:ld

汇编编译器:nasm

虚拟机:qemu

参考:

  1. 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
#!Makefile

#-----------------------------
# 编译: 通过.c.o和.s.o规则,所有的.c和.s文件分别被编译成.o文件。
# 链接: 所有的.o文件通过link规则被链接成单个内核文件hx_kernel。
# 更新映像文件: 通过update_image规则,将内核文件复制到软盘映像中。
# 清理: 通过clean命令删除所有生成的文件,以便重新构建。
# 运行和调试: 提供了多种运行和调试内核的方式,如使用QEMU、Bochs和cgdb。
#-----------------------------


# patsubst 处理所有在 C_SOURCES 字列中的字(一列文件名),如果它的 结尾是 '.c',就用 '.o' 把 '.c' 取代
C_SOURCES = $(shell find . -name "*.c")
C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES)) #将所有.c文件转成 .o文件
S_SOURCES = $(shell find . -name "*.s") #查找所有.s文件
S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES)) #.s 文件转成 .o文件

#定义编译器命令 cc = gcc 相当于起别名 编译器用gcc 链接器用ld 汇编器用nasm
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

# The automatic variable `$<' is just the first prerequisite 表示规则中的第一个依赖项 使用模式规则来编译代码
.c.o:
@echo 编译代码文件 $< ...
$(CC) $(C_FLAGS) $< -o $@

.s.o:
@echo 编译汇编文件 $< ...
$(ASM) $(ASM_FLAGS) $<
#链接所有.o文件生成 内核文件
link:
@echo 链接内核文件...
$(LD) $(LD_FLAGS) $(S_OBJECTS) $(C_OBJECTS) -o hx_kernel

#.PHONY 伪目标,无论是否存在同名文件 命令都被执行
.PHONY:clean
clean:
$(RM) $(S_OBJECTS) $(C_OBJECTS) hx_kernel
#将生成的内核文件挂载在映像文件
.PHONY:update_image
update_image:
sudo mount floppy.img /mnt/kernel
sudo cp hx_kernel /mnt/kernel/hx_kernel
sleep 1
sudo umount /mnt/kernel
#挂载
.PHONY:mount_image
mount_image:
sudo mount floppy.img /mnt/kernel
# 卸载
.PHONY:umount_image
umount_image:
sudo umount /mnt/kernel

#调试方式
.PHONY:qemu
qemu:
qemu -fda floppy.img -boot a
#add '-nographic' option if using server of linux distro, such as fedora-server,or "gtk initialization failed" error will occur.

.PHONY:bochs
bochs:
bochs -f scripts/bochsrc.txt

.PHONY:debug
debug:
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
/*
* kernel.ld −− 针对 kernel 格式所写的链接脚本
*/

ENTRY(start)
SECTIONS
{
/* 段起始位置 */
. = 0x100000; // 后面的段 从0x100000开始 即是1MB
.text :
{
*(.text)
. = ALIGN(4096); // 页对齐到 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 的头部 必须包含以下三个:

  1. magic number : 必须包含一个固定的数 – 0xABADB002 用于引导程序识别兼容multi内核
  2. flags :标志引导程序需要提供哪些额外的信息 是否需要内核对齐 是否需要内存信息等
  3. 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 // include types h

首先make

然后执行 make qemu

make 的时候 可能会报错 sudo mount floppy.img /mnt/kernel mount: /mnt/kernel: mount point does not exist.

这个时候直接去 /mnt 下创建kernel文件夹就行 提示权限不够就用sudo mkdir kernel

此时出现的是空白

image-20240430183522064

执行 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;
}

image-20240430184001925

4.字符模式下的显卡驱动

所有在PC上工作的显卡 在加点初始化之后都会自动初始化到 80*25 文本模式,表示屏幕被划分成了25行,每行可以显示80个字符

所以一屏可以显示2000个字符 0xB8000 - 0xBFFFF这个地址段 便是映射到文本模式的显存的

内码:定义了字符在内存中存储的形式

对应关系从0xB8000 开始 每两个字节表示屏幕上显示一个字符。这两个字节前一个字节是显示字符串的ASCII 码,后一个是控制这个字符颜色和属性的控制信息

image-20240430204238998

image-20240430204540029

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 用于提示编译器将函数的定义直接插入到函数调用的位置,而不是通过函数调用的 方式执行 好处:

  1. 减少函数调用的开销 — 程序本身较小的时候 省略调用过程减少开销
  2. 优化程序性能 — 函数体直接插入调用位置,编译器更容易优化
  3. 避免链接问题
  4. 提升模块化 — 小的 频繁调用的函数定义为inline
  5. 支持递归

宏定义 没有参数类型检查喔 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 // include

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, //yellow
rc_white = 15

}real_color_t;

// 清屏
void console_clear();

// 屏幕输出一个字符带颜色
void console_putc_color(char c, real_color_t back, real_color_t fore);

// 屏幕打印一个以 \0 结尾的字符串 默认黑底白字
void console_write(char *cstr);

// 屏幕打印一个以\0结尾的字符串 带颜色
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 // include

使用枚举的好处

  1. 提高代码可读性:使用枚举可以使代码更易读和维护。
  2. 减少错误:枚举限制了变量可以接受的值,减少了错误的可能性
  3. 便于比较:枚举项可以很容易地进行比较和排序。
  4. 方便维护:如果未来需要添加或修改值,枚举使得这些变更更加集中和统一。

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"
// VGA 的显示缓冲的起点 0xB8000
static uint16_t *video_memory = (uint16_t *)0xB8000;

// 屏幕光标的坐标 static 限定 作用域
static uint8_t cursor_x = 0; // 光标的水平坐标
static uint8_t cursor_y = 0; // 光标的垂直坐标

// 屏幕输入光标的移动
static void move_cursor() {
// 屏幕80 字节宽
uint16_t cursorLocation = cursor_y * 80 + cursor_x;

// VGA 内部的寄存器多达300多个,显然无法一一映射到I/O端口的地址空间。
// 对此 VGA 控制器的解决方案是,将一个端口作为内部寄存器的索引:0x3D4,
// 再通过 0x3D5 端口来设置相应寄存器的值。
// 在这里用到的两个内部寄存器的编号为14与15,分别表示光标位置的高8位与低8位。

outb(0x3D4, 14); // VGA 我们要设置光标的高字节
outb(0x3D5, cursorLocation >> 8); // 发送高8位
outb(0x3D4, 15); // 低字节
outb(0x3D5, cursorLocation); // 发送低8位
}

变量定义

  • static uint16_t *video_memory = (uint16_t *)0xB8000;
    • 这是一个指向视频内存开始地址的指针,VGA文本模式通常从物理地址 0xB8000 开始。这块内存用于存放屏幕上显示的字符及其属性(如颜色)。
    • uint16_t 表示每个内存位置可以存储16位数据,通常前8位是字符的ASCII码,后8位是字符的属性(前景色和背景色)

move_cursor 函数

  • 这个函数用于移动屏幕光标到由 cursor_xcursor_y 指定的位置。

  • uint16_t cursorLocation = cursor_y * 80 + cursor_x;

    • 这行代码计算光标在video内存中的位置索引。因为每行有80个字符,所以每增加一行,索引增加80。
  • outb(0x3D4, 14);

    • 调用 outb 函数向端口 0x3D4 写入值 140x3D4 是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); // 0x0F = 0x00001111
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
// 屏幕滚动 就是将后24行的数据全部向上挪动一行 最后一行清空
static void scroll() {
// attrbute_byte 被构造出一个黑底白字的描述格式
uint8_t attribute_byte = (0 << 4) | (15 & 0x0F);
uint16_t blank = 0x20 | (attribute_byte << 8); // space 是 0x20

// cursor_y 到25的时候就该换行;
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
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;
//背景颜色左移4位,与前景色进行位或操作,合成一个字节的颜色属性。
uint8_t attribute_byte = (back_color << 4) | (fore_color & 0x0F);
uint16_t attribute = attribute_byte << 8; // 将属性字节左移8位,以便与字符的ASCII值合并形成16位的值,用于直接写入video内存

// 0x08 是退格的ASCII
// 0x09 是tab
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++;
}
// 超过80 换行
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
// yijuhua
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
// 16 nums
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); // 先打印出0x

// 16进制转换
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); // 如果输出的数字大于等于10 A-F 则转换相应的小写字母进行输出
} 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
// 10 nums
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);
}
  • while (acc > 0) { c[i] = '0' + acc % 10; acc /= 10; i++; }
    
    • 循环直到 acc 为0。每次循环取出 acc 的最低位(acc % 10),加上字符 ‘0’ 转换成对应的字符,存入 c[i],然后 acc 除以10准备下一次迭代。

执行make clean

make

make qemu

image-20240507165856179