Skip to content

Commit 6dbe80d

Browse files
committed
test: add integration test for multiboot2-lib
1 parent 678bcc1 commit 6dbe80d

25 files changed

+781
-2
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ indent_size = 4
1111
trim_trailing_whitespace = true
1212
max_line_length = 80
1313

14-
[*.yml]
14+
[{*.nix, *.yml}]
1515
indent_size = 2

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ members = [
33
"multiboot2",
44
"multiboot2-header",
55
]
6+
exclude = [
7+
"integration-test"
8+
]

integration-test/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Integrationtests
2+
3+
The `multiboot2`-directory contains an integration test that boots GRUB via legacy
4+
boot in a QEMU VM, which chainloads a multiboot2 integration test binary written
5+
in Rust.
6+
7+
The `multiboot2-header`-directory contains an integration test that boots a
8+
bootloader using the `multiboot2-header` crate via multiboot1 which generates a
9+
multiboot information structure and chainloads the integrationtest from the
10+
`multiboot2`-directory.
11+
12+
Both tests write output to the QEMU debugcon device and the output is matched
13+
against an expected test file.
14+
15+
The easiest way to set up the necessary toolchain is to install Nix and
16+
execute:
17+
- todo
18+
-t odo
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.vol
2+
grub_boot.img
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
3+
# This script builds a bootable image. It bundles the test binary into a GRUB
4+
# installation. The GRUB installation is configured to chainload the binary
5+
# via Multiboot2.
6+
7+
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
8+
set -euo pipefail
9+
IFS=$'\n\t'
10+
11+
DIR=$(dirname "$(realpath "$0")")
12+
cd "$DIR" || exit
13+
14+
MULTIBOOT2_PAYLOAD_DIR="multiboot2_payload"
15+
MULTIBOOT2_PAYLOAD_PATH="$MULTIBOOT2_PAYLOAD_DIR/target/x86-unknown-none/release/multiboot2_payload"
16+
17+
cd "$MULTIBOOT2_PAYLOAD_DIR"
18+
cargo build --release
19+
cd ..
20+
21+
echo "Verifying that the binary is a multiboot2 binary..."
22+
grub-file --is-x86-multiboot2 "$MULTIBOOT2_PAYLOAD_PATH"
23+
24+
# Delete previous state.
25+
rm -rf .vol
26+
27+
mkdir -p .vol/boot/grub
28+
cp grub.cfg .vol/boot/grub
29+
cp "$MULTIBOOT2_PAYLOAD_PATH" .vol
30+
31+
# Create a GRUB image with the files in ".vol" being embedded.
32+
grub-mkrescue -o "grub_boot.img" ".vol" 2>/dev/null

