Skip to content

Commit 63d5130

Browse files
committed
[hyperlight-guest-tracing] Add crates that generate guest tracing records
- `hyperlight-guest-tracing` defines a `TraceBuffer` that keeps `TraceRecord`s that the guest issues. When the buffer capacity is reached, it automatically issues an `Out` instruction with the corresponding info for the host to retrieve the buffer. - The guest can issue `TraceRecord`s by using the `hyperlight-guest-tracing-macro` crate that pushes new records to the buffer. - `hyperlight_common` contains the definitions for the frame types a guest can send to the host using the `Out` instruction. Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent 407a2f4 commit 63d5130

File tree

8 files changed

+599
-2
lines changed

8 files changed

+599
-2
lines changed

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ members = [
1010
"src/hyperlight_guest",
1111
"src/hyperlight_host",
1212
"src/hyperlight_guest_capi",
13+
"src/hyperlight_guest_tracing",
14+
"src/hyperlight_guest_tracing_macro",
1315
"src/hyperlight_testing",
1416
"fuzz",
1517
"src/hyperlight_guest_bin",
@@ -40,6 +42,8 @@ hyperlight-host = { path = "src/hyperlight_host", version = "0.7.0", default-fea
4042
hyperlight-guest = { path = "src/hyperlight_guest", version = "0.7.0", default-features = false }
4143
hyperlight-guest-bin = { path = "src/hyperlight_guest_bin", version = "0.7.0", default-features = false }
4244
hyperlight-testing = { path = "src/hyperlight_testing", default-features = false }
45+
hyperlight-guest-tracing = { path = "src/hyperlight_guest_tracing", default-features = false }
46+
hyperlight-guest-tracing-macro = { path = "src/hyperlight_guest_tracing_macro", default-features = false }
4347
hyperlight-component-util = { path = "src/hyperlight_component_util", version = "0.7.0", default-features = false }
4448
hyperlight-component-macro = { path = "src/hyperlight_component_macro", version = "0.7.0", default-features = false }
4549

src/hyperlight_common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ spin = "0.10.0"
2525
[features]
2626
default = ["tracing"]
2727
fuzzing = ["dep:arbitrary"]
28+
trace_guest = []
2829
unwind_guest = []
2930
mem_profile = []
3031
std = []

src/hyperlight_common/src/outb.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ impl TryFrom<u8> for Exception {
9393
/// - TraceRecordStack: records the stack trace of the guest
9494
/// - TraceMemoryAlloc: records memory allocation events
9595
/// - TraceMemoryFree: records memory deallocation events
96+
/// - TraceRecord: records a trace event in the guest
9697
pub enum OutBAction {
9798
Log = 99,
9899
CallFunction = 101,
@@ -101,9 +102,11 @@ pub enum OutBAction {
101102
#[cfg(feature = "unwind_guest")]
102103
TraceRecordStack = 104,
103104
#[cfg(feature = "mem_profile")]
104-
TraceMemoryAlloc,
105+
TraceMemoryAlloc = 105,
105106
#[cfg(feature = "mem_profile")]
106-
TraceMemoryFree,
107+
TraceMemoryFree = 106,
108+
#[cfg(feature = "trace_guest")]
109+
TraceRecord = 107,
107110
}
108111

109112
impl TryFrom<u16> for OutBAction {
@@ -120,6 +123,8 @@ impl TryFrom<u16> for OutBAction {
120123
105 => Ok(OutBAction::TraceMemoryAlloc),
121124
#[cfg(feature = "mem_profile")]
122125
106 => Ok(OutBAction::TraceMemoryFree),
126+
#[cfg(feature = "trace_guest")]
127+
107 => Ok(OutBAction::TraceRecord),
123128
_ => Err(anyhow::anyhow!("Invalid OutBAction value: {}", val)),
124129
}
125130
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "hyperlight-guest-tracing"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
license.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
readme.workspace = true
10+
description = """Provides the tracing functionality for the hyperlight guest."""
11+
12+
[dependencies]
13+
hyperlight-common = { workspace = true, default-features = false, features = ["trace_guest"] }
14+
spin = "0.10.0"
15+
16+
[lints]
17+
workspace = true
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/*
2+
Copyright 2025 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
#![no_std]
17+
18+
// === Dependencies ===
19+
extern crate alloc;
20+
21+
use core::mem::MaybeUninit;
22+
23+
use hyperlight_common::outb::OutBAction;
24+
use spin::Mutex;
25+
26+
/// Type alias for the function that sends trace records to the host.
27+
type SendToHostFn = fn(u64, &[TraceRecord]);
28+
29+
/// Global trace buffer for storing trace records.
30+
static TRACE_BUFFER: Mutex<TraceBuffer<SendToHostFn>> = Mutex::new(TraceBuffer::new(send_to_host));
31+
32+
/// Maximum number of entries in the trace buffer.
33+
/// From local testing, 32 entries seems to be a good balance between performance and memory usage.
34+
const MAX_NO_OF_ENTRIES: usize = 32;
35+
36+
/// Maximum length of a trace message in bytes.
37+
pub const MAX_TRACE_MSG_LEN: usize = 64;
38+
39+
#[derive(Debug, Copy, Clone)]
40+
/// Represents a trace record of a guest with a number of cycles and a message.
41+
pub struct TraceRecord {
42+
/// The number of CPU cycles returned by the invariant TSC.
43+
pub cycles: u64,
44+
/// The length of the message in bytes.
45+
pub msg_len: usize,
46+
/// The message associated with the trace record.
47+
pub msg: [u8; MAX_TRACE_MSG_LEN],
48+
}
49+
50+
impl From<&str> for TraceRecord {
51+
fn from(mut msg: &str) -> Self {
52+
if msg.len() > MAX_TRACE_MSG_LEN {
53+
// If the message is too long, truncate it to fit the maximum length
54+
msg = &msg[..MAX_TRACE_MSG_LEN];
55+
}
56+
57+
let cycles = invariant_tsc::read_tsc();
58+
59+
TraceRecord {
60+
cycles,
61+
msg: {
62+
let mut arr = [0u8; MAX_TRACE_MSG_LEN];
63+
arr[..msg.len()].copy_from_slice(msg.as_bytes());
64+
arr
65+
},
66+
msg_len: msg.len(),
67+
}
68+
}
69+
}
70+
71+
/// A buffer for storing trace records.
72+
struct TraceBuffer<F: Fn(u64, &[TraceRecord])> {
73+
/// The entries in the trace buffer.
74+
entries: [TraceRecord; MAX_NO_OF_ENTRIES],
75+
/// The index where the next entry will be written.
76+
write_index: usize,
77+
/// Function to send the trace records to the host.
78+
send_to_host: F,
79+
}
80+
81+
impl<F: Fn(u64, &[TraceRecord])> TraceBuffer<F> {
82+
/// Creates a new `TraceBuffer` with uninitialized entries.
83+
const fn new(f: F) -> Self {
84+
Self {
85+
entries: unsafe { [MaybeUninit::zeroed().assume_init(); MAX_NO_OF_ENTRIES] },
86+
write_index: 0,
87+
send_to_host: f,
88+
}
89+
}
90+
91+
/// Push a new trace record into the buffer.
92+
/// If the buffer is full, it sends the records to the host.
93+
fn push(&mut self, entry: TraceRecord) {
94+
let mut write_index = self.write_index;
95+
96+
self.entries[write_index] = entry;
97+
write_index = (write_index + 1) % MAX_NO_OF_ENTRIES;
98+
99+
self.write_index = write_index;
100+
101+
if write_index == 0 {
102+
// If buffer is full send to host
103+
(self.send_to_host)(MAX_NO_OF_ENTRIES as u64, &self.entries);
104+
}
105+
}
106+
107+
/// Flush the trace buffer, sending any remaining records to the host.
108+
fn flush(&mut self) {
109+
if self.write_index > 0 {
110+
(self.send_to_host)(self.write_index as u64, &self.entries);
111+
self.write_index = 0; // Reset write index after flushing
112+
}
113+
}
114+
}
115+
116+
/// Send the trace records to the host.
117+
fn send_to_host(len: u64, records: &[TraceRecord]) {
118+
unsafe {
119+
core::arch::asm!("out dx, al",
120+
in("dx") OutBAction::TraceRecord as u16,
121+
in("rax") len,
122+
in("rcx") records.as_ptr() as u64);
123+
}
124+
}
125+
126+
/// Module for checking invariant TSC support and reading the timestamp counter
127+
pub mod invariant_tsc {
128+
use core::arch::x86_64::{__cpuid, _rdtsc};
129+
130+
/// Check if the processor supports invariant TSC
131+
///
132+
/// Returns true if CPUID.80000007H:EDX[8] is set, indicating invariant TSC support
133+
pub fn has_invariant_tsc() -> bool {
134+
// Check if extended CPUID functions are available
135+
let max_extended = unsafe { __cpuid(0x80000000) };
136+
if max_extended.eax < 0x80000007 {
137+
return false;
138+
}
139+
140+
// Query CPUID.80000007H for invariant TSC support
141+
let cpuid_result = unsafe { __cpuid(0x80000007) };
142+
143+
// Check bit 8 of EDX register for invariant TSC support
144+
(cpuid_result.edx & (1 << 8)) != 0
145+
}
146+
147+
/// Read the timestamp counter
148+
///
149+
/// This function provides a high-performance timestamp by reading the TSC.
150+
/// Should only be used when invariant TSC is supported for reliable timing.
151+
///
152+
/// # Safety
153+
/// This function uses unsafe assembly instructions but is safe to call.
154+
/// However, the resulting timestamp is only meaningful if invariant TSC is supported.
155+
pub fn read_tsc() -> u64 {
156+
unsafe { _rdtsc() }
157+
}
158+
}
159+
160+
/// Create a trace record from the message and push it to the trace buffer.
161+
///
162+
/// **NOTE**: If the message is too long it will be truncated to fit within `MAX_TRACE_MSG_LEN`.
163+
/// This is useful for ensuring that the trace buffer does not overflow.
164+
pub fn create_trace_record(msg: &str) {
165+
let entry = TraceRecord::from(msg);
166+
let mut buffer = TRACE_BUFFER.lock();
167+
168+
buffer.push(entry);
169+
}
170+
171+
/// Flush the trace buffer to send any remaining trace records to the host.
172+
pub fn flush_trace_buffer() {
173+
let mut buffer = TRACE_BUFFER.lock();
174+
buffer.flush();
175+
}
176+
177+
#[cfg(test)]
178+
mod tests {
179+
use alloc::format;
180+
181+
use super::*;
182+
183+
/// This is a mock function for testing purposes.
184+
/// In a real scenario, this would send the trace records to the host.
185+
fn mock_send_to_host(_len: u64, _records: &[TraceRecord]) {}
186+
187+
fn create_test_entry(msg: &str) -> TraceRecord {
188+
let cycles = invariant_tsc::read_tsc();
189+
190+
TraceRecord {
191+
cycles,
192+
msg: {
193+
let mut arr = [0u8; MAX_TRACE_MSG_LEN];
194+
arr[..msg.len()].copy_from_slice(msg.as_bytes());
195+
arr
196+
},
197+
msg_len: msg.len(),
198+
}
199+
}
200+
201+
#[test]
202+
fn test_push_trace_record() {
203+
let mut buffer = TraceBuffer::new(mock_send_to_host);
204+
205+
let msg = "Test message";
206+
let entry = create_test_entry(msg);
207+
208+
buffer.push(entry);
209+
assert_eq!(buffer.write_index, 1);
210+
assert_eq!(buffer.entries[0].msg_len, msg.len());
211+
assert_eq!(&buffer.entries[0].msg[..msg.len()], msg.as_bytes());
212+
assert!(buffer.entries[0].cycles > 0); // Ensure cycles is set
213+
}
214+
215+
#[test]
216+
fn test_flush_trace_buffer() {
217+
let mut buffer = TraceBuffer::new(mock_send_to_host);
218+
219+
let msg = "Test message";
220+
let entry = create_test_entry(msg);
221+
222+
buffer.push(entry);
223+
assert_eq!(buffer.write_index, 1);
224+
assert_eq!(buffer.entries[0].msg_len, msg.len());
225+
assert_eq!(&buffer.entries[0].msg[..msg.len()], msg.as_bytes());
226+
assert!(buffer.entries[0].cycles > 0);
227+
228+
// Flush the buffer
229+
buffer.flush();
230+
231+
// After flushing, the entryes should still be intact, we don't clear them
232+
assert_eq!(buffer.write_index, 0);
233+
assert_eq!(buffer.entries[0].msg_len, msg.len());
234+
assert_eq!(&buffer.entries[0].msg[..msg.len()], msg.as_bytes());
235+
assert!(buffer.entries[0].cycles > 0);
236+
}
237+
238+
#[test]
239+
fn test_auto_flush_on_full() {
240+
let mut buffer = TraceBuffer::new(mock_send_to_host);
241+
242+
// Fill the buffer to trigger auto-flush
243+
for i in 0..MAX_NO_OF_ENTRIES {
244+
let msg = format!("Message {}", i);
245+
let entry = create_test_entry(&msg);
246+
buffer.push(entry);
247+
}
248+
249+
// After filling, the write index should be 0 (buffer is full)
250+
assert_eq!(buffer.write_index, 0);
251+
252+
// The first entry should still be intact
253+
assert_eq!(buffer.entries[0].msg_len, "Message 0".len());
254+
}
255+
256+
/// Test TraceRecord creation with a valid message
257+
#[test]
258+
fn test_trace_record_creation_valid() {
259+
let msg = "Valid message";
260+
let entry = TraceRecord::try_from(msg).expect("Failed to create TraceRecord");
261+
assert_eq!(entry.msg_len, msg.len());
262+
assert_eq!(&entry.msg[..msg.len()], msg.as_bytes());
263+
assert!(entry.cycles > 0); // Ensure cycles is set
264+
}
265+
266+
/// Test TraceRecord creation with a message that exceeds the maximum length
267+
#[test]
268+
fn test_trace_record_creation_too_long() {
269+
let long_msg = "A".repeat(MAX_TRACE_MSG_LEN + 1);
270+
let result = TraceRecord::from(long_msg.as_str());
271+
assert_eq!(result.msg_len, MAX_TRACE_MSG_LEN);
272+
assert_eq!(
273+
&result.msg[..MAX_TRACE_MSG_LEN],
274+
&long_msg.as_bytes()[..MAX_TRACE_MSG_LEN],
275+
);
276+
}
277+
}

0 commit comments

Comments
 (0)