前言
这次还是课程实验。第二次吐槽,这么有意思的事情居然被框在平台的题目评测上,认真写的欲望一下子就降低了,不过选题依旧有价值,还是自己好好研究一下吧
以上是我曾经说出来的话。但当我经历明明实验完成了,评测程序一遍遍不通过,搞得我只好再来一次这种事,最终在配环境和运行这两个最简单的地方卡了整整两天,一共重来了6次。现在我只会说去你的应试教育
对,上面的话是我在两小时前说出来的,但现在已经结束了配环境,开始第一个正式实验了。确实很有意思啊,我又反悔,决定进行记录了
环境
我使用的是Ubuntu 24.04.4 LTS 64位 的虚拟机
- CPU:4 核
- 内存:8GB
- 硬盘:50GB
- 网络:NAT模式

在 Ubuntu 上编译一堆 C + 汇编 → 变成 RISC-V 的内核 → 用 QEMU 模拟一台 RISC-V 机器 → 把你的内核跑起来
安装依赖
|
|
交叉编译工具链
|
|
测试:
|
|
安装 QEMU
|
|
测试:
|
|
进入系统
操作系统运行在 QEMU 虚拟平台上,通过 make 构建生成内核与用户程序镜像。QEMU 模拟 RISC-V 硬件环境,执行 bootloader 后进入内核,最终加载用户程序执行。
系统结构分为 user、kernel 和虚拟硬件三层,其中 user 程序通过系统调用访问内核服务。
克隆好代码,进入代码文件夹,对源码进行编译
|
|
1. 安装 Docker(Ubuntu)
|
|
启动服务:
|
|
验证:
|
|
2. 加入权限(避免一直 sudo)
|
|
二、导入实验镜像(关键步骤)
你老师给的是:
|
|
假设你已经下载好:
|
|
导入镜像:
|
|
查看镜像:
|
|
三、启动实验容器(核心环境)
先建目录(用于代码映射):
|
|
启动容器:
|
|
进入后你会看到类似:
|
|
四、检查工具链是否正常
在容器里执行:
|
|
只要这三个正常,说明环境OK。
五、拉取头歌实验代码(你后面所有操作都在这里)
进入映射目录:
|
|
复制你头歌给的链接:
|
|
克隆:
|
|
系统调用与中断
一、先进入容器
|
|
进入后:
|
|
二、体验 QEMU 用户态模拟
这个最简单。
1. 创建 hello.c
进入:
|
|
创建:
|
|
写入实验给的代码。
2. 编译用户程序
回到项目根目录:
|
|

3. 用 QEMU 用户态运行
|
|
这里调用的是:
QEMU 的“用户态模拟”。
本质是:
|
|
你应该看到:
|
|
4. 查看系统调用(重点)
|
|
你会看到:
|
|
这个非常重要。
因为:
后面你自己的 OS 必须表现得和 Linux 类似
所以这个日志是“参考答案”。

三、运行你自己的操作系统
1. 清理构建(很重要)
实验文档已经强调了。
每次切换:
- debug
- CPU数量
- 配置参数
之前都要:
|
|
2. 启动 OS
遇到问题脚本没执行权限。
直接:
1chmod +x run-qemu.sh然后再运行:
1./run-qemu.sh debug=on如果后面
run-gdb.sh也报一样的问题,同样:
1chmod +x run-gdb.shLinux 里从 Windows 复制、解压、git 拉下来的脚本,经常会丢失 executable 权限。
|
|
这里调用的是:
|
|
这是“系统级模拟”。
它不是运行单个程序,而是:
|
|
包括:
- CPU
- 内存
- 磁盘
- SBI
- virtio设备
3. 在 OS shell 里运行 hello
进入 shell 后:
|
|
你会看到:
|
|
这些是你自己的 OS 输出的 syscall 日志。

四、最关键的实验思想(一定理解)
你现在有两份日志:
Linux + QEMU 用户态
|
|
你自己的 OS
|
|
然后你对比:
|
|
如果不一致:
- syscall 参数错了
- 返回值错了
- 文件系统错了
- 内存错了
五、过滤 syscall 日志(实验重点)
编辑:
|
|
找到:
|
|
改成:
|
|
作用:
|
|
否则整个系统日志会爆炸。


六、GDB 调试(后面实验会频繁用)
1. 启动调试版 QEMU
|
|
系统会停住等待 GDB。

2. 新开终端进入容器
启动:
|
|
连接:
|
|

