Skip to content

Commit 7fb97c3

Browse files
committed
Add tutorial and basic example
- describe the setup of basic-os with basic-kernel in tutorial.md - use bashtestmd to create a shell script from the tutorial - run the shell script to generate the basic example - boot the basic example using qemu - add generation of basic example from tutorial to CI Signed-off-by: Pepper Gray <hello@peppergray.xyz>
1 parent def819c commit 7fb97c3

File tree

17 files changed

+2237
-0
lines changed

17 files changed

+2237
-0
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ jobs:
8585
if: runner.os == 'Linux'
8686
run: cargo test --no-default-features --features bios
8787

88+
examples:
89+
name: Build examples
90+
runs-on: ubuntu-latest
91+
timeout-minutes: 5
92+
steps:
93+
- uses: actions/checkout@v3
94+
- uses: Swatinem/rust-cache@v2
95+
- uses: r7kamura/rust-problem-matchers@v1.1.0
96+
- name: Install QEMU (Linux)
97+
run: sudo apt update && sudo apt install qemu-system-x86
98+
- name: "Install cargo-make"
99+
run: cargo install --force cargo-make
100+
- name: "Create basic example from tutorial"
101+
run: cargo make all
102+
working-directory: docs/tutorial
103+
88104
fmt:
89105
name: Check Formatting
90106
runs-on: ubuntu-latest

docs/tutorial/Makefile.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[tasks.convert]
2+
install_crate = "bashtestmd"
3+
command = "bashtestmd"
4+
args = ["--input", "tutorial.md", "--output", "tutorial.sh", "--tag", "tutorial"]
5+
6+
[tasks.rm]
7+
cwd = "../../examples"
8+
script = "rm -rf basic"
9+
10+
[tasks.create]
11+
cwd = "../../examples"
12+
script = "../docs/tutorial/tutorial.sh"
13+
14+
[tasks.all]
15+
dependencies = ["convert", "rm", "create"]

