构建指南:blog_os 内核开发第一步)
文档教程技术博客操作系统【免费下载链接】blog_osWriting an OS in Rust项目地址https://gitcode.com/GitHub_Trending/bl/blog_os点击查看免费下载本教程是 blog_osWriting an OS in Rust系列教程第二版的第一篇。创建操作系统内核的第一步是构建一个不链接 Rust 标准库、能够在无操作系统bare metal环境下运行的 Rust 可执行文件——即freestanding binary。本文完整讲解从cargo new到最终编译成功的每一步no_std、panic_handler、eh_personality、no_main、自定义_start入口点以及两种绕过链接器错误的方法并给出可直接复制运行的最小示例。为什么内核需要 Freestanding Binary要编写操作系统内核就必须写出不依赖任何操作系统特性的代码。这意味着我们无法使用线程、文件、堆内存、网络、随机数、标准输出等一切依赖操作系统抽象或特定硬件的功能——因为我们正在编写自己的操作系统和驱动这是理所当然的前提。相应地我们无法使用 Rust 标准库standard library的大部分内容。但 Rust 语言本身仍然提供了大量可用的特性例如迭代器iterators闭包closures模式匹配pattern matchingOption/Result字符串格式化string formatting所有权系统ownership system这些特性让我们能够以表达力强、高层级的代码风格编写内核同时不必担心未定义行为undefined behavior和内存安全memory safety问题。要达成这一目标我们需要一个不依赖底层操作系统即可运行的可执行文件它通常被称为freestanding 可执行文件或bare-metal 可执行文件。本文将以多个步骤讲解如何构建它并解释每一步背后的原因。如果你只想要最小示例代码可以直接跳到文末的总结。第一步禁用 Rust 标准库默认情况下所有 Rust crate 都会链接[标准库]它依赖操作系统提供线程、文件、网络等功能同时依赖与操作系统服务密切交互的 C 标准库libc。既然我们的目标是编写操作系统就不能使用任何依赖 OS 的库因此需要通过 [no_std属性][no_std attribute] 禁用标准库的自动链接。创建 Cargo 项目首先通过命令行创建一个新的 cargo 应用项目cargo new blog_os --bin --edition 2018项目名可以叫blog_os也可以任意取名--bin参数告诉 cargo 我们要创建的是可执行文件与库相对--edition 2018参数指定使用 Rust 2018 版次。执行后cargo 会生成如下目录结构blog_os ├── Cargo.toml └── src └── main.rsCargo.toml保存 crate 的配置如 crate 名称、作者、语义化版本号、依赖列表等src/main.rs包含 crate 的根模块和main函数通过cargo build编译后可执行文件blog_os会生成在target/debug子目录中。添加no_std属性当前 crate 隐式链接了标准库。我们在main.rs顶部加入#![no_std]来禁用// main.rs #![no_std] fn main() { println!(Hello, world!); }再次执行cargo build会出现如下错误error: cannot find macro println! in this scope -- src/main.rs:4:5 | 4 | println!(Hello, world!); | ^^^^^^^原因在于println!宏属于标准库而我们已不再链接它。这也符合直觉println向[标准输出]操作系统提供的特殊文件描述符写入数据没有操作系统就无法工作。删掉打印语句用空的main函数再试一次// main.rs #![no_std] fn main() {} cargo build error: #[panic_handler] function required, but not found error: language item required, but not found: eh_personality现在编译器抱怨缺少两样东西一个#[panic_handler]函数以及一个名为eh_personality的 language item。我们逐一解决。第二步实现 Panic Handler#[panic_handler]属性定义的是当程序发生[恐慌panic]时编译器应当调用的函数。标准库自带恐慌处理函数但在no_std环境中我们必须自己定义// in main.rs use core::panic::PanicInfo; /// 发生 panic 时此函数被调用。 #[panic_handler] fn panic(_info: PanicInfo) - ! { loop {} }[PanicInfo] 参数携带恐慌发生的文件名、行号以及可选的恐慌消息该函数绝不能返回因此返回类型是never类型!向编译器声明这是一个发散函数diverging function现阶段我们在这个函数里无事可做所以放入一个无限循环确保函数永不返回。第三步处理eh_personalityLanguage ItemLanguage item 是编译器内部需要的特殊函数和类型。例如 [Copy] trait 就是一个 language item它告诉编译器哪些类型具有复制语义在它的[实现代码]中可以看到特殊的#[lang copy]属性声明。理论上可以自定义 language item 的实现但应作为最后手段原因有二language item 是高度不稳定的实现细节编译器甚至不对其做类型检查不会检查函数参数类型是否正确。幸运的是有一个更稳定的方法来解决上述eh_personality错误。eh_personalitylanguage item 标记用于实现[栈展开stack unwinding]的函数。默认情况下Rust 在发生 panic 时通过栈展开来运行所有存活栈变量的析构函数从而释放所有内存并允许父线程捕获 panic 后继续执行。但栈展开是一个复杂过程需要操作系统特定的库如 Linux 上的libunwind、Windows 上的结构化异常处理 SEH这些正是我们不想为操作系统引入的东西。在 Cargo.toml 中禁用栈展开Rust 提供了panic 时直接 abort的选项它禁用了展开符号信息的生成还能显著减小二进制体积。最简单的设置方式是在Cargo.toml中加入[profile.dev] panic abort [profile.release] panic abort这样devprofilecargo build使用和releaseprofilecargo build --release使用都在 panic 时直接终止程序。现在编译器不再需要eh_personalitylanguage item。解决了上述两个错误之后再次编译会碰到新错误 cargo build error: requires start lang_item我们的程序缺少定义入口点的startlanguage item。第四步用no_main覆盖入口点startlanguage item 与运行时许多人以为main函数是程序运行时第一个被调用的函数。实际上大多数语言都有[运行时系统]runtime system负责诸如垃圾回收如 Java、软件线程如 Go 的 goroutine等工作。运行时需要在main之前初始化自身因此必须先于main被调用。在链接标准库的典型 Rust 二进制中执行从 C 运行时库crt0C runtime zero开始。crt0负责搭建 C 程序的环境创建栈、把参数放入正确的寄存器。随后 C 运行时调用由startlanguage item 标记的 Rust 运行时入口rt::lang_start。Rust 只拥有极简运行时负责设置栈溢出保护、在 panic 时打印回溯信息等小事最后才调用main函数。我们的 freestanding 可执行文件无法访问 Rust 运行时和crt0因此必须自己定义入口点。仅实现startlanguage item 没有用——它仍然依赖crt0的调用。正确做法是直接覆盖crt0的入口点。编写_start函数要告诉 Rust 编译器我们不使用正常的入口点调用链需要添加#![no_main]属性#![no_std] #![no_main] use core::panic::PanicInfo; /// 发生 panic 时此函数被调用。 #[panic_handler] fn panic(_info: PanicInfo) - ! { loop {} }注意main函数已经消失了——没有运行时去调用它main便失去意义。我们改为用自定义的_start函数覆盖操作系统入口点#[no_mangle] pub extern C fn _start() - ! { loop {} }#[no_mangle]禁用[名称修饰name mangling]确保编译器输出名为_start的函数。否则编译器会生成类似_ZN3blog_os4_start7hb173fedf945531caE的加密符号来保证函数名唯一。该属性是必须的因为我们下一步需要把入口点函数名精确告诉链接器。extern C标记让编译器为此函数使用 C 调用约定而非未指定的 Rust 调用约定。把函数命名为_start是因为它是大多数系统默认的入口点名称。!返回类型表示这是一个发散函数绝不允许返回。这是必需的入口点不被任何函数调用而是由操作系统或引导程序直接调用。因此它不能返回而应调用操作系统的exit系统调用等方式终止。对我们来说freestanding 二进制返回后无事可做关闭机器是合理的动作。现在先用无限循环满足!类型的要求。再次运行cargo build就会遭遇难缠的链接器错误。第五步解决链接器错误链接器是把编译器生成的代码组合成可执行文件的程序。由于 Linux、Windows、macOS 的可执行文件格式各不相同每个系统都有自己的链接器报出的错误信息也不同但根本原因一致链接器的默认配置假定我们的程序依赖 C 运行时而我们的 crate 并不依赖它。要解决错误必须告诉链接器不要链接 C 运行时。有两种方法为目标 bare metal 系统编译或者向链接器传递特定参数。方法一为 Bare Metal 目标编译推荐默认情况下Rust 尝试构建能在当前系统环境中运行的可执行文件。例如x86_64Windows 用户会得到使用x86_64指令集的.exe可执行文件。这个环境被称为host系统。为了描述不同环境Rust 使用名为target triple的字符串。运行rustc --version --verbose可以查看当前 host 系统的 target triplerustc 1.35.0-nightly (474e7a648 2019-04-07) binary: rustc commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab commit-date: 2019-04-07 host: x86_64-unknown-linux-gnu release: 1.35.0-nightly LLVM version: 8.0以上输出来自x86_64Linux 系统。host triple 为x86_64-unknown-linux-gnu其中包含 CPU 架构x86_64、硬件厂商unknown、操作系统linux以及 ABIgnu。按 host triple 编译时Rust 编译器和链接器会假定存在 Linux、Windows 这样的底层操作系统并假定其默认使用 C 运行时这正是链接器错误的原因。因此我们可以为一个没有底层操作系统的环境编译从而避开错误。一个 bare metal 环境的例子是thumbv7em-none-eabihftarget triple它描述[嵌入式][embedded][ARM]系统。细节并不重要关键点在于 triple 中的none表示目标系统没有底层操作系统。要编译该目标需先用 rustup 添加它rustup target add thumbv7em-none-eabihf该命令会为目标系统下载对应的标准库及 core 库。现在可以为该目标构建 freestanding 可执行文件cargo build --target thumbv7em-none-eabihf通过--target参数我们告诉 cargo 正在为 bare metal 目标系统[交叉编译]。由于目标系统没有操作系统链接器不会尝试链接 C 运行时构建即可成功。本系列教程构建内核正是采用这一方法——不过后续文章会用描述x86_64bare metal 环境的[自定义 target]来取代thumbv7em-none-eabihf具体细节将在下一篇中说明。方法二传递链接器参数可选除了为 bare metal 目标编译也可以向链接器传入特定参数来消除错误。这不是本系列构建内核所采用的方法本节仅供完整性和进阶学习参考。LinuxLinux 上出现的链接器错误如下已省略部分内容error: linking with cc failed: exit code: 1 | note: cc […] note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function _start: (.text0x12): undefined reference to __libc_csu_fini /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function _start: (.text0x19): undefined reference to __libc_csu_init /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function _start: (.text0x25): undefined reference to __libc_start_main collect2: error: ld returned 1 exit status问题在于链接器默认链接 C 运行时的启动例程也叫_start它需要 C 标准库libc的若干符号而由于no_std我们并没有包含libc导致引用无法解析。解决办法是向链接器传递-nostartfiles标志让它不要链接 C 启动例程。通过 cargo 传递链接器参数的一种方式是用cargo rustc命令——它与cargo build行为一致但允许向底层的rustc传递选项。rustc的-C link-arg标志可向链接器传参。组合后的新构建命令如下cargo rustc -- -C link-arg-nostartfiles现在 crate 在 Linux 上就能作为 freestanding 可执行文件构建成功了。我们无需显式指定入口点函数名因为链接器默认查找名为_start的函数。WindowsWindows 上会遇到不同的链接器错误已省略部分内容error: linking with link.exe failed: exit code: 1561 | note: C:\\Program Files (x86)\\…\\link.exe […] note: LINK : fatal error LNK1561: entry point must be definedentry point must be defined 表示链接器找不到入口点。在 Windows 上默认入口点名称[取决于使用的子系统]CONSOLE子系统查找名为mainCRTStartup的函数WINDOWS子系统查找WinMainCRTStartup。要覆盖默认值、让链接器改找我们的_start函数需传递/ENTRY参数cargo rustc -- -C link-arg/ENTRY:_start从参数格式的不同就能看出Windows 链接器与 Linux 链接器完全是两个程序。随后会出现另一个链接器错误error: linking with link.exe failed: exit code: 1221 | note: C:\\Program Files (x86)\\…\\link.exe […] note: LINK : fatal error LNK1221: a subsystem cant be inferred and must be defined该错误源于 Windows 可执行文件可以使用多种[子系统]。普通程序会根据入口点名称推断子系统入口点叫main时使用CONSOLE子系统叫WinMain时使用WINDOWS子系统。由于我们的_start名称特殊必须显式指定子系统cargo rustc -- -C link-args/ENTRY:_start /SUBSYSTEM:console这里使用CONSOLE子系统WINDOWS子系统同样可行。注意这里用的是-C link-args带 s可以接收空格分隔的多个参数而-C link-arg只能传单个参数。执行后即可在 Windows 上成功构建。macOSmacOS 上的链接器错误如下已省略部分内容error: linking with cc failed: exit code: 1 | note: cc […] note: ld: entry point (_main) undefined. for architecture x86_64 clang: error: linker command failed with exit code 1 […]错误信息表明链接器找不到默认名为main的入口点函数在 macOS 上所有函数名前面都带有_前缀。要把入口点设为我们的_start传递-e链接器参数cargo rustc -- -C link-args-e __start-e标志指定入口点函数名。由于 macOS 所有函数名都有额外_前缀因此入口点要写__start而非_start。随后出现第二个链接器错误error: linking with cc failed: exit code: 1 | note: cc […] note: ld: dynamic main executables must link with libSystem.dylib for architecture x86_64 clang: error: linker command failed with exit code 1 […]macOS 不正式支持静态链接的二进制默认要求程序链接libSystem库。要覆盖这一默认并生成静态二进制需传递-static标志cargo rustc -- -C link-args-e __start -static这仍然不够还会出现第三个错误error: linking with cc failed: exit code: 1 | note: cc […] note: ld: library not found for -lcrt0.o clang: error: linker command failed with exit code 1 […]原因是在 macOS 上程序默认链接crt0C runtime zero这与 Linux 上的错误类似同样可以通过添加-nostartfiles链接器参数解决cargo rustc -- -C link-args-e __start -static -nostartfiles至此程序可以在 macOS 上成功构建了。统一各平台构建命令不同 host 平台需要不同构建命令并不理想。可以创建.cargo/config.toml文件把平台相关的参数统一进去# in .cargo/config.toml [target.cfg(target_os linux)] rustflags [-C, link-arg-nostartfiles] [target.cfg(target_os windows)] rustflags [-C, link-args/ENTRY:_start /SUBSYSTEM:console] [target.cfg(target_os macos)] rustflags [-C, link-args-e __start -static -nostartfiles]rustflags键里的参数会在每次rustc调用时自动附加。现在只需一条cargo build命令就能在三个平台上构建程序了。应该这么做吗虽然为 Linux、Windows 或 macOS 构建 freestanding 可执行文件是可行的但这不是好主意。因为我们的可执行文件仍假定很多事情例如_start函数被调用时栈已经初始化。缺少 C 运行时某些前提可能无法满足程序可能因段错误等原因无法正常运行。如果只是想构建一个运行在现有操作系统之上的最小二进制更合理的做法是链接libc并使用#[start]属性详见 Rust 官方 no-stdlib 文档。总结最小 Freestanding Rust Binary一个最小的 freestanding Rust 可执行文件代码如下src/main.rs:#![no_std] // 不链接 Rust 标准库 #![no_main] // 禁用所有 Rust 层级的入口点 use core::panic::PanicInfo; #[no_mangle] // 不要对函数名做名称修饰 pub extern C fn _start() - ! { // 此函数是入口点因为链接器默认查找名为 _start 的函数 loop {} } /// 发生 panic 时此函数被调用。 #[panic_handler] fn panic(_info: PanicInfo) - ! { loop {} }Cargo.toml:[package] name crate_name version 0.1.0 authors [Author Name authorexample.com] # cargo build 使用的构建配置 [profile.dev] panic abort # panic 时禁用栈展开直接终止程序 # cargo build --release 使用的构建配置 [profile.release] panic abort # panic 时禁用栈展开直接终止程序要构建这个二进制需要为thumbv7em-none-eabihf这样的 bare metal 目标编译cargo build --target thumbv7em-none-eabihf或者通过向链接器传递额外参数为 host 系统编译# Linux cargo rustc -- -C link-arg-nostartfiles # Windows cargo rustc -- -C link-args/ENTRY:_start /SUBSYSTEM:console # macOS cargo rustc -- -C link-args-e __start -static -nostartfiles⚠️注意这只是 freestanding Rust 二进制的最小示例。该二进制假定诸多前提例如_start被调用时栈已初始化。要让它实际完成任何有用工作还需要编写更多代码。下一篇预告下一篇教程 A Minimal Rust Kernel 将讲解把 freestanding 二进制变成最小操作系统内核所需的步骤包括设置自定义 target、将可执行文件与引导程序bootloader结合、学习如何向屏幕输出消息。完整代码分别存放在post-01、post-02等分支中本仓库根目录的 README.md 对系列文章与对应分支有完整索引。赞分享文档教程技术博客操作系统【免费下载链接】blog_osWriting an OS in Rust项目地址https://gitcode.com/GitHub_Trending/bl/blog_os点击查看免费下载相关推荐MXNet 发布流水线实战基于 Jenkins 的 Scala 构件自动化构建、测试与部署全解析MXNet 发布流水线实战基于 Jenkins 的 Scala 构件自动化构建、测试与部署全解析 导读 本文围绕 MXNet 仓库中的 ci/publish文档教程技术博客操作系统Rustup与静态链接构建独立的Rust可执行文件Rustup与静态链接构建独立的Rust可执行文件 痛点与解决方案 你是否曾遇到过Rust程序在不同Linux发行版间移植时的动态链接依赖问题当你在开发环境开发工具探索 luastatic构建独立的Lua可执行文件探索 luastatic 构建独立的Lua可执行文件 在开源技术领域 luastatic 是一个强大的命令行工具它能够将Lua程序编译成独立的可执行文件。上一篇uni-app UTS 内置对象 ArrayBuffer 完全指南跨端二进制数据处理与内存共享实战下一篇CANN Runtime 错误码 EH0005 深度解析AIPP 参数非法Invalid Argument的定位与处理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考