文章目录:
  1. OS Lab1 内核、启动和PRINTF
    1. OS,启动!
    2. 内核代码结构
      1. (1) Makefile
      2. (2) ELF
        1. 「1」编译处理过程
        2. 「2」ELF是什么
        3. 「3」ELF结构
        4. 「4」 ELF内容解析
      3. (3)MIPS内存布局
      4. (4)Linker Script
    3. _start函数
    4. printk函数
      1. 代码结构
      2. 变长参数处理—stdarg.h
  2. 附录
    1. tools/readelf/elf.h
    2. mmu.h部分
    3. MIPS指令
    4. printk代码注释

OS Lab1 内核、启动和PRINTF

OS,启动!

QEMU是一个计算机模拟器,可以模拟CPU等硬件环境,用来运行实验中生成的操作系统可执行文件。

启动流程:加载ELF格式内核到内存,跳转到内核的入口。

内核代码结构

.
├── include              # 系统头文件
│   └── mmu.h            # 里面有内存布局图  
├── init                 # 内核初始化目录
│   └── start.S          # _start 初始化CPU和栈指针 
│   └── init.c           # 各模块的初始化函数  
├── kern                 # 内核主体代码目录,存放内核核心功能模块实现
├── lib                  # 通用库函数目录,提供字符串处理、内存操作、格式化输出等基础函数
├── mk                   
├── out                  # 编译输出目录
├── target               # 目标平台相关目录,存放特定平台/目标系统的文件或产物
│   └── mos              # 当前目标系统相关内容
├── tests                # 测试用例目录,存放本地测试和功能验证代码
├── tools                # 工具目录,不属于MOS,在linux上运行
├── .gitignore           # Git 忽略规则文件,指定无需纳入版本控制的文件
├── .mos-this-lab        # 实验标识文件,用于辅助识别当前实验内容
├── include.mk           # 供 Makefile 引用
├── kernel.lds           # 内核链接脚本,用于指定内核各段的链接布局和加载地址
└── Makefile             # Makefile
(1) Makefile
include include.mk                                 # 部分变量定义在此

# 文件输出定义
target_dir   := target                           # MOS 构建目标所在目录
mos_elf      := $(target_dir)/mos                # 最终需要生成的 ELF 可执行文件
user_disk    := $(target_dir)/fs.img             # MOS 文件系统使用的磁盘镜像文件
link_script  := kernel.lds

