Hw1 配套阅读:NexOS 用户态程序创建指南¶
阅读目标¶
- 作为
Hw1与实验一的配套阅读材料,帮助你理解 NexOS 用户态应用程序的组织方式; - 学习 NexOS 用户态应用程序的组织方式;
- 掌握在 NexOS 中新增一个简单用户程序的完整流程;
- 为完成
Hw1中与用户程序相关的作业题,以及实验一中的后续实践任务做好准备。
阅读前提¶
- 虚拟机:VMware / vlab / WSL 等,要求可以正常编译运行 NexOS;
- NexOS 源码路径:假定为
/home/你的用户名/nexos。
若你使用的是其他路径,请在后续命令中自行替换为自己的 NexOS 源码路径。
若尚未完成 NexOS 的基础环境配置与编译,请先参考
实验零中的“步骤 2:实验内核运行与调试”,确保可以在本机成功运行 NexOS(能进入系统 Shell,执行ls、hello等基本命令)。
1. NexOS 用户程序结构概览¶
进入 NexOS 源码目录,可以看到一个名为 user 的子目录:
其中 user/ 目录中存放的是所有用户态应用程序的源码,例如:
init.c:系统启动后的第一个用户进程;sh.c:NexOS 的简单 Shell;hello.c:示例用户程序;ls.c/cat.c/echo.c等:类 Unix 工具程序;user.h:用户态可以调用的系统调用封装(write、printf、exit等)。
在仓库根目录的 Makefile 中,有一组与用户程序相关的变量:
UPROGS:需要参与编译、并被打包进文件系统镜像(fs.img)的用户程序列表;fsimg规则:通过tools/mkxv6fs.py将各个*.elf用户程序打包到fs.img中,供 NexOS 运行时使用。
结论:要在 NexOS 下添加一个新的应用程序,需要完成三个步骤:
- 在
user/目录下编写一个新的.c源文件; - 在根目录
Makefile的UPROGS中加入该程序的名字; - 在根目录
Makefile的fsimg规则中,将该程序打包进fs.img。
2. 创建一个打印学号的 NexOS 应用¶
下面我们以一个名为 sid 的用户程序为例,演示如何在 NexOS 中创建一个打印自己学号的应用。
假设你的学号为 PBXXXXXXXX(请替换为你自己的真实学号)。
2.1 在 user/ 目录中新建程序文件¶
- 进入 NexOS 源码目录,并切换到
user/子目录:
- 以
sid.c为例,新建一个用户程序源文件(可以使用vim、nano或 VSCode 等编辑器,这里以vim为例):
- 在
sid.c中编写如下代码(请将示例学号替换为你自己的真实学号):
#include "user.h"
int main(void) {
// TODO: 请将 PBXXXXXXXX 替换为你自己的真实学号
printf("My student ID is PBXXXXXXXX\n");
// 程序正常结束
exit(0);
}
保存并退出编辑器。
说明:
#include "user.h":包含 NexOS 用户态可用的系统调用封装;printf:会通过系统调用在控制台上打印字符串;exit(0):通知内核该进程正常结束。
此时 user/ 目录下应能看到新建的 sid.c:
2.2 在 Makefile 中注册新的用户程序¶
NexOS 使用根目录下的 Makefile 来统一管理内核和用户程序的构建流程。
要让新建的 sid.c 被编译并打包进 fs.img,需要在 Makefile 中进行两处修改。
- 回到 NexOS 源码根目录,并用编辑器打开
Makefile:
- 找到
UPROGS定义:
在这一行的末尾添加 sid,并在前一行的末尾补上\:
- 继续向下滚动,找到
fsimg规则中使用tools/mkxv6fs.py打包用户程序的部分:
fsimg: $(UELFS) tools/mkxv6fs.py
python3 tools/mkxv6fs.py --image fs.img --size-blocks 65536 \
--add init=$(UBUILD)/init.elf --add sh=$(UBUILD)/sh.elf \
... \
--add touch=$(UBUILD)/touch.elf
在最后追加一行,将 sid 程序也加入文件系统镜像(注意在前一行的末尾补上\):
fsimg: $(UELFS) tools/mkxv6fs.py
python3 tools/mkxv6fs.py --image fs.img --size-blocks 65536 \
--add init=$(UBUILD)/init.elf --add sh=$(UBUILD)/sh.elf \
... \
--add gpudemo=$(UBUILD)/gpudemo.elf \
--add sid=$(UBUILD)/sid.elf
保存并退出 Makefile。
小贴士:
- 如果不在
UPROGS中加入sid,则sid.c根本不会被编译;- 如果忘记在
fsimg规则中添加--add sid=...,则即使编译出了sid.elf,也不会被打包进fs.img,在 NexOS Shell 里将找不到sid这个命令。
2.3 重新编译 NexOS 并生成文件系统镜像¶
完成代码和 Makefile 的修改后,需要重新构建用户程序和文件系统镜像:
- 在 NexOS 源码根目录下执行:
如果此前尚未编译过内核,也可以直接执行:
- 若编译成功,会在根目录下看到更新后的
fs.img文件。
如果编译过程中出现报错,请仔细阅读报错信息,检查:
sid.c是否保存正确(如缺少#include "user.h"、;等语法错误);Makefile的UPROGS和fsimg行是否有拼写错误(如sid拼成了别的名字)。
2.4 在 NexOS 中运行你的学号程序¶
- 使用 QEMU 启动 NexOS(以默认命令为例,具体命令以本机已配置的为准):
- 等系统启动成功、出现 Shell 提示符后,输入:
- 按下回车执行后,预期输出类似:
若能看到这行信息,说明你已经在 NexOS 中成功创建并运行了一个属于自己的用户态应用程序。
你也可以尝试多运行几次、或者在程序中输出更多信息,例如:
附录¶
1.C语言程序编译流程¶
- 预处理(Preprocessing) 预处理是编译过程的第一步,主要使用预处理器来处理源代码文件。在预处理阶段,源代码中的宏定义、条件编译指令以及头文件的包含等将被展开和处理,生成经过处理的中间代码。预处理的结果是一个经过宏定义替换、头文件包含等操作后的中间代码文件。
- 编译(Compiling) 编译是将预处理之后的中间代码翻译成汇编代码的过程。编译器会将中间代码文件转化为与特定平台相关的汇编语言代码文件。在编译阶段,进行语法分析、语义分析等操作,生成与特定平台相关的汇编代码。
- 汇编(Assembling) 汇编是将汇编语言代码翻译为机器代码的过程。汇编器将汇编代码转换为目标文件,其中包含了与特定平台相关的机器指令和数据。目标文件中包含了汇编语言代码转换而成的机器码。
- 链接(Linking) 链接是将各个目标文件(包括自己编写的文件和库文件)合并为一个可执行文件的过程。链接器将目标文件中的符号解析为地址,并将它们连接到最终的可执行文件中。链接过程包括符号解析、地址重定位等步骤,确保各个模块能够正确地相互调用。
- 可执行文件(Executable) 最终的输出是一个可执行文件,其中包含了所有必要的指令和数据,可以在特定平台上运行。这个可执行文件经过编译链接过程,包含了源代码的所有功能和逻辑,可以被操作系统加载并执行。