docs/tutorial/tutorial.md

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# Tutorial: create and boot a minimal OS
2+
3+
A step by step guide to setup bootloader with your kernel.
4+
5+
### 1. Create a new os crate at the top level that defines a workspace
6+
7+
```sh,tutorial
8+
$ cargo new basic --bin
9+
$ cd basic
10+
```
11+
```sh,tutorial,bashtestmd:raw
12+
$ cat >> Cargo.toml <<EOL
13+
14+
[workspace]
15+
resolver = "3"
16+
EOL
17+
```
18+
```sh,tutorial,bashtestmd:raw
19+
$ cat > basic-os.md <<EOL
20+
# basic-os
21+
22+
A minimal os to showcase the usage of bootloader.
23+
EOL
24+
```
25+
```sh,tutorial,bashtestmd:raw
26+
$ cat > .gitignore <<EOL
27+
/target/
28+
**/*.rs.bk
29+
EOL
30+
```
31+
32+
### 2. Add bootloader to the workspace
33+
34+
#### 2.1 Add a build-dependencies on the bootloader crate
35+
36+
```sh,tutorial
37+
$ cargo add --build bootloader
38+
```
39+
40+
#### 2.2 Create a [build.rs](https://doc.rust-lang.org/cargo/reference/build-scripts.html) build script
41+
42+
```sh,tutorial,bashtestmd:raw
43+
$ cat > build.rs <<EOL
44+
use std::path::PathBuf;
45+
46+
fn main() {
47+
// set by cargo, build scripts should use this directory for output files
48+
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
49+
// set by cargo's artifact dependency feature, see
50+
// https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies
51+
let kernel = PathBuf::from(std::env::var_os("CARGO_BIN_FILE_KERNEL_kernel").unwrap());
52+
53+
// create an UEFI disk image (optional)
54+
let uefi_path = out_dir.join("uefi.img");
55+
bootloader::UefiBoot::new(&kernel)
56+
.create_disk_image(&uefi_path)
57+
.unwrap();
58+
59+
// create a BIOS disk image
60+
let bios_path = out_dir.join("bios.img");
61+
bootloader::BiosBoot::new(&kernel)
62+
.create_disk_image(&bios_path)
63+
.unwrap();
64+
65+
// pass the disk image paths as env variables to the
66+
println!("cargo:rustc-env=UEFI_PATH={}", uefi_path.display());
67+
println!("cargo:rustc-env=BIOS_PATH={}", bios_path.display());
68+
}
69+
EOL
70+
```
71+
72+
#### 2.3 Set up an [artifact dependency](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) to add your kernel crate as a build-dependency:
73+
74+
Enable the unstable artifact-dependencies feature:
75+
```sh,tutorial,bashtestmd:raw
76+
$ mkdir .cargo
77+
$ cat > .cargo/config.toml <<EOF
78+
[unstable]
79+
bindeps = true
80+
EOF
81+
$ cat > rust-toolchain.toml <<EOF
82+
[toolchain]
83+
channel = "nightly"
84+
components = ["rustfmt", "clippy"]
85+
targets = ["x86_64-unknown-none"]
86+
EOF
87+
```
88+
89+
Add your kernel crate as a build-dependency:
90+
91+
```sh,tutorial,bashtestmd:raw
92+
$ sed -i '/^\[build-dependencies\]$/r /dev/stdin' Cargo.toml <<'EOF'
93+
kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" }
94+
EOF
95+
```
96+
97+
### 3. Move your full kernel code into a kernel subdirectory
98+
99+
#### 3.1 Copy your existing kernel or create a new one
100+
101+
Here we re-use `basic_boot` from default-settings test kernels to create a new kernel:
102+
103+
```sh,tutorial
104+
$ cargo new kernel --bin
105+
$ cp .gitignore kernel/
106+
$ cd kernel
107+
```
108+
```sh,tutorial,bashtestmd:raw
109+
$ cat > basic-kernel.md <<EOL
110+
# basic-kernel
111+
112+
A minimal kernel to showcase the usage of bootloader.
113+
EOL
114+
```
115+
```sh,tutorial,bashtestmd:raw
116+
$ cat > src/main.rs <<EOF
117+
#![no_std] // don't link the Rust standard library
118+
#![no_main] // disable all Rust-level entry points
119+
120+
use bootloader_api::{BootInfo, entry_point};
121+
use core::fmt::Write;
122+
123+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124+
#[repr(u32)]
125+
pub enum QemuExitCode {
126+
Success = 0x10,
127+
Failed = 0x11,
128+
}
129+
130+
pub fn exit_qemu(exit_code: QemuExitCode) -> ! {
131+
use x86_64::instructions::{nop, port::Port};
132+
133+
unsafe {
134+
let mut port = Port::new(0xf4);
135+
port.write(exit_code as u32);
136+
}
137+
138+
loop {
139+
nop();
140+
}
141+
}
142+
143+
pub fn serial() -> uart_16550::SerialPort {
144+
let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) };
145+
port.init();
146+
port
147+
}
148+
149+
entry_point!(kernel_main);
150+
151+
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
152+
writeln!(serial(), "Entered kernel with boot info: {boot_info:?}").unwrap();
153+
writeln!(serial(), "\n=(^.^)= meow\n").unwrap();
154+
exit_qemu(QemuExitCode::Success);
155+
}
156+
157+
/// This function is called on panic.
158+
#[panic_handler]
159+
#[cfg(not(test))]
160+
fn panic(info: &core::panic::PanicInfo) -> ! {
161+
let _ = writeln!(serial(), "PANIC: {info}");
162+
exit_qemu(QemuExitCode::Failed);
163+
}
164+
EOF
165+
```
166+
Add dependencies:
167+
168+
```sh,tutorial
169+
$ cargo add bootloader_api
170+
$ cargo add x86_64 --features instructions
171+
$ cargo add uart_16550
172+
```
173+
174+
Check [README.md#kernel](/README.md#kernel) how to make your kernel compatible with bootloader.
175+
176+
#### 3.2 Compile your kernel
177+
178+
Compile your kernel to an ELF executable by running cargo build --target x86_64-unknown-none. You might need to run rustup target add x86_64-unknown-none before to download precompiled versions of the core and alloc crates.
179+
180+
```sh,tutorial
181+
$ rustup target add x86_64-unknown-none
182+
```
183+
184+
```sh,tutorial,bashtestmd:raw
185+
$ mkdir .cargo
186+
$ cat > .cargo/config.toml <<EOF
187+
[build]
188+
target = "x86_64-unknown-none"
189+
EOF
190+
```
191+
```sh,tutorial
192+
$ cargo build
193+
```
194+
195+
#### 3.3 Compile the workspace
196+
197+
```sh,tutorial
198+
$ cd ..
199+
$ cargo build
200+
```
201+
202+
### 4. Do something with the bootable disk images
203+
204+
For example, run them with QEMU
205+
206+
#### 4.1 Create a qemu launcher
207+
208+
```sh,tutorial,bashtestmd:raw
209+
$ cargo add ovmf-prebuilt
210+
$ cat > src/main.rs <<EOF
211+
use ovmf_prebuilt::{Arch, FileType, Prebuilt, Source};
212+
use std::env;
213+
use std::process::{Command, exit};
214+
215+
fn main() {
216+
// read env variables that were set in build script
217+
let uefi_path = env!("UEFI_PATH");
218+
let bios_path = env!("BIOS_PATH");
219+
220+
// parse mode from CLI
221+
let args: Vec<String> = env::args().collect();
222+
let prog = &args[0];
223+
224+
// choose whether to start the UEFI or BIOS image
225+
let uefi = match args.get(1).map(|s| s.to_lowercase()) {
226+
Some(ref s) if s == "uefi" => true,
227+
Some(ref s) if s == "bios" => false,
228+
Some(ref s) if s == "-h" || s == "--help" => {
229+
println!("Usage: {prog} [uefi|bios]");
230+
println!(" uefi - boot using OVMF (UEFI)");
231+
println!(" bios - boot using legacy BIOS");
232+
exit(0);
233+
}
234+
_ => {
235+
eprintln!("Usage: {prog} [uefi|bios]");
236+
exit(1);
237+
}
238+
};
239+
240+
let mut cmd = Command::new("qemu-system-x86_64");
241+
cmd.arg("-serial").arg("mon:stdio");
242+
cmd.arg("-device")
243+
.arg("isa-debug-exit,iobase=0xf4,iosize=0x04");
244+
cmd.arg("-display").arg("none");
245+
246+
if uefi {
247+
let prebuilt =
248+
Prebuilt::fetch(Source::LATEST, "target/ovmf").expect("failed to update prebuilt");
249+
250+
let code = prebuilt.get_file(Arch::X64, FileType::Code);
251+
let vars = prebuilt.get_file(Arch::X64, FileType::Vars);
252+
253+
cmd.arg("-drive")
254+
.arg(format!("format=raw,file={uefi_path}"));
255+
cmd.arg("-drive").arg(format!(
256+
"if=pflash,format=raw,unit=0,file={},readonly=on",
257+
code.display()
258+
));
259+
cmd.arg("-drive").arg(format!(
260+
"if=pflash,format=raw,unit=1,file={},snapshot=on",
261+
vars.display()
262+
));
263+
} else {
264+
cmd.arg("-drive")
265+
.arg(format!("format=raw,file={bios_path}"));
266+
}
267+
268+
let mut child = cmd.spawn().expect("failed to start qemu-system-x86_64");
269+
let status = child.wait().expect("failed to wait on qemu");
270+
if !status.success() {
271+
exit(status.code().unwrap_or(1));
272+
}
273+
}
274+
EOF
275+
```
276+
277+
#### 4.1 Run the kernel
278+
279+
Check for return code 33 (0x10) for success:
280+
281+
```sh,tutorial
282+
$ cargo run -- bios || [ $? -eq 33 ]
283+
```
284+
```sh,tutorial
285+
$ cargo run -- uefi || [ $? -eq 33 ]
286+
```
287+
288+
## Generate basic example
289+
290+
The [basic example](/examples/basic) is generated from this tutorial.
291+
292+
Convert `tutorial.md` to `tutorial.sh`:
293+
```sh
294+
$ cargo make convert
295+
```
296+
Create [basic example](/examples/basic) from `tutorial.sh`:
297+
```sh
298+
$ cargo make create
299+
```
300+
301+
The tutorial can be used as a boilerplate for your project, by calling `tutorial.sh` from an arbitrary directory.

0 commit comments

Comments
 (0)