提示找不到文件,原因是gdb没有进入容器而是在宿主机,进入容器的实验目录即可
3. 常用 GDB 命令
断点:
|
|
继续:
|
|
单步:
|
|
查看寄存器:
|
|
查看调用栈:
|
|
遇到问题:评测不通过,命令改成eh –lab-id 2 -t解决
调试
先别急着改代码,先按实验流程看“它到底报什么错”。
运行:
|
|
把最后的报错贴出来。
这个 lab 的思路不是乱找,而是:
- 看错误
- 顺着启动链找
- 定位 init0 为什么没起来
实验提示已经把范围缩很小了:
|
|
说明问题大概率在:
- init0 名字
- exec 加载
- 文件镜像
- 第一个用户进程创建
这几个地方之一。
先把 QEMU 输出给我。
这个实验本质是在让你理解:
|
|
你现在看到的报错:
|
|
不是随机报错。
这是有人故意把系统“第一个启动的用户程序”改错了。
你现在一步一步看。
系统启动过程其实是:
|
|
这里最关键的是:
|
|
实验就是在这里动了手脚。
你已经找到:
|
|
这里是:
|
|
重点来了。
这里并不是:
|
|
而是:
|
|
说明:
|
|
所以继续追。
你用了:
|
|
找到:
|
|
这就是真正 bug。
正常应该是:
|
|
但实验故意改成:
|
|
于是启动过程变成:
|
|
为什么会打印:
|
|
因为这个“masquerade”程序本身就是故意放进去的假程序。
它被启动后:
- 不进入 shell
- 不正常工作
- 只打印一句提示
- 然后退出
而:
|
|
才是真正系统初始化程序。
Linux 里其实也有类似概念:
|
|
如果它退出:
|
|
你这里看到:
|
|
就是同类逻辑。
因为:
|
|
否则系统没人管理了。
所以修复步骤:
打开:
|
|
找到:
|
|
改成:
|
|
保存。
然后重新编译:
|
|
这里必须 make clean。
因为:
|
|
很多 .o 文件缓存可能不会完全重新生成。
之后系统会:
|
|
你可以把这个实验理解成:
|
|
重点不是改那一行。
而是理解:
|
|
以及:
|
|
为了让你对这次 RISC-V 系统调用实验有最深层的掌握,我将整个过程拆解为“底层逻辑与操作流程”以及“多维排错指南”。
🛠️ 第一部分:系统调用实现全流程
实现一个系统调用的核心在于:建立用户态与内核态的通信桥梁。
1. 静态注册:建立编号映射
内核需要一个索引来识别用户到底想打哪个“电话”。
- 文件路径:
entry/syscall.tbl - 操作:添加一行
666 echo sys_echo。 - 底层原理:构建脚本(如
syscall_gen.sh)会读取此表,生成一个函数指针数组。当a7寄存器为 666 时,内核会自动索引到sys_echo这个函数符号。
2. 内核实现:跨越特权级的数据处理
这是实验的核心代码,必须处理地址空间隔离问题。
- 文件路径:
src/sys_echo.c - 核心逻辑:
C
|
|
3. 编译集成:配置 Makefile
- 文件路径:
src/Makefile - 操作:添加
obj-y += sys_echo.o。 - 底层原理:Kbuild 系统会递归扫描该变量,将
sys_echo.c编译并链接进built-in.o,最终合并入内核镜像。
4. 用户态调用:触发陷阱
- 文件路径:
user/src/test_echo.c - 操作:
C
|
|
🔍 第二部分:全维度排错指南 (Debug Ledger)
根据你的实验记录,我们将报错分为四个等级:
1. 预编译级:头文件与标准库陷阱
- 典型报错:
fatal error: stdio.h: No such file or directory - 错误逻辑:在内核里使用
#include <stdio.h>。 - 深度解析:内核是裸机程序 (Bare-metal),不运行在任何操作系统之上,所以没有标准库。
- 对策:删除所有标准库头文件。内核打印只能用框架提供的
printf.h。
2. 编译级:隐式声明错误
- 典型报错:
error: implicit declaration of function 'printf' [-Werror=implicit-function-declaration] - 错误逻辑:没包含
printf.h就直接用了printf。 - 深度解析:由于开启了
-Werror,编译器要求所有函数必须先声明后使用。 - 对策:包含正确的内核头文件。如果框架里的
common.h没写好,就手动包含#include "printf.h"。
3. 链接级:符号丢失与 Makefile 权限
- 典型报错:
undefined reference to 'sys_echo'或E212: Can't open file for writing - 错误逻辑:Makefile 没改对,或者改了没保存。
- 深度解析:链接器(ld)在生成最终镜像时,发现
syscall_table里有一个叫sys_echo的坑位,但找不到对应的机器码实现。 - 对策:
- 检查
src/Makefile里的文件名是否拼写正确(是.o不是.c)。 - 解决权限问题:使用
chmod 666 src/Makefile赋予写权限,或在保存时使用:w!。
- 检查
4. 运行级:指针解引用与类型转换
- 典型报错:
passing argument 2 of 'copy_from_user' makes integer from pointer without a cast - 错误逻辑:直接把
char *传给copy_from_user。 - 深度解析:在内核代码里,用户空间的地址只是一个符号,内核无法直接跳转到那个地址取值(硬件特权级限制)。
copy_from_user期望的是一个uint64_t类型的地址数值。 - 对策:使用
uint64_t addr; argaddr(0, &addr);拿到的addr才是正确的。
💡 总结建议
这次实验你遇到的最大阻碍是内核开发的隔离感。记住:内核里没有“魔法”,所有用户态看起来理所应当的 printf 或指针访问,在内核里都要通过显式包含头文件和软件页表翻译(copy_from_user)来实现。
现在,你可以放心运行 ./run-qemu.sh,在 Shell 里输入 test_echo 见证你的第一个系统调用了。
没有找到test——echo是因为写错文件夹
太棒了,看到 test_echo 成功出现在 ls 列表中且编译通过,说明你的整个工具链(交叉编译器、CMake、磁盘镜像打包)已经完全跑通了。
针对刚才“命令输入错误”导致 execvp failure 的这一关键环节,我已将其整理入最终版的排错与操作指南中。
🛠️ 第一部分:系统调用实现·标准操作流程
实现一个系统调用是内核开发中典型的“纵向贯穿”任务,涉及以下四个关键层级:
1. 静态注册层 (entry/syscall.tbl)
- 动作:添加
666 echo sys_echo。 - 目的:分配系统调用号,并将号与内核函数符号关联。
2. 内核实现层 (src/sys_echo.c)
- 动作:使用
argaddr和argint获取寄存器参数,用copy_from_user拷贝内存,最后用printf打印。 - 注意:必须包含
#include "printf.h"以满足内核环境下的函数声明需求。
3. 用户接口层 (user/include/syscall.h)
- 动作:确保定义了对应的号(如果框架未自动同步)。
- 目的:让用户态程序知道
666代表的是哪个功能。
4. 测试构建层 (user/src/test_echo.c & Makefile)
- 动作:在
user/Makefile(或 CMakeLists)中添加test_echo目标,并编写调用syscall(666, ...)的测试代码。
🔍 第二部分:深度排错指南 (Troubleshooting Ledger)
我们将你踩过的坑按发生阶段重新归类,这能帮你建立起一套职业的内核调试思维。
1. 编译链接级:环境与声明错误
- 报错:
implicit declaration of function 'printf'或undefined reference to 'printf'。 - 根因:内核不包含标准 C 库。
- 对策:禁止包含
<stdio.h>;必须包含内核专属头文件(如"printf.h")。
2. 构建级:镜像同步与权限错误
- 报错:
ls找不到文件,或E212: Can't open file for writing。 - 根因:Makefile 没改对、未保存或没有权限。
- 对策:
- 使用
chmod或sudo解决权限。 - 核心准则:修改代码后,必须在根目录执行
make clean && make以强制重新生成磁盘镜像fs.img。
- 使用
3. 加载运行级:execvp failure (关键坑点)
- 现象:尝试运行程序时,Shell 报错
execvp failure。 - 根因 A(文件类型错):输入了源码文件名(如
./test_echo.c)。内核无法将文本文件识别为 ELF 格式。 - 根因 B(路径不支持):有些简易 Shell 不支持相对路径
./。 - 根因 C(格式损坏):由于未
make clean,导致旧的二进制残留或链接脚本失效。 - 对策:
- 检查输入:只输入程序名
test_echo,不要带.c。 - 验证格式:确保它是经由
riscv64-unknown-elf-gcc编译出的 ELF 文件。
- 检查输入:只输入程序名
4. 内核逻辑级:指针解引用错误
- 报错:
passing argument 2 ... makes integer from pointer或内核 Panic。 - 根因:试图在内核里直接使用用户态传来的指针。
- 对策:参数必须用
uint64_t接收地址值,并强制通过copy_from_user转换。
💡 终极复盘心得
“在内核的世界里,文件名和文件是两回事,地址和指针是两回事。”
这次实验最深刻的教训在于:内核是非常死板的。 你输入 test_echo.c 时,内核忠实地去读文件头的 4 个字节,发现不是 0x7f 0x45 0x4c 0x46 (ELF 的魔数),于是果断报出 execvp failure。
现在,去掉后缀,再次输入 test_echo,迎接你的成功输出吧!