integration-test/multiboot2/grub.cfg

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# GRUB 2 configuration that boots the integration test binary via Multiboot2.
2+
3+
set timeout=0
4+
set default=0
5+
# set debug=all
6+
7+
menuentry "Integration Test" {
8+
# The leading slash is very important.
9+
multiboot2 /multiboot2_payload some commandline arguments
10+
# Pass some module + command line.
11+
module2 /boot/grub/grub.cfg grub-config
12+
boot
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[unstable]
2+
build-std = ["core", "compiler_builtins", "alloc"]
3+
build-std-features = ["compiler-builtins-mem"]
4+
5+
[build]
6+
target = "x86-unknown-none.json"
7+
rustflags = [
8+
"-C", "code-model=kernel",
9+
"-C", "link-arg=-Tlink.ld",
10+
"-C", "relocation-model=static",
11+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "multiboot2_payload"
3+
description = "Multiboot2 integration test"
4+
version = "0.1.0"
5+
edition = "2021"
6+
publish = false
7+
8+
[profile.release]
9+
codegen-units = 1
10+
lto = true
11+
12+
[dependencies]
13+
anyhow = { version = "1.0.69", default-features = false }
14+
good_memory_allocator = "0.1.7"
15+
log = { version = "0.4.17", default-features = false }
16+
multiboot2 = { path = "../../../multiboot2", features = ["unstable"] }
17+
# qemu-exit = "3.0.1"
18+
qemu-exit = { path = "/home/pschuster/dev/other/qemu-exit" }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("cargo:rerun-if-changed=link.ld");
3+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
ENTRY(start)
2+
3+
PHDRS
4+
{
5+
/* PT_LOAD FLAGS (page table permissions) not necessary, as we perform
6+
legacy boot. Linker probably sets sensible defaults anyway. */
7+
kernel_rx PT_LOAD;
8+
kernel_rw PT_LOAD;
9+
kernel_ro PT_LOAD;
10+
}
11+
12+
SECTIONS {
13+
.text 8M : AT(8M) ALIGN(4K)
14+
{
15+
*(.multiboot2_header)
16+
*(.text .text.*)
17+
} : kernel_rx
18+
19+
.rodata :
20+
{
21+
*(.rodata .rodata.*)
22+
} : kernel_ro
23+
24+
.data :
25+
{
26+
*(.data .data.*)
27+
} : kernel_rw
28+
29+
.bss :
30+
{
31+
*(COMMON)
32+
*(.bss .bss.*)
33+
} : kernel_rw
34+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use good_memory_allocator::SpinLockedAllocator;
2+
3+
#[repr(align(0x4000))]
4+
struct Align16K<T>(T);
5+
6+
/// 16 KiB naturally aligned backing storage for heap.
7+
static mut HEAP: Align16K<[u8; 0x4000]> = Align16K([0; 0x4000]);
8+
9+
#[global_allocator]
10+
static ALLOCATOR: SpinLockedAllocator = SpinLockedAllocator::empty();
11+
12+
/// Initializes the allocator. Call only once.
13+
pub fn init() {
14+
unsafe {
15+
ALLOCATOR.init(HEAP.0.as_ptr().cast::<usize>() as _, HEAP.0.len());
16+
}
17+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//! Driver for QEMU's debugcon device.
2+
3+
use core::fmt::{Arguments, Write};
4+
use log::{LevelFilter, Log, Metadata, Record};
5+
6+
static LOGGER: DebugconLogger = DebugconLogger;
7+
8+
struct Debugcon;
9+
10+
/// Internal API for the `println!` macro.
11+
pub fn _print(args: Arguments) {
12+
Debugcon.write_fmt(args).unwrap();
13+
}
14+
15+
impl Debugcon {
16+
/// I/O port of QEMUs debugcon device on x86.
17+
const IO_PORT: u16 = 0xe9;
18+
19+
pub fn write_byte(byte: u8) {
20+
unsafe {
21+
core::arch::asm!(
22+
"outb %al, %dx",
23+
in("al") byte,
24+
in("dx") Self::IO_PORT,
25+
options(att_syntax, nomem, nostack, preserves_flags)
26+
)
27+
}
28+
}
29+
}
30+
31+
impl Write for Debugcon {
32+
fn write_str(&mut self, s: &str) -> core::fmt::Result {
33+
for &byte in s.as_bytes() {
34+
Debugcon::write_byte(byte);
35+
}
36+
Ok(())
37+
}
38+
}
39+
40+
pub struct DebugconLogger;
41+
42+
impl DebugconLogger {
43+
pub fn init() {
44+
// Ignore, as we can't do anything about it here.
45+
let _ = log::set_logger(&LOGGER);
46+
log::set_max_level(LevelFilter::Trace);
47+
}
48+
}
49+
50+
impl Log for DebugconLogger {
51+
fn enabled(&self, _metadata: &Metadata) -> bool {
52+
true
53+
}
54+
55+
fn log(&self, record: &Record) {
56+
// Ignore result as we can't do anything about it.
57+
let _ = writeln!(
58+
Debugcon,
59+
"[{:>5}: {}@{}]: {}",
60+
record.level(),
61+
record.file().unwrap_or("<unknown>"),
62+
record.line().unwrap_or(0),
63+
record.args()
64+
);
65+
}
66+
67+
fn flush(&self) {}
68+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use crate::println;
2+
use alloc::format;
3+
use alloc::vec::Vec;
4+
use multiboot2::BootInformation;
5+
6+
pub fn run(mbi: &BootInformation) -> anyhow::Result<()> {
7+
basic_sanity_checks(mbi)?;
8+
print_mbi(mbi)?;
9+
print_memory_map(mbi)?;
10+
print_module_info(mbi)?;
11+
print_elf_info(mbi)?;
12+
Ok(())
13+
}
14+
15+
fn basic_sanity_checks(mbi: &BootInformation) -> anyhow::Result<()> {
16+
// Some basic sanity checks
17+
let bootloader_name = mbi
18+
.boot_loader_name_tag()
19+
.ok_or("No bootloader tag")
20+
.map_err(anyhow::Error::msg)?
21+
.name()
22+
.map_err(anyhow::Error::msg)?;
23+
let cmdline = mbi
24+
.command_line_tag()
25+
.ok_or("No cmdline tag")
26+
.map_err(anyhow::Error::msg)?
27+
.command_line()
28+
.map_err(anyhow::Error::msg)?;
29+
assert_eq!(bootloader_name, "GRUB 2.06");
30+
assert_eq!(cmdline, "some commandline arguments");
31+
32+
Ok(())
33+
}
34+
35+
fn print_mbi(mbi: &BootInformation) -> anyhow::Result<()> {
36+
println!("{mbi:#?}");
37+
println!();
38+
Ok(())
39+
}
40+
41+
fn print_memory_map(mbi: &BootInformation) -> anyhow::Result<()> {
42+
let memmap = mbi
43+
.memory_map_tag()
44+
.ok_or("Should have memory map")
45+
.map_err(anyhow::Error::msg)?;
46+
println!("Memory Map:");
47+
memmap.memory_areas().for_each(|e| {
48+
println!(
49+
" 0x{:010x} - 0x{:010x} ({:.3} MiB {})",
50+
e.start_address(),
51+
e.end_address(),
52+
e.size() as f32 / 1024.0 / 1024.0,
53+
e.typ()
54+
);
55+
});
56+
println!();
57+
Ok(())
58+
}
59+
60+
fn print_elf_info(mbi: &BootInformation) -> anyhow::Result<()> {
61+
let sections_tag = mbi
62+
.elf_sections_tag()
63+
.ok_or("Should have elf sections")
64+
.map_err(anyhow::Error::msg)?;
65+
println!("ELF sections:");
66+
for s in sections_tag.sections() {
67+
let typ = format!("{:?}", s.section_type());
68+
let flags = format!("{:?}", s.flags());
69+
let name = s.name().map_err(anyhow::Error::msg)?;
70+
println!(
71+
" {:<13} {:<17} {:<22} 0x{:010x} 0x{:010x} {:>5.2} MiB align={}",
72+
name,
73+
typ,
74+
flags,
75+
s.start_address(),
76+
s.end_address(),
77+
s.size() as f32 / 1024.0,
78+
s.addralign(),
79+
);
80+
}
81+
println!();
82+
Ok(())
83+
}
84+
85+
fn print_module_info(mbi: &BootInformation) -> anyhow::Result<()> {
86+
let modules = mbi.module_tags().collect::<Vec<_>>();
87+
if modules.len() != 1 {
88+
Err(anyhow::Error::msg("Should have exactly one boot module"))?
89+
}
90+
let module = modules.first().unwrap();
91+
let module_cmdline = module.cmdline().map_err(anyhow::Error::msg)?;
92+
println!("Modules:");
93+
println!(
94+
" 0x{:010x} - 0x{:010x} ({} B, cmdline='{}')",
95+
module.start_address(),
96+
module.end_address(),
97+
module.module_size(),
98+
module_cmdline
99+
);
100+
println!(" grub cfg passed as boot module:");
101+
let grup_cfg_ptr = module.start_address() as *const u32 as *const u8;
102+
let grub_cfg = unsafe {
103+
core::slice::from_raw_parts(
104+
grup_cfg_ptr,
105+
module.module_size() as usize,
106+
)
107+
};
108+
let grub_cfg = core::str::from_utf8(grub_cfg).map_err(anyhow::Error::msg)?;
109+
println!("=== file begin ===");
110+
println!("{grub_cfg}");
111+
println!("=== file end ===");
112+
println!();
113+
Ok(())
114+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#[macro_export]
2+
macro_rules! println {
3+
() => {
4+
$crate::println!("")
5+
};
6+
($($arg:tt)*) => {
7+
$crate::debugcon::_print(format_args!($($arg)*));
8+
$crate::debugcon::_print(format_args!("\n"));
9+
};
10+
}

0 commit comments

Comments
 (0)