文章目录:
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是什么
一般有三种类型:
- 可重定位文件,例如:
*.o,通常叫“目标文件”,还不能直接运行,需要链接。 - 可执行文件,例如:链接完成后的程序(最终mos文件),可以被操作系统加载运行。
- 共享对象文件,例如:
*.so,是动态链接库。
「3」ELF结构
分为这几个部分:
分为几个部分,看图很好理解:
- ELF头,包括程序的基本信息,比如体系结构和操作系统,同时也包含了节头表和段头表相对文件的偏移量(offset)。
- 段头表(或程序头表),主要包含程序中各个段(segment)的信息, 段的信息需要在运行时刻使用。
- 节头表,主要包含程序中各个节(section)的信息,节的信息需要在程序编译和链接的时候使用。
- 段头表中的每一个表项,记录了该段数据载入内存时的目标位置等,记录了用于指导应用程序加载的各类信息。
- 节头表中的每一个表项,记录了该节程序的代码段、数据段等各个段的内容,主要是链接器在链接的过程中需要使用。
即通过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内容:
各列含义如下:
- Type:段类型
- Offset:这段内容在 ELF 文件中的偏移
- VirtAddr:加载到进程虚拟地址空间后的地址
- PhysAddr:物理地址,现代 OS 下一般不太关心
- FileSiz:文件中这段占多少字节
- MemSiz:装入内存后这段应占多少字节(MemSiz >= FileSiz,因为ELF里面有的在.bss段)
- Flags:权限
这块内容为了说明什么?>> 模拟器加载内核的时候,也需要将内核代码、数据加载到对应的位置上,才能保证内核正常运行 >> 所以我们需要知道内核的正确加载位置是哪里。
(3)MIPS内存布局
程序中一般使用的是虚拟地址,通过MMU转为物理地址发往总线访存。
本次实验使用的MIPS虚拟地址空间为4GB,划分为四个大区域:
| 区域 | 地址范围 | 大小 | 访问权限 | 是否经 TLB | 是否缓存 |
|---|---|---|---|---|---|
| kuseg | 0x00000000 - 0x7FFFFFFF | 2GB | 用户态 / 内核态都可用 | 是 | 取决于页属性,通常可缓存 |
| kseg0 | 0x80000000 - 0x9FFFFFFF | 512MB | 仅内核态 | 否,直接映射 | 是 |
| kseg1 | 0xA0000000 - 0xBFFFFFFF | 512MB | 仅内核态 | 否,直接映射 | 否 |
| kseg2/3 | 0xC0000000 - 0xFFFFFFFF | 1GB | 仅内核态 | 是 | 取决于页属性,通常可缓存 |
因为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 段都放进来。
.text :
定义输出段名字叫.text{ *(.text) }*表示所有目标文件(.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函数
代码结构
- kern/machine.c:这个文件负责往 QEMU 的控制台输出字符,其原理为读写某一个特殊 的内存地址。
- kern/printk.c:此文件中,实现了 printk,但其所做的,实际上是把输出字符的函数, 接受的输出参数给传递给了vprintfmt 这个函数。
- lib/print.c:此文件中,实现了 vprintfmt 函数,其实现了格式化输出的主体逻辑。
变长参数处理—stdarg.h
va_list,变长参数表的变量类型;va_start(va_list ap, lastarg),用于初始化变长参数表的宏,其中lastarg为该函数最后一个命名的形式参数,比如:void f(int a, int b, ...) { va_list ap; va_start(ap, b); // b 是 ... 前最后一个命名参数,ap取到的第一个是b }va_arg(va_list ap, 类型),用于取变长参数表下一个参数的宏,如int num = va_arg(ap, int),然后ap会指向下一个参数;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指令
| 类别 | 指令 | 格式 | 作用 | 备注 |
|---|---|---|---|---|
| 算术 | add | add rd, rs, rt | rd = rs + rt | 有符号加法,检查溢出 |
| 算术 | addu | addu rd, rs, rt | rd = rs + rt | 不检查溢出 |
| 算术 | addi | addi rt, rs, imm | rt = rs + imm | 立即数加法 |
| 算术 | addiu | addiu rt, rs, imm | rt = rs + imm | 不检查溢出 |
| 算术 | sub | sub rd, rs, rt | rd = rs - rt | 有符号减法 |
| 算术 | subu | subu rd, rs, rt | rd = rs - rt | 不检查溢出 |
| 算术 | mul | mul rd, rs, rt | 乘法结果低位放 rd | 有些环境支持 |
| 算术 | mult | mult rs, rt | 乘积放入 HI/LO | 高位在 HI,低位在 LO |
| 算术 | multu | multu rs, rt | 无符号乘法 | 结果放 HI/LO |
| 算术 | div | div rs, rt | 商到 LO,余数到 HI | 有符号除法 |
| 算术 | divu | divu rs, rt | 商到 LO,余数到 HI | 无符号除法 |
| 特殊寄存器 | mflo | mflo rd | rd = LO | 取乘除法结果的商/低位 |
| 特殊寄存器 | mfhi | mfhi rd | rd = HI | 取乘除法余数/高位 |
| 特殊寄存器 | mtlo | mtlo rs | LO = rs | 写 LO |
| 特殊寄存器 | mthi | mthi rs | HI = rs | 写 HI |
| 逻辑 | and | and rd, rs, rt | 按位与 | |
| 逻辑 | andi | andi rt, rs, imm | 按位与立即数 | 常用于掩码 |
| 逻辑 | or | or rd, rs, rt | 按位或 | |
| 逻辑 | ori | ori rt, rs, imm | 按位或立即数 | 常配合 lui |
| 逻辑 | xor | xor rd, rs, rt | 按位异或 | |
| 逻辑 | xori | xori rt, rs, imm | 按位异或立即数 | |
| 逻辑 | nor | nor rd, rs, rt | 按位或后取反 | 常用于实现 not |
| 移位 | sll | sll rd, rt, shamt | 逻辑左移 | 低位补 0 |
| 移位 | srl | srl rd, rt, shamt | 逻辑右移 | 高位补 0 |
| 移位 | sra | sra rd, rt, shamt | 算术右移 | 高位补符号位 |
| 移位 | sllv | sllv rd, rt, rs | 可变左移 | 位数在 rs 中 |
| 移位 | srlv | srlv rd, rt, rs | 可变逻辑右移 | |
| 移位 | srav | srav rd, rt, rs | 可变算术右移 | |
| 比较 | slt | slt rd, rs, rt | 若 rs < rt,则 rd=1 | 有符号比较 |
| 比较 | sltu | sltu rd, rs, rt | 若 rs < rt,则 rd=1 | 无符号比较 |
| 比较 | slti | slti rt, rs, imm | rs < imm 则 rt=1 | 有符号比较 |
| 比较 | sltiu | sltiu rt, rs, imm | rs < imm 则 rt=1 | 无符号比较 |
| 访存 | lw | lw rt, offset(base) | 读一个字到寄存器 | 4 字节 |
| 访存 | sw | sw rt, offset(base) | 把一个字写入内存 | 4 字节 |
| 访存 | lb | lb rt, offset(base) | 读一个字节 | 符号扩展 |
| 访存 | lbu | lbu rt, offset(base) | 读一个字节 | 零扩展 |
| 访存 | sb | sb rt, offset(base) | 写一个字节 | |
| 访存 | lh | lh rt, offset(base) | 读半字 | 符号扩展,2 字节 |
| 访存 | lhu | lhu rt, offset(base) | 读半字 | 零扩展 |
| 访存 | sh | sh rt, offset(base) | 写半字 | 2 字节 |
| 访存 | ll | ll rt, offset(base) | 原子读 | 配合 sc |
| 访存 | sc | sc rt, offset(base) | 原子写 | 配合 ll |
| 跳转/分支 | beq | beq rs, rt, label | 若相等则跳转 | 常用于 if |
| 跳转/分支 | bne | bne rs, rt, label | 若不等则跳转 | |
| 跳转/分支 | bgez | bgez rs, label | 若 rs >= 0 则跳转 | 和 0 比较 |
| 跳转/分支 | bgtz | bgtz rs, label | 若 rs > 0 则跳转 | |
| 跳转/分支 | blez | blez rs, label | 若 rs <= 0 则跳转 | |
| 跳转/分支 | bltz | bltz rs, label | 若 rs < 0 则跳转 | |
| 跳转/分支 | j | j label | 无条件跳转 | |
| 跳转/分支 | jal | jal label | 跳转并保存返回地址 | 返回地址进 $ra |
| 跳转/分支 | jr | jr rs | 跳到寄存器中的地址 | 常写 jr $ra |
| 跳转/分支 | jalr | jalr rd, rs | 间接调用 | 常用于函数指针 |
| 数据装入 | lui | lui rt, imm | 立即数放高 16 位 | 低 16 位补 0 |
| 伪指令 | li | li rt, imm | 装立即数 | 伪指令 |
| 伪指令 | la | la rt, label | 装地址 | 伪指令 |
| 伪指令 | move | move rd, rs | rd = rs | 常等价于 addu rd, rs, $zero |
| 伪指令 | neg | neg rd, rs | rd = -rs | 伪指令 |
| 伪指令 | not | not rd, rs | 按位取反 | 常等价于 nor rd, rs, $zero |
| 伪指令 | nop | nop | 空操作 | 常等价于 sll $zero, $zero, 0 |
| 系统调用 | syscall | syscall | 执行系统调用 | 服务号放 $v0 |
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);
}



