mirror of
https://github.com/asterinas/asterinas.git
synced 2025-06-08 21:06:48 +00:00
287 lines
9.2 KiB
Rust
287 lines
9.2 KiB
Rust
//! jinux-runner is the Jinux runner script to ease the pain of running
|
|
//! and testing Jinux inside a QEMU VM. It should be built and run as the
|
|
//! cargo runner: https://doc.rust-lang.org/cargo/reference/config.html
|
|
//!
|
|
//! The runner will generate the filesystem image for starting Jinux. If
|
|
//! we should use the runner in the default mode, which invokes QEMU with
|
|
//! a GRUB boot device image, the runner would be responsible for generating
|
|
//! the appropriate kernel image and the boot device image. It also supports
|
|
//! to directly boot the kernel image without GRUB using the QEMU microvm
|
|
//! machine type.
|
|
//!
|
|
|
|
pub mod machine;
|
|
|
|
use std::{
|
|
fs::OpenOptions,
|
|
io::Write,
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
};
|
|
|
|
use clap::{Parser, ValueEnum};
|
|
|
|
use crate::machine::{microvm, qemu_grub_efi};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
|
|
enum BootMethod {
|
|
QemuGrub,
|
|
Microvm,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
|
|
pub enum BootProtocol {
|
|
Multiboot,
|
|
Multiboot2,
|
|
Linux,
|
|
}
|
|
/// The CLI of this runner.
|
|
#[derive(Parser, Debug)]
|
|
#[command(author, version, about, long_about = None)]
|
|
struct Args {
|
|
// Positional arguments.
|
|
/// The Jinux binary path.
|
|
path: PathBuf,
|
|
|
|
/// Provide the kernel commandline, which specifies
|
|
/// the init process.
|
|
kcmdline: String,
|
|
|
|
// Optional arguments.
|
|
/// Boot method. Can be one of the following items:
|
|
/// - `qemu-grub`;
|
|
/// - `microvm`.
|
|
#[arg(long, value_enum, default_value_t = BootMethod::QemuGrub)]
|
|
boot_method: BootMethod,
|
|
|
|
/// Boot protocol. Can be one of the following items:
|
|
/// - `multiboot`;
|
|
/// - `multiboot2`;
|
|
/// - `linux`.
|
|
#[arg(long, value_enum, default_value_t = BootProtocol::Multiboot2)]
|
|
boot_protocol: BootProtocol,
|
|
|
|
/// Enable KVM when running QEMU.
|
|
#[arg(long, default_value_t = false)]
|
|
enable_kvm: bool,
|
|
|
|
/// Emulate Intel IOMMU by QEMU.
|
|
#[arg(long, default_value_t = false)]
|
|
emulate_iommu: bool,
|
|
|
|
/// Run QEMU as a GDB server.
|
|
#[arg(long, default_value_t = false)]
|
|
halt_for_gdb: bool,
|
|
|
|
/// Boot without displaying the GRUB menu.
|
|
#[arg(long, default_value_t = false)]
|
|
skip_grub_menu: bool,
|
|
|
|
/// Run a GDB client instead of running the kernel.
|
|
#[arg(long, default_value_t = false)]
|
|
run_gdb_client: bool,
|
|
}
|
|
|
|
pub const COMMON_ARGS: &[&str] = &[
|
|
"--no-reboot",
|
|
"-cpu",
|
|
"Icelake-Server,+x2apic",
|
|
"-m",
|
|
"2G",
|
|
"-nographic", // TODO: figure out why grub can't shown up without it
|
|
"-monitor",
|
|
"vc",
|
|
"-serial",
|
|
"mon:stdio",
|
|
"-display",
|
|
"none",
|
|
"-device",
|
|
"isa-debug-exit,iobase=0xf4,iosize=0x04",
|
|
"-object",
|
|
"filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap",
|
|
];
|
|
|
|
pub fn random_hostfwd_ports() -> (u16, u16) {
|
|
let start = 32768u16;
|
|
let end = 61000u16;
|
|
let port1 = rand::random::<u16>() % (end - 1 - start) + start;
|
|
let port2 = rand::random::<u16>() % (end - port1) + port1;
|
|
(port1, port2)
|
|
}
|
|
|
|
pub const GDB_ARGS: &[&str] = &[
|
|
"-chardev",
|
|
"socket,path=/tmp/jinux-gdb-socket,server=on,wait=off,id=gdb0",
|
|
"-gdb",
|
|
"chardev:gdb0",
|
|
"-S",
|
|
];
|
|
|
|
fn run_gdb_client(path: &PathBuf, gdb_grub: bool) {
|
|
let path = std::fs::canonicalize(path).unwrap();
|
|
let mut gdb_cmd = Command::new("gdb");
|
|
// Set the architecture, otherwise GDB will complain about.
|
|
gdb_cmd.arg("-ex").arg("set arch i386:x86-64:intel");
|
|
let grub_script = "/tmp/jinux-gdb-grub-script";
|
|
if gdb_grub {
|
|
let grub_dir =
|
|
PathBuf::from(qemu_grub_efi::GRUB_LIB_PREFIX).join(qemu_grub_efi::GRUB_VERSION);
|
|
// Load symbols from GRUB using the provided grub gdb script.
|
|
// Read the contents from `gdb_grub` and
|
|
// replace the lines containing "target remote :1234".
|
|
gdb_cmd.current_dir(&grub_dir);
|
|
let grub_script_content = std::fs::read_to_string(grub_dir.join("gdb_grub")).unwrap();
|
|
let lines = grub_script_content.lines().collect::<Vec<_>>();
|
|
let mut f = OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.open(grub_script)
|
|
.unwrap();
|
|
for line in lines {
|
|
if line.contains("file kernel.exec") {
|
|
writeln!(f, "{}", line).unwrap();
|
|
// A horrible hack on GRUB EFI debugging.
|
|
// https://stackoverflow.com/questions/43872078/debug-grub2-efi-image-running-on-qemu
|
|
// Please use our custom built debug OVMF image to confirm the entrypoint address.
|
|
writeln!(f, "add-symbol-file kernel.exec 0x0007E69F000").unwrap();
|
|
} else if line.contains("target remote :1234") {
|
|
// Connect to the GDB server.
|
|
writeln!(f, "target remote /tmp/jinux-gdb-socket").unwrap();
|
|
} else {
|
|
writeln!(f, "{}", line).unwrap();
|
|
}
|
|
}
|
|
gdb_cmd.arg("-x").arg(grub_script);
|
|
} else {
|
|
// Load symbols from the kernel image.
|
|
gdb_cmd.arg("-ex").arg(format!("file {}", path.display()));
|
|
// Connect to the GDB server.
|
|
gdb_cmd
|
|
.arg("-ex")
|
|
.arg("target remote /tmp/jinux-gdb-socket");
|
|
}
|
|
// Connect to the GDB server and run.
|
|
println!("running:{:#?}", gdb_cmd);
|
|
gdb_cmd.status().unwrap();
|
|
if gdb_grub {
|
|
// Clean the temporary script file then return.
|
|
std::fs::remove_file(grub_script).unwrap();
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let args = Args::parse();
|
|
|
|
if args.run_gdb_client {
|
|
let gdb_grub = args.boot_method == BootMethod::QemuGrub;
|
|
// You should comment out the next line if you want to debug grub instead
|
|
// of the kernel because this argument is not exposed by runner CLI.
|
|
let gdb_grub = gdb_grub && false;
|
|
run_gdb_client(&args.path, gdb_grub);
|
|
return;
|
|
}
|
|
|
|
let mut qemu_cmd = Command::new("qemu-system-x86_64");
|
|
|
|
qemu_cmd.args(COMMON_ARGS);
|
|
|
|
qemu_cmd.arg("-netdev");
|
|
let (port1, port2) = random_hostfwd_ports();
|
|
qemu_cmd.arg(format!(
|
|
"user,id=net01,hostfwd=tcp::{}-:22,hostfwd=tcp::{}-:8080",
|
|
port1, port2
|
|
));
|
|
println!(
|
|
"[jinux-runner] Binding host ports to guest ports: ({} -> {}); ({} -> {}).",
|
|
port1, 22, port2, 8080
|
|
);
|
|
|
|
if args.halt_for_gdb {
|
|
if args.enable_kvm {
|
|
println!("[jinux-runner] Can't enable KVM when running QEMU as a GDB server. Abort.");
|
|
return;
|
|
}
|
|
qemu_cmd.args(GDB_ARGS);
|
|
}
|
|
|
|
if args.enable_kvm {
|
|
qemu_cmd.arg("-enable-kvm");
|
|
}
|
|
// Add machine-specific arguments
|
|
if args.boot_method == BootMethod::QemuGrub {
|
|
qemu_cmd.args(qemu_grub_efi::MACHINE_ARGS);
|
|
} else if args.boot_method == BootMethod::Microvm {
|
|
qemu_cmd.args(microvm::MACHINE_ARGS);
|
|
}
|
|
// Add device arguments
|
|
if args.boot_method == BootMethod::Microvm {
|
|
qemu_cmd.args(microvm::DEVICE_ARGS);
|
|
} else if args.emulate_iommu {
|
|
qemu_cmd.args(qemu_grub_efi::IOMMU_DEVICE_ARGS);
|
|
} else {
|
|
qemu_cmd.args(qemu_grub_efi::NOIOMMU_DEVICE_ARGS);
|
|
}
|
|
|
|
let fs_image = create_fs_image(args.path.as_path());
|
|
qemu_cmd.arg("-drive");
|
|
qemu_cmd.arg(fs_image);
|
|
|
|
if args.boot_method == BootMethod::Microvm {
|
|
let image = microvm::create_bootdev_image(args.path);
|
|
qemu_cmd.arg("-kernel");
|
|
qemu_cmd.arg(image.as_os_str());
|
|
qemu_cmd.arg("-append");
|
|
qemu_cmd.arg(&args.kcmdline);
|
|
qemu_cmd.arg("-initrd");
|
|
qemu_cmd.arg("regression/build/initramfs.cpio.gz");
|
|
} else if args.boot_method == BootMethod::QemuGrub {
|
|
let grub_cfg = qemu_grub_efi::generate_grub_cfg(
|
|
"runner/grub/grub.cfg.template",
|
|
&args.kcmdline,
|
|
args.skip_grub_menu,
|
|
args.boot_protocol,
|
|
);
|
|
let initramfs_path = PathBuf::from("regression/build/initramfs.cpio.gz");
|
|
let bootdev_image = qemu_grub_efi::create_bootdev_image(
|
|
args.path,
|
|
initramfs_path,
|
|
grub_cfg,
|
|
args.boot_protocol,
|
|
);
|
|
qemu_cmd.arg("-cdrom");
|
|
qemu_cmd.arg(bootdev_image.as_os_str());
|
|
}
|
|
|
|
println!("[jinux-runner] Running: {:#?}", qemu_cmd);
|
|
|
|
let exit_status = qemu_cmd.status().unwrap();
|
|
if !exit_status.success() {
|
|
// FIXME: Exit code manipulation is not needed when using non-x86 QEMU
|
|
let qemu_exit_code = exit_status.code().unwrap();
|
|
let kernel_exit_code = qemu_exit_code >> 1;
|
|
match kernel_exit_code {
|
|
0x10 /*jinux_frame::QemuExitCode::Success*/ => { std::process::exit(0); },
|
|
0x20 /*jinux_frame::QemuExitCode::Failed*/ => { std::process::exit(1); },
|
|
_ => { std::process::exit(qemu_exit_code) },
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn create_fs_image(path: &Path) -> String {
|
|
let mut fs_img_path = path.parent().unwrap().to_str().unwrap().to_string();
|
|
fs_img_path.push_str("/fs.img");
|
|
let path = Path::new(fs_img_path.as_str());
|
|
if path.exists() {
|
|
return format!("file={},if=none,format=raw,id=x0", fs_img_path.as_str());
|
|
}
|
|
let f = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.open(fs_img_path.as_str())
|
|
.unwrap();
|
|
// 32MiB
|
|
f.set_len(64 * 1024 * 1024).unwrap();
|
|
format!("file={},if=none,format=raw,id=x0", fs_img_path.as_str())
|
|
}
|