hnu操作系统-实验踩坑实录
建议大家一边做一边给虚拟机拍个快照;用wsl2的友友们给子系统备个份。
血泪教训。
lab1
实验目的
- 完成实验环境配置
- 熟悉cmake构建系统
- 学会调试
安装工具链
宿主机: Description: Ubuntu 24.04.2 LTS
找一个合适的目录下载工具链,解压并重命名工具链目录:
1 |
|
arrch64环境变量配置
添加环境变量:
1 |
|
进入vim编辑器后按进入insert模式,在文件末尾插入:
1 |
|
重新加载~/.bashrc文件
1 |
|
测试工具链是否安装成功
1 |
|
QEMU安装
命令行安装qemu
1 |
|
==报错: Package ‘qemu’ has no installation candidate==
查阅官方文档
直接输入以上命令即可
安装cmake
Linux
1 |
|
创建裸机(Bare Metal)程序
由于我们的目标是编写一个操作系统,所以我们需要创建一个独立于操作系统的可执行程序,又称 独立式可执行程序(freestanding executable) 或 裸机程序(bare-metal executable) 。这意味着所有依赖于操作系统的库我们都不能使用。比如 std 中的大部分内容(io, thread, file system, etc.)都需要操作系统的支持,所以这部分内容我们不能使用。
创建项目
项目结构
最好跟实验文档的项目目录创建得一模一样,省得后续改一堆路径。
命令使用:
1 |
|
==注意:prt_typedef.h,arrch64-qemu.ld需要下载完整文件。==
main.c源码
1 |
|
prt_typedef.h源码
https://os2024lab.readthedocs.io/zh-cn/latest/_static/prt_typedef.h
start.S 源码
1 |
|
prt_reset_vector.S 源码
1 |
|
关键硬件与概念
- 异常级别(Exception Level, EL)
- EL0:用户态(应用程序)。
- EL1:内核态(操作系统)。
- EL2:虚拟化监控态(Hypervisor)。
- EL3:安全监控态(Secure Monitor)。
- CurrentEL 寄存器
- ARMv8 系统寄存器,存储当前异常级别信息。
- 位
[3:2]
表示当前 EL:0b00
→ EL00b01
→ EL10b10
→ EL20b11
→ EL3
- DAIF 寄存器
- ARMv8 的系统寄存器,控制中断和异常的处理状态:
- D (Debug):调试异常使能。
- A (SError Abort):系统错误中止使能。
- I (IRQ):普通中断使能。
- F (FIQ):快速中断使能。
- 写入
1
到对应位会禁用相关事件响应。
- ARMv8 的系统寄存器,控制中断和异常的处理状态:
- BL 指令
- 分支链接(Branch with Link)指令,用于调用函数:
- 跳转到目标地址(如
main
)。 - 将返回地址(下一条指令地址)保存到
LR
(x30)寄存器
- 跳转到目标地址(如
- 分支链接(Branch with Link)指令,用于调用函数:
链接脚本 aarch64-qemu.ld 脚本
工程构建
操作系统是一个复杂的工程。如当前版本的 UniProton 包含了近 500 个文件,超过 10 万行的代码及说明,而 Linux 内核则包含有 6 万多个文件,超过 2700 万行的代码 (2020)。如果纯手工构建这样的系统是不可想象的,所以我们需要构建系统的帮助。
CMake 是一个跨平台的开源构建系统。CMake 通过简单的、与平台和编译器无关的配置文件来控制软件编译过程
CMakeList.txt
1 |
|
==记得修改交叉工具链aarch64-none-elf所在目录,即重命名后的gcc-arm-11.2-2022.02-x86_64-aarch64-none-elf。==
1 |
|
src/bsp/下的CMakeLists.txt
1 |
|
编译
在lab1中新建目录build,cd进build执行以下命令,”..”代表上级目录
1 |
|
[!tip] tip
输入命令时,要时刻注意所在目录,根据所在目录调整命令输入的路径
运行
在lab1下执行,输入命令
1 |
|
输出以下,即成功
调试
这里真的卡了我很久…先装的vscode,报错了一步一步debug,郁闷。
参考公众号:爱吃罐头的熊二,感谢前辈,感谢为我提供公众号的cy!
安装,运行vscode
命令行使用snap安装
1 |
|
vscode打开lab1(一定要是这一级目录,不然要在CMakeLists.txt改路径)
根据实验文档添加配置
在左边面板顶部选择刚添加的 aarch64-gdb 选项,点击旁边的绿色 开始调试(F5) 按钮开始调试。
此时报错:
deepseek告诉我,它找不到 libncursesw.so.5
共享库
检查一下gdb依赖项
1 |
|
输出这一大段,大概就是这几个共享库没有,非常遗憾,apt-get也没有这几个包:
1 |
|
想办法装上,有点忘了这块当时怎么搞的,弄了个软连接还是啥的
历史记录翻到这个,可以试试看
项目首页 - Ubuntulibncurses.so.5离线安装包:Ubuntu libncurses.so.5 离线安装包 - GitCode
python3.6.15安装
我们还缺一个libpython3.6m.so.1.0库,按照公众号的做法一步一步下来,连接国外源可能会有点问题,可以换个镜像源或者在主机上下好拉进虚拟机里
Python Release Python 3.6.15 | Python.org
python-release-source安装包下载_开源镜像站-阿里云
解压(找一个合适的目录装python哈)
1 |
|
到这一步可能会报错
1 |
|
可能是gcc版本导致的不相容
装个gcc10,修改一下默认的gcc版本,记得下完之后再改回来
1 |
|
自动化脚本
makeMiniEuler.sh
1 |
|
runMiniEuler.sh
1 |
|
vscode调试
打开 main.c 文件,点击 vscode左侧的运行和调试按钮,弹出对话框选择创建 launch.json文件,增加如下配置:
1 |
|
在lab1下新建makeMiniEuler.sh脚本编译项目,runMiniEuler.sh脚本运行项目
==在vscode下方终端里运行实验文档的连接,sh runMiniEuler.sh -S,一定要接-S哈==
开始调试
- 在左边面板顶部选择刚添加的 aarch64-gdb 选项,点击旁边的绿色 开始调试(F5) 按钮开始调试。
- 在调试控制台输入-exec gdb调试指令
- 查看指定地址的内存内容。在调试控制台执行 -exec x/20xw 0x40000000 即可,其中 x表示查看命令,20表示查看数量,x表示格式,可选格式包括 Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),t(binary), f(float), a(address), i(instruction), c(char) and s(string).Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).,最后的 w表示字宽,b表示单字节,h表示双字节,w表示四字节,g表示八字节。还可以是指令:-exec x/20i 0x40000000; 字符串:-exec x/20s 0x40000000
- 显示所有寄存器。-exec info all-registers
- 查看寄存器内容。-exec p/x $pc
- 修改寄存器内容。-exec set $x24 = 0x5
- 修改指定内存位置的内容。-exec set {int}0x4000000 = 0x1 或者 -exec set *((int *) 0x4000000) = 0x1
- 修改指定MMIO 寄存器的内容。 -exec set *((volatile int *) 0x08010004) = 0x1
- 退出调试 -exec q
- 要打断点的话,在settings里面勾选此项:
gdb调试
首先运行一下两个自动化脚本
新开一个终端,输入以下命令
1 |
|
在gdb窗口输入
1 |
|
作业1
请操作 NZCV 寄存器获取 start.S 中执行 CMP w6, w2 前后 NZCV 寄存器的变化。
预备知识
CPSR(Current Program Status Register,当前程序状态寄存器)是ARM处理器中的一个重要寄存器,用于存储当前处理器的状态信息。CPSR包含条件码标志、中断禁止位、当前处理器模式以及其他状态和控制信息
NZCV标志位存储在cpsr寄存器
标志位 | 名称 | 触发条件 |
---|---|---|
N | Negative | 运算结果的最高位为 1 (结果为负数)时置 1 。 |
Z | Zero | 运算结果为零时置 1 。 |
C | Carry | 无符号运算时发生进位(加法)或无借位(减法)) |
V | oVerflow | 有符号运算结果超出可表示范围时置 1 。 |
调试过程
- 打上断点
- 查看cpsr值
-CPSR = 0x60003c5
转换为二进制:
0100 0000 0000 0000 0011 1100 0101
- NZCV ([31:28]
):0110
→N=0
,Z=1
,C=0
,V=0
- 含义:初始状态为 运算结果为零(Z=1
)。 - 执行CMP后查看cpsr值
-
CPSR = 0x60003c5
转换为二进制:
0110 0000 0000 0000 0011 1100 0101
- NZCV (
[31:28]
):0110
→N=0
,Z=1
,C=1
,V=0
- 含义:初始状态为 运算结果为零(
Z=1
)且无符号运算无进位(C=1
)。
- 含义:初始状态为 运算结果为零(
- NZCV (
-
作业2
商业操作系统都有复杂的构建系统,试简要分析 UniProton 的构建系统。
脚本链接
整个Uniproton系统项目如下
通过build.py脚本对系统进行构建
- 类功能表格
类/方法 | 功能描述 |
---|---|
Compile类 | 主构建控制器,管理整个编译流程 |
get_config() | 根据CPU类型和平台加载编译配置(编译器路径、库类型等) |
setCmdEnv() | 设置日志文件和构建时间标签 |
SetCMakeEnviron() | 配置CMake所需的环境变量(如HCC_PATH 、CPU_TYPE ) |
prepare_env() | 准备构建环境(调用get_config 和SetCMakeEnviron ) |
getOsPlatform() | 检测主机架构(x86/arm64/riscv64)并设置工具链路径 |
CMake() | 生成CMake构建目录并执行CMake命令 |
make() | 执行Make命令(编译和安装) |
UniProton_clean() | 清理构建生成的临时文件和日志 |
SdkCompaile() | 组合流程:生成构建定义 → CMake → Make |
==UniProtonCompile()== | ==对外接口,根据参数选择全量或部分构建== |
MakeBuildef() | 调用外部脚本生成构建定义文件(如prt_buildef.h ) |
- 流程解析
1. 初始化与环境准备
- 类实例化: def init
- 脚本通过
Compile
类的构造函数初始化编译所需的参数和路径。 - 参数包括
cpu_type
(CPU类型)、compile_option
(编译选项)、lib_run_type
(库运行平台)、make_choice
(编译目标选择)、make_phase
(编译阶段)等。 - 初始化过程中调用
getOsPlatform
方法获取当前操作系统平台信息,并将其写入环境变量。
- 脚本通过
- 环境变量设置:
SetCMakeEnviron
方法将一系列编译相关的配置(如CPU类型、平台类型、库类型、编译选项等)写入环境变量,供后续CMake使用。- 设置了
PATH
环境变量,确保编译工具链路径正确。
- 日志环境初始化:
setCmdEnv
方法生成构建时间标签,并设置日志目录和日志文件路径,为后续的日志记录做好准备。
2. 编译环境配置
- 获取编译配置:
get_config
方法根据cpu_type
和cpu_plat
从全局配置中加载编译所需的信息,包括库类型、平台类型、HCC路径、KConfig目录、系统类型和核心配置。- 如果未找到正确的配置,会记录错误日志并退出程序。
- 准备编译环境:
prepare_env
方法整合了上述步骤,完成编译环境的全面准备。- 包括调用
get_config
获取配置信息、调用setCmdEnv
设置日志环境、调用SetCMakeEnviron
设置环境变量
3. 清理缓存
- 清理操作:
- 如果
cpu_type
为clean
,调用UniProton_clean
方法清理项目构建过程中生成的临时文件和缓存。 - 清理范围包括
logs
、output
、__pycache__
等目录以及特定文件(如prt_buildef.h
)。
- 如果
4. CMake配置
- 生成Makefile:
CMake
方法负责调用CMake生成Makefile文件。- 根据不同的编译选项(如
normal
、fortify
、hllt
等),拼接相应的CMake命令。 - 如果生成失败,记录错误日志并返回
False
。
5. 执行Make构建
- Make构建过程:
make
方法根据make_phase
的值决定是否执行Make构建。- 首先执行
make clean
清理之前的构建结果。 - 然后执行
make all
构建目标,最后根据编译选项决定是否执行make install
安装目标。 - 构建过程中会记录日志,并在失败时返回
False
。
6. SDK编译
编译流程:
SdkCompaile
方法是整个编译的核心逻辑,包含以下几个步骤:- 判断当前环境中是否需要编译。如果不需要,则直接返回
True
。 - 调用
MakeBuildef
生成buildef文件。 - 调用
CMake
生成Makefile文件。 - 调用
make
并返回 - 如果编译失败,则记录失败日志并返回False
- 判断当前环境中是否需要编译。如果不需要,则直接返回
如果所有步骤都成功,则记录成功日志并返回
7.主入口逻辑
- 脚本执行入口:
- 在
if __name__ == "__main__":
块中,脚本解析命令行参数或使用默认参数。 - 遍历指定的平台列表,为每个平台创建
Compile
对象并调用==UniProtonCompile
==方法进行编译。 - 如果任意一个平台的编译失败,则退出程序并返回非零状态码。
- 在
作业3
学习调试项目
gdb调试
运行makeMiniEuler.sh runMiniEuler.sh 两个脚本,启动调试服务器,默认端口1234
1 |
|
新建终端,启动调试客户端
1 |
|
连接服务器
逐步调试
vscode调试
在终端启动调试服务器
创建launch.json文件->设置允许在任何文件 设置断点->F5开始调试->F10逐步 调试
调试控制台调试:
lab2 Hello miniEuler
实验目的
print函数是学习几乎任何一种软件开发语言时最先学习使用的函数,同时该函数也是最基本和原始的程序调试手段,但该函数的实现却并不简单。本实验的目的在于理解操作系统与硬件的接口方法,并实现一个可打印字符的函数(非系统调用),用于后续的调试和开发。
实验过程
Virt 机器详解
1. 基本概念
Virt 机器(Virt Machine)是一种轻量级虚拟机(Virtual Machine),通常由 QEMU 或类似模拟器提供,专为快速启动、开发和测试设计。
- 核心特点:
- 无需完整操作系统镜像(如磁盘文件),直接加载内核和内存文件运行。
- 虚拟化标准硬件(如 UART、RAM、CPU),不模拟特定物理设备。
- 常用于 嵌入式开发 和 操作系统内核调试(如 ARM Cortex-M/A、RISC-V)。
2. 典型应用场景
场景 | 说明 |
---|---|
嵌入式开发 | 测试裸机程序(如 start.S 启动代码)或 RTOS(如 UniProton)。 |
操作系统内核调试 | 快速启动 Linux 内核,无需完整磁盘镜像(如 qemu-system-arm -kernel zImage )。 |
教学与原型验证 | 提供干净的虚拟硬件环境,避免物理开发板的复杂性。 |
3. Virt 机器的硬件组成
Virt 机器模拟的硬件通常是最小化通用设备,例如:
- CPU:ARM Cortex-A53/A72、RISC-V 64 等。
- 内存:默认 128MB~1GB(可通过参数调整)。
外设:
- 串口(UART)用于输入输出。
- 虚拟中断控制器(GIC)。
- 简单帧缓冲区(可选,用于图形显示)。
通过QEMU导出设备树
1 |
|
QEMU 导出 Virt 虚拟机的硬件设备树二进制(virt.dtb
)。将二进制 DTB 转换为可读的 DTS 文件,用于分析或修改硬件配置。
virt.dtb转换后生成的virt.dts中可找到如下内容
由上可以看出,virt机器包含有pl011的设备,该设备的寄存器在0x9000000开始处。pl011实际上是一个UART设备,即串口。可以看到virt选择使用pl011作为标准输出,这是因为与PC不同,大部分嵌入式系统默认情况下并不包含VGA设备。
实现PRR_Printf函数
宏定义
技术手册
在 print.c 中包含所需头文件,并定义后续将会用到的宏,
1 |
|
[!NOTE] Title
注意到我们只是向UART0写入,而没从UART0读出(如果读出会读出其他设备通过串口发送过来的数据,而不是刚才写入的数据,注意体会这与读写内存时是不一样的,详情参见pl011的技术手册),编译器在优化时可能对这部分代码进行错误的优化,如把这些操作都忽略掉。在 ==UART_REG_READ 宏和 UART_REG_WRITE 宏中使用 volatile 关键字==的目的是告诉编译器,这些读取或写入有特定目的,不应将其优化(也就是告诉编译器不要瞎优化,这些写入和读出都有特定用途。如连续两次读,编译器可能认为第二次读就是前次的值,所以优化掉第二次读,但对外设寄存器的连续读可能返回不同的值。再比如写,编译器可能认为写后没有读所以写没有作用,或者连续的写会覆盖前面的写,但对外设而言对这些寄存器的写入都有特定作用)。
串口的初始化
1 |
|
往串口发送字符
1 |
|
支持格式化输出
1 |
|
实现vsnprintf_s 函数
源码
新建 src/bsp/vsnprintf_s.c 实现 vsnprintf_s 函数
vsnprintf_s 函数的主要作用是依据格式控制符将可变参数列表转换成字符列表写入缓冲区。
调用PRT_Printf函数
main.c 修改为调用 PRT_Printf 函数输出信息。
1 |
|
将新建文件纳入构建系统
修改 src/bsp/CMakeLists.txt 文件加入新增文件 print.c 和 vsnprintf_s.c
1 |
|
启用 FPU
构建项目并执行发现程序没有任何输出。 需启用 FPU (src/bsp/start.S)。
1 |
|
作业1
查询文档
DAT寄存器的transmit 和 receive fifo所在处
fifo和单字节传输规则:
LCR寄存器的FEN位控制FIFO使能
FLAG_REGISTER的TXFF,若FIFO使能为1且发送FIFO已满,设为1;
FLAG_REGISTER的TXFE,若FIFO使能为9且发送FIFO底位已空,设为1;
DATA REGISTER:
如果 FIFO 被禁用,UART 会退化为 单字节传输模式(即每个字节单独发送/接收),数据通过 数据寄存器(UARTDR) 直接传输。
修改代码使其可以动态切换数据传输模式
- 新增宏定义
1 |
|
宏定义解释
- 修改串口初始化函数,使其兼容两种数据发送模式
1 |
|
- 修改标志位检查函数
1 |
|
- 重新构建系统,运行成功
作业2
采用 UniProton 提供的 libboundscheck 库实现 vsnprintf_s 函数。
从gitee上下载libboundsheck库
将src中的文件放入项目目录下的bsp中,include下的头文件放入项目目录下的include中
修改bsp中的cmakelist.txt
重新构建系统,运行工程,虽然有很多警告XD,但是能跑
ds
(1) 数据写入发送FIFO
用户或程序通过向UART数据寄存器(如
UARTDR
)写入数据,数据被推送到发送FIFO。1
2
3
4
5
6// 示例:向UART发送字符串
const char *msg = "Hello, World!\n";
for (int i = 0; msg[i] != '\0'; i++) {
while (UART_TX_FIFO_FULL); // 等待FIFO有空位
UARTDR = msg[i]; // 写入发送FIFO
}
(2) UART硬件自动发送
- UART控制器从发送FIFO中按顺序取出数据,将其转换为串行信号:
- 添加起始位:逻辑低电平(0)。
- 数据位:8位(或5-9位,取决于配置)。
- 校验位(可选):奇偶校验位。
- 停止位:逻辑高电平(1),通常1-2位。
- 信号输出:通过UART的TX引脚以配置的波特率(如115200 bps)发送信号。
(3) 接收端UART接收
- 接收端UART(如PC的串口或嵌入式设备的UART)通过RX引脚捕获串行信号。
- 信号解析:去除起始位、停止位,校验数据完整性。
- 存入接收FIFO:解析后的数据字节和状态信息(如校验错误)被推送到接收FIFO。
(4) 驱动程序/操作系统处理
操作系统驱动(如Linux的TTY驱动)从接收FIFO中读取数据:
1
2
3
4
5// 示例:Linux驱动读取UART数据
while (!UART_RX_FIFO_EMPTY) {
char c = UARTDR; // 从接收FIFO读取
tty_insert_flip_char(&port->port, c, TTY_NORMAL); // 推送至TTY缓冲区
}TTY层:将数据传递给用户空间程序(如终端模拟器)。
(5) 显示终端输出
- 终端程序(如PuTTY、minicom或嵌入式系统的LCD驱动)将数据渲染到屏幕:
- 字符模式:直接显示ASCII字符。
- 图形模式:通过GUI框架(如Qt)渲染文本到显示器。