合并内核与应用程序

到目前为止我们的 OS 还没有文件系统,所以我们只需将最终得到的应用程序可执行文件直接链接到内核中,合并在一起,形成一个 image,这样让 bootloader 一开始就把内核和应用程序一并加载到内存中。

这里的实现有一些技巧,我们先写一个编译脚本 build.rs。注意是直接放在项目文件夹 os 中,而不是源码文件夹 src

  1. // build.rs
  2. use std::fs::File;
  3. use std::io::{Result, Write};
  4. fn main() {
  5. println!("cargo:rerun-if-env-changed=USER_IMG");
  6. if let Ok(user_img) = std::env::var("USER_IMG") {
  7. println!("cargo:rerun-if-changed={}", user_img);
  8. }
  9. gen_link_user_asm().unwrap();
  10. }
  11. /// Generate assembly file for linking user image
  12. fn gen_link_user_asm() -> Result<()> {
  13. let mut f = File::create("src/link_user.S").unwrap();
  14. let user_img = std::env::var("USER_IMG").unwrap();
  15. writeln!(f, "# generated by build.rs - do not edit")?;
  16. writeln!(f, r#"
  17. .section .data
  18. .global _user_img_start
  19. .global _user_img_end
  20. _user_img_start:
  21. .incbin "{}"
  22. _user_img_end:
  23. "#, user_img)?;
  24. Ok(())
  25. }

然后在 init.rs 中加入:

  1. // init.rs
  2. global_asm!(include_str!("link_user.S"));

这段编译脚本会在每次编译的最开始运行。它的作用是生成一段汇编代码,将用户程序可执行文件原封不动地链接到内核的

合并内核与应用程序 - 图1

段中。这段汇编被生成到 src/link_user.S 文件中,然后我们在 init.rs 里把它导入进来。此后可以在其它地方通过 _user_img_start_user_img_end 这两个符号得知它所在的虚拟地址。

我们用一个环境变量 USER_IMG 记录用户程序可执行文件的路径,编译脚本在执行时,会将这个字符串填入生成的汇编中。所以我们只需在编译之前利用 export 修改环境变量 USER_IMG 为我们最终得到的可执行文件的路径即可。

最后让我们关注一开始的两条奇怪语句:

  1. println!("cargo:rerun-if-env-changed=USER_IMG");
  2. println!("cargo:rerun-if-changed={}", user_img);

这是编译脚本发送给构建工具 cargo 的特殊指令,含义是:当检测到环境变量 USER_IMG 或者它所指向的文件发生变化时,就强制重新编译。并且每次编译时,都会生成一个新的 link_user.S 文件。

这波操作要解决的问题是:由于编译器具有自动增量构建的特性,会导致当用户镜像发生变化时,编译器无法自动感知到,最后链接的还是以前的版本,使得我们不得不手动 cargo clean 清理干净中间产物后重新编译。

现在,我们每次更新并编译生成用户程序执行文件后,都可以放心地直接 make run 了!