# 子模块定义
modules      := lib init kern                    # 需要生成的子模块
objects      := $(addsuffix /*.o, $(modules))    # 遍历需要生成的目标文件

QEMU_FLAGS   := -cpu 4Kc -m 64 -nographic -M malta \
                $(shell [ -f '$(user_disk)' ] && \
                echo '-drive id=ide,file=$(user_disk),if=ide,format=raw') \
                -no-reboot                       # QEMU 运行参数

.PHONY: all $(modules) clean

all: $(mos_elf)                                  # 我们的“最终目标”

$(mos_elf): $(modules)                           # 调用链接器 $(LD) 链接所有目标文件
    $(LD) $(LDFLAGS) -o $(mos_elf) -N -T $(link_script) $(objects)

$(modules):                                      # 进入各个子目录进行 make
    $(MAKE) --directory=$@

clean:
    for d in $(modules); do \
        $(MAKE) --directory=$$d clean; \
    done; \
    rm -rf *.o *~ $(mos_elf)

run:
    $(QEMU) $(QEMU_FLAGS) -kernel $(mos_elf)

先要make子模块(lib init kern…),然后使用链接器链接子模块,大概展开后是这样:

mips-linux-gnu-ld $(LDFLAGS) -o target/mos -N -T kernel.lds lib/*.o init/*.o kern/*.o

可以看到把上一步子模块里面生成的.o文件链接到了一起,使用的链接脚本为上面定义的kernel.lds文件(后面也单独讲了这个文件),最后生成了target/mos这个最终的可执行文件。使用all目标作为make的最终执行结果。

清理:make clean,运行:make run,均为.PHONY目标(即不依赖文件生成时间,每次都会执行)。

这里使用mips-linux-gnu-前缀为交叉编译器,意思是在本地环境(可能x86下)编译,要拿到MIPS模拟器里面运行,即目标代码运行的平台和编译器运行的平台不同。

(2) ELF
「1」编译处理过程

对于含有多个.c文件的工程来说,编译器会首先将所有的.c文件以文件为单位,编译成.o 文件。最后再将所有的.o 文件以及函数库链接在一起,形成最终的可执行文件。

预处理: 仍是C语言文本,把#include头文件复制粘贴,#define定义的宏替换为代码,以及条件编译指令#if等,去掉注释,生成.i文件。

$ gcc -E hello.c -o hello.i

编译: C语言翻译为汇编代码,生成.s文件。

$ gcc -S hello.i -o hello.s

汇编: 将汇编代码转换为机器代码,生成.o目标文件

$ gcc -c hello.s -o hello.o

链接: 和系统库等文件编译到一起,得到可执行文件

「2」ELF是什么

一般有三种类型:

「3」ELF结构

分为这几个部分:

image-20260326154738943

分为几个部分,看图很好理解:

  1. ELF头,包括程序的基本信息,比如体系结构和操作系统,同时也包含了节头表和段头表相对文件的偏移量(offset)。
  2. 段头表(或程序头表),主要包含程序中各个段(segment)的信息, 段的信息需要在运行时刻使用。
  3. 节头表,主要包含程序中各个节(section)的信息,节的信息需要在程序编译和链接的时候使用。
  4. 段头表中的每一个表项,记录了该段数据载入内存时的目标位置等,记录了用于指导应用程序加载的各类信息。
  5. 节头表中的每一个表项,记录了该节程序的代码段、数据段等各个段的内容,主要是链接器在链接的过程中需要使用。

即通过ELF头,我们可以得知段头表、节头表位置,通过这两个表可以查出其中的表项在ELF文件里面的位置,如.text等。

其中节头表主要用于链接阶段,段头表主要用于运行

Exercise 1.1 (只保留补全部分)

/* Exercise 1.1: Your code here. (1/2) */
sh_table = (const char *)binary + ehdr->e_shoff;
sh_entry_count = ehdr->e_shnum;
sh_entry_size = ehdr->e_shentsize;

/* Exercise 1.1: Your code here. (2/2) */
shdr = (const Elf32_Shdr *)((const char *)sh_table + i * sh_entry_size);
addr = shdr->sh_addr;

解析:看elf.h里面各个结构体的定义即可(中文注释见附录)。同时所有的偏移地址按照字节寻址,最终的节头表需要保存为对应结构体指针格式。

「4」 ELF内容解析

使用readelf -l查看一个可执行文件的ELF内容:

image-20260326165434262

各列含义如下:

这块内容为了说明什么?>> 模拟器加载内核的时候,也需要将内核代码、数据加载到对应的位置上,才能保证内核正常运行 >> 所以我们需要知道内核的正确加载位置是哪里。

(3)MIPS内存布局

程序中一般使用的是虚拟地址,通过MMU转为物理地址发往总线访存。

本次实验使用的MIPS虚拟地址空间为4GB,划分为四个大区域:

image-20260326172212211

区域地址范围大小访问权限是否经 TLB是否缓存
kuseg0x00000000 - 0x7FFFFFFF2GB用户态 / 内核态都可用取决于页属性,通常可缓存
kseg00x80000000 - 0x9FFFFFFF512MB仅内核态否,直接映射
kseg10xA0000000 - 0xBFFFFFFF512MB仅内核态否,直接映射
kseg2/30xC0000000 - 0xFFFFFFFF1GB仅内核态取决于页属性,通常可缓存

因为TLB依赖于操作系统,所以OS装在只能使用Kseg0/1,其中Kseg0因为缓存了,速度快一些,所以一般使用kseg0。

实验中需要使用部分地址(见附录mmu.h),.h头文件已经定义好对应的变量了:

#define PAGE_SIZE   4096            // 4KB
#define PTMAP       PAGE_SIZE       // 4KB
#define PDMAP       (4 * 1024 * 1024) // 4MB

#define KERNBASE    0x80020000      // 内核代码起始地址
#define ULIM        0x80000000      // 用户空间上限 / 内核空间起点

#define KSTACKTOP   (ULIM + PDMAP)  // 0x80400000,内核栈顶
#define UVPT        (ULIM - PDMAP)  // 0x7FC00000,用户页表映射区
#define UPAGES      (UVPT - PDMAP)  // 0x7F800000,物理页信息区
#define UENVS       (UPAGES - PDMAP)// 0x7F400000,进程/环境信息区

#define UTOP        UENVS           // 0x7F400000,用户空间顶部
#define UXSTACKTOP  UTOP            // 0x7F400000,用户异常栈顶
#define USTACKTOP   (UTOP - 2 * PTMAP) // 0x7F3FE000,普通用户栈顶

#define UTEXT       PDMAP           // 0x00400000,用户程序入口
#define UCOW        (UTEXT - PTMAP) // 0x003FF000,写时复制保留页
#define UTEMP       (UCOW - PTMAP)  // 0x003FE000,临时映射页
(4)Linker Script

在不同的设备上使用链接器ld时,由于不同设备加载地址不同,所以需要Linker Script记录各个节应该如何映射到段,以及各个段应该被加载到的位置。

其基本格式:

SECTIONS
{
    . = 0x10000;
    .text : { *(.text) }
    . = 0x8000000;
    .data : { *(.data) }
    .bss : { *(.bss) }
}

其中,.代表当前位置,即接下来的内容从什么位置开始放起。

而第二行:定义一个输出段.text,把所有输入文件里的 .text 段都放进来。

后面的一样。如果没写.,则满足对齐要求后,接着往后排。

应该只用会这两个语法。

Exercise 1.2

/* Step 1: Set the loading address of the text section to the location counter ".". */
/* Exercise 1.2: Your code here. (1/4) */
. = 0x80020000;
/* Step 2: Define the text section. */
/* Exercise 1.2: Your code here. (2/4) */
.text : { *(.text) }
/* Step 3: Define the data section. */
/* Exercise 1.2: Your code here. (3/4) */
.data : { *(.data) }
bss_start = .;
/* Step 4: Define the bss section. */
/* Exercise 1.2: Your code here. (4/4) */
.bss : { *(.bss) }
bss_end = .;

第一个去找内核启动地址(mmu.h),应该可以替换为KERNBASE。后面的那不是和前面例子一模一样吗()

另外,

  • bss_start.bss 段起始地址
  • bss_end.bss 段结束地址

gpt说是用于方便给.bss段清零。

_start函数

Exercise 1.3
#include <asm/asm.h>
#include <mmu.h>

.text
EXPORT(_start)
.set at
.set reorder
/* Lab 1 Key Code "enter-kernel" */
    /* clear .bss segment */
    la      v0, bss_start
    la      v1, bss_end
clear_bss_loop:
    beq     v0, v1, clear_bss_done
    sb      zero, 0(v0)
    addiu   v0, v0, 1
    j       clear_bss_loop
/* End of Key Code "enter-kernel" */

clear_bss_done:
    /* disable interrupts */
    mtc0    zero, CP0_STATUS

    /* hint: you can refer to the memory layout in include/mmu.h */
    /* set up the kernel stack */
    /* Exercise 1.3: Your code here. (1/2) */
    li        sp, 0x80400000
    /* jump to mips_init */
    /* Exercise 1.3: Your code here. (2/2) */
    jal     mips_init

这个函数内主要做了这么几件事:首先是_start函数的声明,随后2、3行的.set允许汇编器使用at寄存器,也允许对接下来的代码进行重排序。接下来直到clear_bss_done标签的代码对.bss段进行了清零操作。 而最后一行代码则禁用了外部中断。

要首先将sp寄存器设置到内核栈空间的位置上,随后跳转到mips_init函数。

栈指针的位置在mmu.h中可以看到,直接赋值即可。然后jal跳转到mips_init地址即可。

printk函数

代码结构
变长参数处理—stdarg.h
  1. va_list,变长参数表的变量类型;
  2. va_start(va_list ap, lastarg),用于初始化变长参数表的宏,其中lastarg为该函数最后一个命名的形式参数,比如:

    void f(int a, int b, ...)
    {
        va_list ap;
        va_start(ap, b);   // b 是 ... 前最后一个命名参数,ap取到的第一个是b
    }
  3. va_arg(va_list ap, 类型),用于取变长参数表下一个参数的宏,如int num = va_arg(ap, int),然后ap会指向下一个参数;
  4. va_end(va_list ap),结束使用变长参数表的宏。
Exercise 1.4 见附录

附录

tools/readelf/elf.h

/* 本文件定义了标准 ELF 类型、结构体和宏。 */

#ifndef _ELF_H
#define _ELF_H

#include <stdint.h>

/* 来自 GNU C Library 的 ELF 定义文件。我们为了实验对该文件做了简化,
 * 移除了 ELF64、以及我们不关心的结构体和枚举定义。
 */

/* 16 位数据类型。 */
typedef uint16_t Elf32_Half;

/* 有符号和无符号 32 位数据类型。 */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;

/* 有符号和无符号 64 位数据类型。 */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;

/* 地址类型。 */
typedef uint32_t Elf32_Addr;

/* 文件偏移类型。 */
typedef uint32_t Elf32_Off;

/* 节索引类型,为 16 位数据。 */
typedef uint16_t Elf32_Section;

/* 符号索引类型。 */
typedef uint32_t Elf32_Symndx;

/* 实验 1 关键代码 "readelf-struct-def" */

/* ELF 文件头。它出现在每个 ELF 文件的开头。 */

#define EI_NIDENT (16)

typedef struct {
    unsigned char e_ident[EI_NIDENT]; /* 魔数及其他信息 */
    Elf32_Half e_type;          /* 目标文件类型 */
    Elf32_Half e_machine;          /* 体系结构 */
    Elf32_Word e_version;          /* 目标文件版本 */
    Elf32_Addr e_entry;          /* 入口点虚拟地址 */
    Elf32_Off e_phoff;          /* 程序头表在文件中的偏移 */
    Elf32_Off e_shoff;          /* 节头表在文件中的偏移 */
    Elf32_Word e_flags;          /* 处理器特定标志 */
    Elf32_Half e_ehsize;          /* ELF 头大小(字节) */
    Elf32_Half e_phentsize;          /* 程序头表项大小 */
    Elf32_Half e_phnum;          /* 程序头表项数量 */
    Elf32_Half e_shentsize;          /* 节头表项大小 */
    Elf32_Half e_shnum;          /* 节头表项数量 */
    Elf32_Half e_shstrndx;          /* 节头字符串表索引 */
} Elf32_Ehdr;

/* e_ident 数组中的各字段。EI_* 宏是该数组中的索引。
   每个 EI_* 宏下面定义的宏,是该字节可能取的值。 */

#define EI_MAG0 0    /* 文件标识第 0 字节索引 */
#define ELFMAG0 0x7f /* 魔数字节 0 */

#define EI_MAG1 1   /* 文件标识第 1 字节索引 */
#define ELFMAG1 'E' /* 魔数字节 1 */

#define EI_MAG2 2   /* 文件标识第 2 字节索引 */
#define ELFMAG2 'L' /* 魔数字节 2 */

#define EI_MAG3 3   /* 文件标识第 3 字节索引 */
#define ELFMAG3 'F' /* 魔数字节 3 */

/* 节头。 */
typedef struct {
    Elf32_Word sh_name;     /* 节名称 */
    Elf32_Word sh_type;     /* 节类型 */
    Elf32_Word sh_flags;     /* 节标志 */
    Elf32_Addr sh_addr;     /* 节地址 */
    Elf32_Off sh_offset;     /* 节偏移 */
    Elf32_Word sh_size;     /* 节大小 */
    Elf32_Word sh_link;     /* 节链接 */
    Elf32_Word sh_info;     /* 节额外信息 */
    Elf32_Word sh_addralign; /* 节对齐 */
    Elf32_Word sh_entsize;     /* 节表项大小 */
} Elf32_Shdr;

/* 程序段头。 */

typedef struct {
    Elf32_Word p_type;   /* 段类型 */
    Elf32_Off p_offset;  /* 段在文件中的偏移 */
    Elf32_Addr p_vaddr;  /* 段虚拟地址 */
    Elf32_Addr p_paddr;  /* 段物理地址 */
    Elf32_Word p_filesz; /* 段在文件中的大小 */
    Elf32_Word p_memsz;  /* 段在内存中的大小 */
    Elf32_Word p_flags;  /* 段标志 */
    Elf32_Word p_align;  /* 段对齐 */
} Elf32_Phdr;
/* 关键代码 "readelf-struct-def" 结束 */

/* p_type(段类型)的合法取值。 */

#define PT_NULL 0         /* 未使用的程序头表项 */
#define PT_LOAD 1         /* 可装载程序段 */
#define PT_DYNAMIC 2         /* 动态链接信息 */
#define PT_INTERP 3         /* 程序解释器 */
#define PT_NOTE 4         /* 辅助信息 */
#define PT_SHLIB 5         /* 保留 */
#define PT_PHDR 6         /* 头表自身的表项 */
#define PT_NUM 7         /* 已定义类型的数量。 */
#define PT_LOOS 0x60000000   /* 操作系统特定范围起始 */
#define PT_HIOS 0x6fffffff   /* 操作系统特定范围结束 */
#define PT_LOPROC 0x70000000 /* 处理器特定范围起始 */
#define PT_HIPROC 0x7fffffff /* 处理器特定范围结束 */

/* p_flags(段标志)的合法取值。 */

#define PF_X (1 << 0)           /* 段可执行 */
#define PF_W (1 << 1)           /* 段可写 */
#define PF_R (1 << 2)           /* 段可读 */
#define PF_MASKPROC 0xf0000000 /* 处理器特定 */

#endif /* _ELF_H */

mmu.h部分

 o     4G ----------->  +----------------------------+------------0x100000000
 o                      |       ...                  |  kseg2
 o      KSEG2    -----> +----------------------------+------------0xc000 0000
 o                      |          Devices           |  kseg1
 o      KSEG1    -----> +----------------------------+------------0xa000 0000
 o                      |      Invalid Memory        |   /|\
 o                      +----------------------------+----|-------Physical Memory Max
 o                      |       ...                  |  kseg0
 o      KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------end
 o                      |       Kernel Stack         |    | KSTKSIZE            /|\
 o                      +----------------------------+----|------                |
 o                      |       Kernel Text          |    |                    PDMAP
 o      KERNBASE -----> +----------------------------+----|-------0x8002 0000    |
 o                      |      Exception Entry       |   \|/                    \|/
 o      ULIM     -----> +----------------------------+------------0x8000 0000-------
 o                      |         User VPT           |     PDMAP                /|\
 o      UVPT     -----> +----------------------------+------------0x7fc0 0000    |
 o                      |           pages            |     PDMAP                 |
 o      UPAGES   -----> +----------------------------+------------0x7f80 0000    |
 o                      |           envs             |     PDMAP                 |
 o  UTOP,UENVS   -----> +----------------------------+------------0x7f40 0000    |
 o  UXSTACKTOP -/       |     user exception stack   |     PTMAP                 |
 o                      +----------------------------+------------0x7f3f f000    |
 o                      |                            |     PTMAP                 |
 o      USTACKTOP ----> +----------------------------+------------0x7f3f e000    |
 o                      |     normal user stack      |     PTMAP                 |
 o                      +----------------------------+------------0x7f3f d000    |
 a                      |                            |                           |
 a                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                           |
 a                      .                            .                           |
 a                      .                            .                         kuseg
 a                      .                            .                           |
 a                      |~~~~~~~~~~~~~~~~~~~~~~~~~~~~|                           |
 a                      |                            |                           |
 o       UTEXT   -----> +----------------------------+------------0x0040 0000    |
 o                      |      reserved for COW      |     PTMAP                 |
 o       UCOW    -----> +----------------------------+------------0x003f f000    |
 o                      |   reversed for temporary   |     PTMAP                 |
 o       UTEMP   -----> +----------------------------+------------0x003f e000    |
 o                      |       invalid memory       |                          \|/
 a     0 ------------>  +----------------------------+ ----------------------------

MIPS指令

类别指令格式作用备注
算术addadd rd, rs, rtrd = rs + rt有符号加法,检查溢出
算术adduaddu rd, rs, rtrd = rs + rt不检查溢出
算术addiaddi rt, rs, immrt = rs + imm立即数加法
算术addiuaddiu rt, rs, immrt = rs + imm不检查溢出
算术subsub rd, rs, rtrd = rs - rt有符号减法
算术subusubu rd, rs, rtrd = rs - rt不检查溢出
算术mulmul rd, rs, rt乘法结果低位放 rd有些环境支持
算术multmult rs, rt乘积放入 HI/LO高位在 HI,低位在 LO
算术multumultu rs, rt无符号乘法结果放 HI/LO
算术divdiv rs, rt商到 LO,余数到 HI有符号除法
算术divudivu rs, rt商到 LO,余数到 HI无符号除法
特殊寄存器mflomflo rdrd = LO取乘除法结果的商/低位
特殊寄存器mfhimfhi rdrd = HI取乘除法余数/高位
特殊寄存器mtlomtlo rsLO = rsLO
特殊寄存器mthimthi rsHI = rsHI
逻辑andand rd, rs, rt按位与
逻辑andiandi rt, rs, imm按位与立即数常用于掩码
逻辑oror rd, rs, rt按位或
逻辑oriori rt, rs, imm按位或立即数常配合 lui
逻辑xorxor rd, rs, rt按位异或
逻辑xorixori rt, rs, imm按位异或立即数
逻辑nornor rd, rs, rt按位或后取反常用于实现 not
移位sllsll rd, rt, shamt逻辑左移低位补 0
移位srlsrl rd, rt, shamt逻辑右移高位补 0
移位srasra rd, rt, shamt算术右移高位补符号位
移位sllvsllv rd, rt, rs可变左移位数在 rs
移位srlvsrlv rd, rt, rs可变逻辑右移
移位sravsrav rd, rt, rs可变算术右移
比较sltslt rd, rs, rtrs < rt,则 rd=1有符号比较
比较sltusltu rd, rs, rtrs < rt,则 rd=1无符号比较
比较sltislti rt, rs, immrs < immrt=1有符号比较
比较sltiusltiu rt, rs, immrs < immrt=1无符号比较
访存lwlw rt, offset(base)读一个字到寄存器4 字节
访存swsw rt, offset(base)把一个字写入内存4 字节
访存lblb rt, offset(base)读一个字节符号扩展
访存lbulbu rt, offset(base)读一个字节零扩展
访存sbsb rt, offset(base)写一个字节
访存lhlh rt, offset(base)读半字符号扩展,2 字节
访存lhulhu rt, offset(base)读半字零扩展
访存shsh rt, offset(base)写半字2 字节
访存llll rt, offset(base)原子读配合 sc
访存scsc rt, offset(base)原子写配合 ll
跳转/分支beqbeq rs, rt, label若相等则跳转常用于 if
跳转/分支bnebne rs, rt, label若不等则跳转
跳转/分支bgezbgez rs, labelrs >= 0 则跳转和 0 比较
跳转/分支bgtzbgtz rs, labelrs > 0 则跳转
跳转/分支blezblez rs, labelrs <= 0 则跳转
跳转/分支bltzbltz rs, labelrs < 0 则跳转
跳转/分支jj label无条件跳转
跳转/分支jaljal label跳转并保存返回地址返回地址进 $ra
跳转/分支jrjr rs跳到寄存器中的地址常写 jr $ra
跳转/分支jalrjalr rd, rs间接调用常用于函数指针
数据装入luilui rt, imm立即数放高 16 位低 16 位补 0
伪指令lili rt, imm装立即数伪指令
伪指令lala rt, label装地址伪指令
伪指令movemove rd, rsrd = rs常等价于 addu rd, rs, $zero
伪指令negneg rd, rsrd = -rs伪指令
伪指令notnot rd, rs按位取反常等价于 nor rd, rs, $zero
伪指令nopnop空操作常等价于 sll $zero, $zero, 0
系统调用syscallsyscall执行系统调用服务号放 $v0

image-20260326195253005

printk代码注释

kern/prink.c

#include <print.h>   // 声明打印相关函数,如 vprintfmt、printcharc 等
#include <printk.h>  // 声明 printk 函数原型
#include <trap.h>    // 声明 Trapframe 结构体,用于保存陷入/异常现场

/**
 * @brief printk 的底层输出函数(回调函数)
 *
 * 这个函数通常不会由用户直接调用,而是作为回调函数传给 vprintfmt。
 * vprintfmt 会把格式化后的结果分成一段一段的字符缓冲区,然后调用这个函数输出。
 *
 * @param data 额外的用户数据指针。
 *             在当前实现里没有使用,所以传什么都无所谓,通常传 NULL。
 * @param buf  指向待输出字符缓冲区的指针。
 * @param len  buf 中需要输出的字符个数。
 *
 * @note
 * 该函数的作用就是把 buf[0..len-1] 中的字符逐个调用 printcharc 输出。
 *
 * @example
 * 一般不是直接调用,而是这样配合 vprintfmt 使用:
 *     vprintfmt(outputk, NULL, "value=%d", ap);
 */
void outputk(void *data, const char *buf, size_t len) {
    for (int i = 0; i < len; i++) {
        printcharc(buf[i]);   // 逐个字符输出到控制台/串口
    }
}

/**
 * @brief 内核态格式化打印函数,功能类似 printf
 *
 * 这个函数用于在内核中打印格式化字符串。
 * 它接收一个格式字符串 fmt,以及后面可变数量的参数,
 * 最终通过 vprintfmt 进行格式化,再调用 outputk 输出。
 *
 * @param fmt 格式控制字符串,写法和 printf 类似。
 *            例如:"x = %d, str = %s\n"
 * @param ... 与 fmt 中格式说明符对应的可变参数。
 *
 * @note
 * 支持的格式化能力取决于 vprintfmt 的实现。
 *
 * @example
 *     printk("hello world\n");
 *     printk("num = %d\n", 10);
 *     printk("addr = %08x\n", 0x1234);
 */
void printk(const char *fmt, ...) {
    va_list ap;              // 用于保存可变参数列表
    va_start(ap, fmt);       // 初始化 ap,让它指向 fmt 后面的第一个可变参数
    vprintfmt(outputk, NULL, fmt, ap);  // 格式化输出,底层调用 outputk
    va_end(ap);              // 清理可变参数列表
}

/**
 * @brief 打印 Trapframe(陷阱帧/异常现场)中的寄存器信息
 *
 * 当发生异常、中断或系统调用时,CPU 当前现场通常会被保存到 Trapframe 中。
 * 这个函数用于把 Trapframe 里的寄存器值全部打印出来,方便调试。
 *
 * 打印内容包括:
 * 1. 通用寄存器 regs[]
 * 2. 特殊寄存器 HI 和 LO
 * 3. CP0 相关寄存器:
 *    - Status
 *    - BadVAddr
 *    - Cause
 *    - EPC
 *
 * @param tf 指向 Trapframe 结构体的指针。
 *           必须是一个有效指针,不能为空。
 *
 * @example
 *     struct Trapframe *tf = ...;
 *     print_tf(tf);
 *
 * @warning
 * 如果 tf 为 NULL,那么 tf->regs 等访问会导致错误。
 */
void print_tf(struct Trapframe *tf) {
    // 打印通用寄存器数组 regs[] 中的每一个寄存器
    for (int i = 0; i < sizeof(tf->regs) / sizeof(tf->regs[0]); i++) {
        printk("$%2d = %08x\n", i, tf->regs[i]);
    }

    // 打印乘除法相关特殊寄存器
    printk("HI  = %08x\n", tf->hi);
    printk("LO  = %08x\n\n", tf->lo);

    // 打印协处理器 CP0 的关键寄存器
    printk("CP0.SR    = %08x\n", tf->cp0_status);   // 状态寄存器
    printk("CP0.BadV  = %08x\n", tf->cp0_badvaddr); // 出错的虚拟地址
    printk("CP0.Cause = %08x\n", tf->cp0_cause);    // 异常原因寄存器
    printk("CP0.EPC   = %08x\n", tf->cp0_epc);      // 异常返回地址
}

lib/print.c

#include <print.h>   // 声明格式化输出相关类型和函数,例如 fmt_callback_t、va_list 等

/* 前向声明(告诉编译器:后面会定义这些函数) */

/**
 * @brief 输出单个字符,并按宽度做左右对齐
 *
 * @param out     输出回调函数
 * @param data    传给回调函数的额外参数
 * @param c       要输出的字符
 * @param length  输出宽度(最少占多少列)
 * @param ladjust 是否左对齐,1 表示左对齐,0 表示右对齐
 */
static void print_char(fmt_callback_t, void *, char, int, int);

/**
 * @brief 输出字符串,并按宽度做左右对齐
 *
 * @param out     输出回调函数
 * @param data    传给回调函数的额外参数
 * @param s       要输出的字符串
 * @param length  输出宽度
 * @param ladjust 是否左对齐
 */
static void print_str(fmt_callback_t, void *, const char *, int, int);

/**
 * @brief 输出数字,支持不同进制、负号、补齐字符、大小写十六进制等
 *
 * @param out      输出回调函数
 * @param data     传给回调函数的额外参数
 * @param u        要输出的数字(按无符号处理)
 * @param base     进制:2/8/10/16
 * @param neg_flag 是否输出负号,1 表示前面加 '-'
 * @param length   输出宽度
 * @param ladjust  是否左对齐
 * @param padc     不足宽度时用什么字符补齐,例如 ' ' 或 '0'
 * @param upcase   十六进制字母是否大写,1 表示 A~F,0 表示 a~f
 */
static void print_num(fmt_callback_t, void *, unsigned long, int, int, int, int, char, int);

/**
 * @brief 格式化输出核心函数,类似标准库中的 vprintf
 *
 * 这个函数并不直接决定“输出到哪里”,而是把格式化后的内容交给回调函数 out。
 * 所以它既可以用于打印到控制台,也可以用于输出到缓冲区、串口等。
 *
 * 支持的格式包括:
 *   %b   二进制
 *   %d   十进制有符号数
 *   %o   八进制
 *   %u   十进制无符号数风格输出
 *   %x   十六进制小写
 *   %X   十六进制大写
 *   %c   单个字符
 *   %s   字符串
 *
 * 支持的格式控制:
 *   -    左对齐
 *   0    用 0 补齐
 *   数字 指定宽度,例如 %08x
 *   l    long 类型,例如 %ld
 *
 * @param out  输出回调函数。
 *             类型通常类似:void out(void *data, const char *buf, size_t len)
 *             vprintfmt 会把格式化后的字符片段交给它输出。
 * @param data 传给 out 的额外参数。
 *             如果用不到,通常传 NULL。
 * @param fmt  格式字符串,类似 printf 的格式串。
 * @param ap   可变参数列表,通常由 va_start 初始化。
 *
 * @example
 *     va_list ap;
 *     va_start(ap, fmt);
 *     vprintfmt(outputk, NULL, fmt, ap);
 *     va_end(ap);
 */
void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap) {
    char c;         // 用来保存 %c 取出的字符
    const char *s;  // 用来保存 %s 取出的字符串
    long num;       // 用来保存取出的整数参数

    int width;      // 输出宽度
    int long_flag;  // 是否使用 long 类型(例如 %ld)
    int neg_flag;   // 当前数字是否为负数(仅对 %d 有意义)
    int ladjust;    // 是否左对齐,1 左对齐,0 右对齐
    char padc;      // 填充字符,通常是 ' ' 或 '0'

    for (;;) {
        /* 扫描 fmt,找到下一个 '%' 或字符串结束 '\0' */

        const char *nw = fmt;  // nw 用来向后扫描
        int len = 0;           // 记录普通文本(非格式串部分)的长度

        // 一直扫描到 '%' 或字符串结束
        for (; *nw != '%' && *nw != '\0'; nw++) {
            len++;
        }

        /* 先把 fmt 到 nw 之间的普通字符串直接输出 */
        out(data, fmt, len);

        /* 如果已经到达字符串结尾,就退出循环 */
        if (*nw == '\0') {
            break;
        }

        /* 走到这里说明遇到了 '%',跳过它,开始解析格式 */
        fmt = nw + 1;

        /* 解析格式标志 flag
         * 默认:右对齐、空格填充
         */
        padc = ' ';
        ladjust = 0;

        /* 最多解析两个 flag:'-' 和 '0'
         * 例如:%-8d 或 %08x
         */
        for (int i = 1; i <= 2; i++) {
            if (*fmt == '-') {
                ladjust = 1;   // 左对齐
            } else if (*fmt == '0') {
                padc = '0';    // 用 0 填充
            } else {
                break;         // 不是 flag,停止解析
            }
            fmt++;
        }

        /* 解析宽度 width,例如 %8d、%04x 中的 8 或 4 */
        width = 0;
        while (*fmt <= '9' && *fmt >= '0') {
            width = width * 10 + (*fmt - '0');
            fmt++;
        }

        /* 检查是否有 long 标志,例如 %ld、%lx */
        long_flag = 0;
        if (*fmt == 'l') {
            long_flag = 1;
            fmt++;
        }

        neg_flag = 0;   // 默认不是负数

        /* 根据格式字符进行分支处理 */
        switch (*fmt) {
        case 'b':   // 二进制输出
            if (long_flag) {
                num = va_arg(ap, long int);
            } else {
                num = va_arg(ap, int);
            }
            print_num(out, data, num, 2, 0, width, ladjust, padc, 0);
            break;

        case 'd':   // 十进制有符号输出
        case 'D':
            if (long_flag) {
                num = va_arg(ap, long int);
            } else {
                num = va_arg(ap, int);
            }

            /* 如果是负数:
             * 1. 记录 neg_flag = 1
             * 2. 把 num 转成正数部分
             * 最终 print_num 会负责加上 '-'
             */
            neg_flag = 0;
            if (num < 0) {
                neg_flag = 1;
                num = -num;
            }

            print_num(out, data, num, 10, neg_flag, width, ladjust, padc, 0);
            break;

        case 'o':   // 八进制输出
        case 'O':
            if (long_flag) {
                num = va_arg(ap, long int);
            } else {
                num = va_arg(ap, int);
            }
            print_num(out, data, num, 8, 0, width, ladjust, padc, 0);
            break;

        case 'u':   // 十进制无符号风格输出
        case 'U':
            if (long_flag) {
                num = va_arg(ap, long int);
            } else {
                num = va_arg(ap, int);
            }
            print_num(out, data, num, 10, 0, width, ladjust, padc, 0);
            break;

        case 'x':   // 十六进制小写输出
            if (long_flag) {
                num = va_arg(ap, long int);
            } else {
                num = va_arg(ap, int);
            }
            print_num(out, data, num, 16, 0, width, ladjust, padc, 0);
            break;

        case 'X':   // 十六进制大写输出
            if (long_flag) {
                num = va_arg(ap, long int);
            } else {
                num = va_arg(ap, int);
            }
            print_num(out, data, num, 16, 0, width, ladjust, padc, 1);
            break;

        case 'c':   // 字符输出
            c = (char)va_arg(ap, int);
            print_char(out, data, c, width, ladjust);
            break;

        case 's':   // 字符串输出
            s = (char *)va_arg(ap, char *);
            print_str(out, data, s, width, ladjust);
            break;

        case '\0':  // 如果 '%' 正好出现在格式串结尾
            fmt--;
            break;

        default:
            /* 如果是不支持的格式字符,就原样输出这个字符本身 */
            out(data, fmt, 1);
        }

        /* 移动到下一个字符,继续处理剩余格式串 */
        fmt++;
    }
}

