Add coverage support

Co-authored-by: Marsman1996 <lqliuyuwei@outlook.com>
This commit is contained in:
YanWQ-monad 2025-06-25 11:53:10 +08:00 committed by Tate, Hongliang Tian
parent 03fc309b95
commit 79335b272f
17 changed files with 319 additions and 15 deletions

126
Cargo.lock generated
View File

@ -431,6 +431,15 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -1136,6 +1145,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "minicov"
version = "0.3.7"
source = "git+https://github.com/asterinas/minicov?rev=bd5454a#bd5454a58f5e64ef67519df9fb2025c96b47f8be"
dependencies = [
"cc",
"walkdir",
]
[[package]]
name = "multiboot2"
version = "0.24.0"
@ -1259,6 +1277,7 @@ dependencies = [
"intrusive-collections",
"linux-boot-params",
"log",
"minicov",
"multiboot2",
"num-traits",
"ostd-macros",
@ -1531,6 +1550,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "sbi-rt"
version = "0.0.3"
@ -1587,6 +1615,12 @@ dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.0"
@ -1882,12 +1916,104 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8ca9a5d4debca0633e697c88269395493cebf2e10db21ca2dbde37c1356452"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.5.40"

View File

@ -20,6 +20,7 @@ SMP ?= 1
OSTD_TASK_STACK_SIZE_IN_PAGES ?= 64
FEATURES ?=
NO_DEFAULT_FEATURES ?= 0
COVERAGE ?= 0
# End of global build options.
# GDB debugging and profiling options.
@ -110,6 +111,10 @@ else
CARGO_OSDK_COMMON_ARGS += --boot-method="$(BOOT_METHOD)"
endif
ifeq ($(COVERAGE), 1)
CARGO_OSDK_COMMON_ARGS += --coverage
endif
ifdef FEATURES
CARGO_OSDK_COMMON_ARGS += --features="$(FEATURES)"
endif

View File

@ -25,6 +25,29 @@ comma separated configuration list:
- `vscode`: generate a '.vscode/launch.json' for debugging with Visual Studio Code
(Requires [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)).
Besides, to collect coverage data, we can use option `--coverage`. This option
enables the coverage feature and collect coverage data to `coverage.profraw` when exit.
It actually does several things:
- It adds `-Cinstrument-coverage -Zno-profiler-runtime` to `RUSTFLAGS` so LLVM will
generate coverage instrument. And `coverage` features will be enabled for `ostd`,
then before `exit_qemu` actually quit QEMU, it will call `minicov` to collect
the coverage data to guest's own memory, and print its address and size, so that
OSDK can dump it out of guest.
- Next, `--no-shutdown` will be enabled for QEMU, and OSDK will setup a monitor
connection to QEMU to monitor its status. Once exit, it dumps the coverage data
from guest's memory to `coverage.profraw`.
**Note.** The code coverage feature of OSDK requires a non-default OSTD feature called
`coverage`, which relies on dependencies that are not published on crates.io.
To utilize this feature, projects must specify OSTD as a dependency using a Git
repository or a local filesystem path in their `Cargo.toml`. For example:
```toml
[dependencies]
ostd = { git = "https://github.com/asterinas/asterinas", rev = "v0.11.0", features = ["coverage"] }
```
See [Debug Command](debug.md) to interact with the GDB server in terminal.
## Examples

View File

@ -74,6 +74,7 @@ riscv = { version = "0.11.1", features = ["s-mode"] }
[features]
all = ["cvm_guest"]
cvm_guest = ["dep:tdx-guest", "ostd/cvm_guest", "aster-virtio/cvm_guest"]
coverage = ["ostd/coverage"]
[lints]
workspace = true

14
osdk/Cargo.lock generated
View File

@ -202,6 +202,7 @@ dependencies = [
"serde_json",
"shlex",
"syn",
"tempfile",
"toml",
"which",
]
@ -941,6 +942,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [
"cfg-if",
"fastrand",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "termtree"
version = "0.5.1"

View File

@ -25,6 +25,7 @@ serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
shlex = "1.3.0"
syn = { version = "2.0.52", features = ["extra-traits", "full", "parsing", "printing"] }
tempfile = "3.14.0"
toml = { version = "0.8.8", features = ["preserve_order"] }
which = "8.0.0"

View File

@ -6,7 +6,13 @@ pub mod vm_image;
use bin::AsterBin;
use file::{BundleFile, Initramfs};
use std::process;
use std::{
io::{BufRead, BufReader, Write},
os::unix::net::UnixStream,
process,
time::Duration,
};
use tempfile::NamedTempFile;
use vm_image::{AsterVmImage, AsterVmImageType};
use std::{
@ -258,19 +264,40 @@ impl Bundle {
}
}
info!("Running QEMU: {:#?}", qemu_cmd);
let exit_status = if action.qemu.with_monitor {
let qemu_socket = NamedTempFile::new().unwrap().into_temp_path();
qemu_cmd.arg("-monitor").arg(format!(
"unix:{},server,nowait",
qemu_socket.to_string_lossy()
));
let exit_status = qemu_cmd.status().unwrap();
info!("Running QEMU: {qemu_cmd:#?}");
let mut qemu_child = qemu_cmd.spawn().unwrap();
std::thread::sleep(Duration::from_secs(1)); // Wait for QEMU to start
let mut qemu_monitor_stream = UnixStream::connect(qemu_socket).unwrap();
// Find the QEMU output in "qemu.log", read it and check if it failed with a panic.
// Setting a QEMU log is required for source line stack trace because piping the output
// is less desirable when running QEMU with serial redirected to standard I/O.
let qemu_log_path = config.work_dir.join("qemu.log");
if let Ok(file) = std::fs::File::open(qemu_log_path) {
if let Some(aster_bin) = &self.manifest.aster_bin {
crate::util::trace_panic_from_log(file, self.path.join(aster_bin.path()));
// Check VM status every 0.1 seconds and break the loop if the VM is stopped.
while qemu_monitor_stream.write_all(b"info status\n").is_ok() {
let status = BufReader::new(&qemu_monitor_stream)
.lines()
.find(|line| line.as_ref().is_ok_and(|s| s.starts_with("VM status:")));
if status.is_some_and(|msg| msg.unwrap() == "VM status: paused (shutdown)") {
break;
}
std::thread::sleep(Duration::from_millis(100));
}
}
info!("VM is paused (shutdown)");
self.post_run_action(config, Some(&mut qemu_monitor_stream));
let _ = qemu_monitor_stream.write_all(b"quit\n");
qemu_child.wait().unwrap()
} else {
info!("Running QEMU: {qemu_cmd:#?}");
let exit_status = qemu_cmd.status().unwrap();
self.post_run_action(config, None);
exit_status
};
// FIXME: When panicking it sometimes returns success, why?
if !exit_status.success() {
@ -309,4 +336,23 @@ impl Bundle {
let manifest_file_path = self.path.join("bundle.toml");
std::fs::write(manifest_file_path, manifest_file_content).unwrap();
}
fn post_run_action(&self, config: &Config, qemu_monitor_stream: Option<&mut UnixStream>) {
// Find the QEMU output in "qemu.log", read it and check if it failed with a panic.
// Setting a QEMU log is required for source line stack trace because piping the output
// is less desirable when running QEMU with serial redirected to standard I/O.
let qemu_log_path = config.work_dir.join("qemu.log");
if let Ok(file) = std::fs::File::open(&qemu_log_path) {
if let Some(aster_bin) = &self.manifest.aster_bin {
crate::util::trace_panic_from_log(file, self.path.join(aster_bin.path()));
}
}
// Find the coverage data information in "qemu.log", and dump it if found.
if let Some(qemu_monitor_stream) = qemu_monitor_stream {
if let Ok(file) = std::fs::File::open(&qemu_log_path) {
crate::util::dump_coverage_from_qemu(file, qemu_monitor_stream);
}
}
}
}

View File

@ -24,7 +24,11 @@ pub fn main() {
let load_config = |common_args: &CommonArgs| {
let manifest = TomlManifest::load();
let scheme = manifest.get_scheme(common_args.scheme.as_ref());
Config::new(scheme, common_args)
let mut config = Config::new(scheme, common_args);
config
.build
.append_rustflags(&std::env::var("RUSTFLAGS").unwrap_or_default());
config
};
let cli = Cli::parse();
@ -481,4 +485,6 @@ pub struct CommonArgs {
global = true
)]
pub encoding: Option<PayloadEncoding>,
#[arg(long = "coverage", help = "Enable coverage", global = true)]
pub coverage: bool,
}

View File

@ -135,6 +135,8 @@ pub fn do_cached_build(
ActionChoice::Test => (&config.test.build, &config.test.boot),
};
let mut rustflags = rustflags.to_vec();
rustflags.push(&build.rustflags);
let aster_elf = build_kernel_elf(
config.target_arch,
&build.profile,
@ -142,7 +144,7 @@ pub fn do_cached_build(
build.no_default_features,
&build.override_configs[..],
&cargo_target_directory,
rustflags,
&rustflags,
);
// Check the existing bundle's reusability
@ -199,11 +201,9 @@ fn build_kernel_elf(
let target_os_string = OsString::from(&arch.triple());
let rustc_linker_script_arg = format!("-C link-arg=-T{}.ld", arch);
let env_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
let mut rustflags = Vec::from(rustflags);
// Asterinas does not support PIC yet.
rustflags.extend(vec![
&env_rustflags,
&rustc_linker_script_arg,
"-C relocation-model=static",
"-C relro-level=off",
@ -244,6 +244,10 @@ fn build_kernel_elf(
command.arg("--config").arg(override_config);
}
const CFLAGS: &str = "CFLAGS_x86_64-unknown-none";
let env_cflags = std::env::var(CFLAGS).unwrap_or_default();
command.env(CFLAGS, env_cflags + " -fPIC");
info!("Building kernel ELF using command: {:#?}", command);
info!("Building directory: {:?}", std::env::current_dir().unwrap());

View File

@ -178,6 +178,10 @@ fn apply_args_after_finalize(action: &mut Action, args: &CommonArgs) {
if args.display_grub_menu {
action.grub.display_grub_menu = true;
}
if args.coverage {
action.qemu.args += " --no-shutdown";
action.qemu.with_monitor = true;
}
}
impl Config {

View File

@ -26,6 +26,8 @@ pub struct BuildScheme {
#[serde(default)]
pub strip_elf: bool,
pub encoding: Option<PayloadEncoding>,
#[serde(default)]
pub rustflags: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -42,6 +44,8 @@ pub struct Build {
#[serde(default)]
pub strip_elf: bool,
pub encoding: PayloadEncoding,
#[serde(default)]
pub rustflags: String,
}
impl Default for Build {
@ -54,6 +58,7 @@ impl Default for Build {
linux_x86_legacy_boot: false,
strip_elf: false,
encoding: PayloadEncoding::default(),
rustflags: String::new(),
}
}
}
@ -79,6 +84,16 @@ impl Build {
if let Some(encoding) = common_args.encoding.clone() {
self.encoding.clone_from(&encoding);
}
if common_args.coverage {
self.append_rustflags("-Cinstrument-coverage -Zno-profiler-runtime");
self.features.push("coverage".to_string());
}
}
pub fn append_rustflags(&mut self, rustflags: &str) {
self.rustflags += " ";
self.rustflags += rustflags;
}
}
@ -102,6 +117,7 @@ impl BuildScheme {
if self.encoding.is_none() {
self.encoding.clone_from(&parent.encoding);
}
self.rustflags = parent.rustflags.clone() + " " + &self.rustflags;
}
pub fn finalize(self) -> Build {
@ -113,6 +129,7 @@ impl BuildScheme {
linux_x86_legacy_boot: self.linux_x86_legacy_boot,
strip_elf: self.strip_elf,
encoding: self.encoding.unwrap_or_default(),
rustflags: self.rustflags,
}
}
}

View File

@ -32,6 +32,8 @@ pub struct QemuScheme {
pub bootdev_append_options: Option<String>,
/// The path of qemu
pub path: Option<PathBuf>,
/// Whether to run QEMU as daemon and connect to it in monitor mode.
pub with_monitor: Option<bool>,
}
#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
@ -43,6 +45,8 @@ pub struct Qemu {
/// [`crate::bundle::Bundle::run`].
pub bootdev_append_options: Option<String>,
pub path: PathBuf,
/// Whether to run QEMU as daemon and connect to it in monitor mode.
pub with_monitor: bool,
}
impl Default for Qemu {
@ -51,6 +55,7 @@ impl Default for Qemu {
args: String::new(),
bootdev_append_options: None,
path: PathBuf::from(get_default_arch().system_qemu()),
with_monitor: false,
}
}
}
@ -66,6 +71,7 @@ impl PartialEq for Qemu {
strip_numbers(&self.args) == strip_numbers(&other.args)
&& self.bootdev_append_options == other.bootdev_append_options
&& self.path == other.path
&& self.with_monitor == other.with_monitor
}
}
@ -92,6 +98,9 @@ impl QemuScheme {
if self.path.is_none() {
self.path.clone_from(&from.path);
}
if self.with_monitor.is_none() {
self.with_monitor.clone_from(&from.with_monitor);
}
}
pub fn finalize(self, arch: Arch) -> Qemu {
@ -99,6 +108,7 @@ impl QemuScheme {
args: self.args.unwrap_or_default(),
bootdev_append_options: self.bootdev_append_options,
path: self.path.unwrap_or(PathBuf::from(arch.system_qemu())),
with_monitor: self.with_monitor.unwrap_or(false),
}
}
}

View File

@ -5,6 +5,7 @@ use std::{
ffi::OsStr,
fs::{self, File},
io::{BufRead, BufReader, Result, Write},
os::unix::net::UnixStream,
path::{Path, PathBuf},
process::Command,
sync::{LazyLock, Mutex},
@ -310,6 +311,30 @@ pub fn trace_panic_from_log(qemu_log: File, bin_path: PathBuf) {
addr2line_proc.wait().unwrap();
}
/// Dump the coverage data from QEMU if the coverage information is found in the log.
pub fn dump_coverage_from_qemu(qemu_log: File, monitor_socket: &mut UnixStream) {
const COVERAGE_SIGNATRUE: &str = "#### Coverage: ";
let reader = rev_buf_reader::RevBufReader::new(qemu_log);
let Some(line) = reader
.lines()
.find(|l| l.as_ref().unwrap().starts_with(COVERAGE_SIGNATRUE))
.map(|l| l.unwrap())
else {
return;
};
let line = line.strip_prefix(COVERAGE_SIGNATRUE).unwrap();
let (addr, size) = line.split_once(' ').unwrap();
let addr = usize::from_str_radix(addr.strip_prefix("0x").unwrap(), 16).unwrap();
let size: usize = size.parse().unwrap();
let cmd = format!("memsave 0x{addr:x} {size} coverage.profraw\n");
if monitor_socket.write_all(cmd.as_bytes()).is_ok() {
info!("Coverage data saved to coverage.profraw");
}
}
/// A guard that ensures the current working directory is restored
/// to its original state when the guard goes out of scope.
pub struct DirGuard(PathBuf);

View File

@ -38,6 +38,8 @@ unwinding = { version = "=0.2.5", default-features = false, features = ["fde-gnu
volatile = "0.6.1"
bitvec = { version = "1.0", default-features = false, features = ["alloc"] }
minicov = { git = "https://github.com/asterinas/minicov", rev = "bd5454a", version = "0.3", optional = true }
[target.x86_64-unknown-none.dependencies]
x86_64 = "0.14.13"
x86 = "0.52.0"
@ -59,6 +61,7 @@ fdt = { version = "0.1.5", features = ["pretty-printing"] }
default = ["cvm_guest"]
# The guest OS support for Confidential VMs (CVMs), e.g., Intel TDX
cvm_guest = ["dep:tdx-guest", "dep:iced-x86"]
coverage = ["minicov"]
[lints]
workspace = true

View File

@ -24,6 +24,9 @@ pub enum QemuExitCode {
/// QEMU command line arguments that specifies the ISA debug exit device:
/// `-device isa-debug-exit,iobase=0xf4,iosize=0x04`.
pub fn exit_qemu(exit_code: QemuExitCode) -> ! {
#[cfg(feature = "coverage")]
crate::coverage::dump_profraw();
use x86_64::instructions::port::Port;
let mut port = Port::new(0xf4);

13
ostd/src/coverage.rs Normal file
View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::vec::Vec;
pub fn dump_profraw() {
let mut coverage = Vec::new();
unsafe {
minicov::capture_coverage(&mut coverage).unwrap();
}
let coverage = coverage.leak();
crate::early_println!("#### Coverage: {:p} {}", coverage.as_ptr(), coverage.len());
}

View File

@ -50,6 +50,9 @@ pub mod trap;
pub mod user;
pub mod util;
#[cfg(feature = "coverage")]
mod coverage;
use core::sync::atomic::{AtomicBool, Ordering};
pub use ostd_macros::{