/* --------------- 本地辅助函数 --------------------- */

/**
 * @brief 输出一个字符,并根据 length 和 ladjust 决定是否补空格
 *
 * @param out     输出回调函数
 * @param data    传给回调函数的额外参数
 * @param c       要输出的字符
 * @param length  最小输出宽度
 * @param ladjust 是否左对齐
 *
 * @example
 *     print_char(out, NULL, 'A', 5, 0);  // 输出 "    A"
 *     print_char(out, NULL, 'A', 5, 1);  // 输出 "A    "
 */
void print_char(fmt_callback_t out, void *data, char c, int length, int ladjust) {
    int i;

    /* 宽度至少为 1,因为至少要输出这个字符本身 */
    if (length < 1) {
        length = 1;
    }

    const char space = ' ';

    if (ladjust) {
        /* 左对齐:先输出字符,再补空格 */
        out(data, &c, 1);
        for (i = 1; i < length; i++) {
            out(data, &space, 1);
        }
    } else {
        /* 右对齐:先补空格,再输出字符 */
        for (i = 0; i < length - 1; i++) {
            out(data, &space, 1);
        }
        out(data, &c, 1);
    }
}

/**
 * @brief 输出字符串,并根据宽度进行左右对齐
 *
 * @param out     输出回调函数
 * @param data    传给回调函数的额外参数
 * @param s       要输出的字符串
 * @param length  最小输出宽度
 * @param ladjust 是否左对齐
 *
 * @example
 *     print_str(out, NULL, "abc", 6, 0); // 输出 "   abc"
 *     print_str(out, NULL, "abc", 6, 1); // 输出 "abc   "
 */
void print_str(fmt_callback_t out, void *data, const char *s, int length, int ladjust) {
    int i;
    int len = 0;
    const char *s1 = s;

    /* 手动计算字符串长度 */
    while (*s1++) {
        len++;
    }

    /* 如果给定宽度比字符串本身短,就按实际长度输出 */
    if (length < len) {
        length = len;
    }

    if (ladjust) {
        /* 左对齐:先输出字符串,再补空格 */
        out(data, s, len);
        for (i = len; i < length; i++) {
            out(data, " ", 1);
        }
    } else {
        /* 右对齐:先补空格,再输出字符串 */
        for (i = 0; i < length - len; i++) {
            out(data, " ", 1);
        }
        out(data, s, len);
    }
}

/**
 * @brief 按指定进制输出数字
 *
 * 这是整个格式化输出中最核心的数字处理函数。
 *
 * 它的处理思路是:
 * 1. 先把数字逐位取出,按“逆序”存入缓冲区
 *    例如 123 会先存成 '3' '2' '1'
 * 2. 如果是负数,则在逆序缓冲区后面加 '-'
 * 3. 根据 length 决定是否需要补空格或补 0
 * 4. 再把整个缓冲区反转过来,得到最终正确顺序
 * 5. 调用 out 输出
 *
 * @param out      输出回调函数
 * @param data     传给回调函数的额外参数
 * @param u        数字值(这里按无符号数处理)
 * @param base     进制:2、8、10、16
 * @param neg_flag 是否需要加负号
 * @param length   最小输出宽度
 * @param ladjust  是否左对齐
 * @param padc     补齐字符,通常为 ' ' 或 '0'
 * @param upcase   十六进制字母是否大写
 *
 * @example
 *     print_num(out, NULL, 255, 16, 0, 4, 0, '0', 0); // 输出 "00ff"
 *     print_num(out, NULL, 255, 16, 0, 4, 0, '0', 1); // 输出 "00FF"
 *     print_num(out, NULL, 12, 10, 1, 5, 0, '0', 0);  // 输出 "-0012"
 */
void print_num(fmt_callback_t out, void *data, unsigned long u, int base, int neg_flag, int length,
           int ladjust, char padc, int upcase) {
    /* 算法说明:
     * 1. 先把数字按“逆序”写入 buf
     * 2. 如果输出宽度 length 更大,则补 padc
     * 3. 若不是左对齐,则把整个串反转
     * 4. 若是左对齐,则只反转数字部分,后面的填充保持在右侧
     */

    int actualLength = 0;     // 数字本身(含负号)的实际长度
    char buf[length + 70];    // 临时缓冲区,预留额外空间给数字转换
    char *p = buf;            // p 始终指向当前写入位置
    int i;

    /* 把数字转换为指定进制,按逆序写入缓冲区 */
    do {
        int tmp = u % base;   // 取当前最低位

        if (tmp <= 9) {
            *p++ = '0' + tmp;  // 0~9
        } else if (upcase) {
            *p++ = 'A' + tmp - 10;  // A~F
        } else {
            *p++ = 'a' + tmp - 10;  // a~f
        }

        u /= base;  // 去掉最低位
    } while (u != 0);

    /* 如果需要负号,就把 '-' 也放进缓冲区(仍然是逆序状态) */
    if (neg_flag) {
        *p++ = '-';
    }

    /* 计算当前实际长度 */
    actualLength = p - buf;

    /* 如果指定宽度比实际长度小,则按实际长度输出 */
    if (length < actualLength) {
        length = actualLength;
    }

    /* 添加填充字符 */
    if (ladjust) {
        /* 左对齐时不能用 0 填充,只能用空格补右边 */
        padc = ' ';
    }

    /* 特殊情况:
     * 如果是负数 + 右对齐 + 0 填充,
     * 负号应该出现在最前面,例如 -0012,而不是 00-12
     */
    if (neg_flag && !ladjust && (padc == '0')) {
        for (i = actualLength - 1; i < length - 1; i++) {
            buf[i] = padc;
        }
        buf[length - 1] = '-';
    } else {
        /* 普通补齐:直接在后面补 padc,最后统一反转 */
        for (i = actualLength; i < length; i++) {
            buf[i] = padc;
        }
    }

    /* 准备反转字符串 */
    int begin = 0;
    int end;

    if (ladjust) {
        /* 左对齐时,只反转数字实际部分,右边补空格不反转 */
        end = actualLength - 1;
    } else {
        /* 右对齐时,整个长度范围都要反转 */
        end = length - 1;
    }

    /* 原地反转 buf[begin..end] */
    while (end > begin) {
        char tmp = buf[begin];
        buf[begin] = buf[end];
        buf[end] = tmp;
        begin++;
        end--;
    }

    /* 把最终结果输出 */
    out(data, buf, length);
}