Skip to content

Commit e0338cc

Browse files
committed
[trace-guest] update outb instructions to include trace info
Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent 3d62622 commit e0338cc

File tree

4 files changed

+179
-3
lines changed

4 files changed

+179
-3
lines changed

src/hyperlight_common/src/outb.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,16 @@ impl TryFrom<u8> for Exception {
9090
/// - CallFunction: makes a call to a host function,
9191
/// - Abort: aborts the execution of the guest,
9292
/// - DebugPrint: prints a message to the host
93+
/// - TraceBatch: reports a batch of spans and vents from the guest
9394
/// - TraceMemoryAlloc: records memory allocation events
9495
/// - TraceMemoryFree: records memory deallocation events
9596
pub enum OutBAction {
9697
Log = 99,
9798
CallFunction = 101,
9899
Abort = 102,
99100
DebugPrint = 103,
101+
#[cfg(feature = "trace_guest")]
102+
TraceBatch = 104,
100103
#[cfg(feature = "mem_profile")]
101104
TraceMemoryAlloc = 105,
102105
#[cfg(feature = "mem_profile")]
@@ -111,6 +114,8 @@ impl TryFrom<u16> for OutBAction {
111114
101 => Ok(OutBAction::CallFunction),
112115
102 => Ok(OutBAction::Abort),
113116
103 => Ok(OutBAction::DebugPrint),
117+
#[cfg(feature = "trace_guest")]
118+
104 => Ok(OutBAction::TraceBatch),
114119
#[cfg(feature = "mem_profile")]
115120
105 => Ok(OutBAction::TraceMemoryAlloc),
116121
#[cfg(feature = "mem_profile")]

src/hyperlight_guest/src/exit.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,32 @@ use hyperlight_common::outb::OutBAction;
2222
/// Halt the execution of the guest and returns control to the host.
2323
#[inline(never)]
2424
pub fn halt() {
25-
unsafe { asm!("hlt", options(nostack)) }
25+
#[cfg(feature = "trace_guest")]
26+
{
27+
// End any ongoing trace before halting
28+
hyperlight_guest_tracing::end_trace();
29+
// If tracing is enabled, we need to pass the trace batch info
30+
// along with the halt instruction so the host can retrieve it
31+
if let Some(tbi) = hyperlight_guest_tracing::guest_trace_info() {
32+
unsafe {
33+
asm!("hlt",
34+
in("r8") OutBAction::TraceBatch as u64,
35+
in("r9") tbi.spans_ptr,
36+
in("r10") tbi.guest_start_tsc,
37+
options(nostack)
38+
)
39+
};
40+
} else {
41+
// If tracing is not enabled, we can directly halt
42+
unsafe { asm!("hlt", options(nostack)) };
43+
}
44+
}
45+
46+
#[cfg(not(feature = "trace_guest"))]
47+
{
48+
// If tracing is not enabled, we can directly halt
49+
unsafe { asm!("hlt", options(nostack)) };
50+
}
2651
}
2752

2853
/// Exits the VM with an Abort OUT action and code 0.
@@ -33,6 +58,9 @@ pub extern "C" fn abort() -> ! {
3358

3459
/// Exits the VM with an Abort OUT action and a specific code.
3560
pub fn abort_with_code(code: &[u8]) -> ! {
61+
// End any ongoing trace before aborting
62+
#[cfg(feature = "trace_guest")]
63+
hyperlight_guest_tracing::end_trace();
3664
outb(OutBAction::Abort as u16, code);
3765
outb(OutBAction::Abort as u16, &[0xFF]); // send abort terminator (if not included in code)
3866
unreachable!()
@@ -43,6 +71,9 @@ pub fn abort_with_code(code: &[u8]) -> ! {
4371
/// # Safety
4472
/// This function is unsafe because it dereferences a raw pointer.
4573
pub unsafe fn abort_with_code_and_message(code: &[u8], message_ptr: *const c_char) -> ! {
74+
// End any ongoing trace before aborting
75+
#[cfg(feature = "trace_guest")]
76+
hyperlight_guest_tracing::end_trace();
4677
unsafe {
4778
// Step 1: Send abort code (typically 1 byte, but `code` allows flexibility)
4879
outb(OutBAction::Abort as u16, code);
@@ -81,6 +112,28 @@ pub(crate) fn outb(port: u16, data: &[u8]) {
81112

82113
/// OUT function for sending a 32-bit value to the host.
83114
pub(crate) unsafe fn out32(port: u16, val: u32) {
115+
#[cfg(feature = "trace_guest")]
116+
{
117+
if let Some(tbi) = hyperlight_guest_tracing::guest_trace_info() {
118+
// If tracing is enabled, send the trace batch info along with the OUT action
119+
unsafe {
120+
asm!("out dx, eax",
121+
in("dx") port,
122+
in("eax") val,
123+
in("r8") OutBAction::TraceBatch as u64,
124+
in("r9") tbi.spans_ptr,
125+
in("r10") tbi.guest_start_tsc,
126+
options(preserves_flags, nomem, nostack)
127+
)
128+
};
129+
} else {
130+
// If tracing is not enabled, just send the value
131+
unsafe {
132+
asm!("out dx, eax", in("dx") port, in("eax") val, options(preserves_flags, nomem, nostack))
133+
};
134+
}
135+
}
136+
#[cfg(not(feature = "trace_guest"))]
84137
unsafe {
85138
asm!("out dx, eax", in("dx") port, in("eax") val, options(preserves_flags, nomem, nostack));
86139
}

src/hyperlight_guest_tracing/src/lib.rs

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616
#![no_std]
1717
use heapless as hl;
1818
#[cfg(feature = "trace")]
19-
pub use trace::init_guest_tracing;
19+
pub use trace::{TraceBatchInfo, end_trace, guest_trace_info, init_guest_tracing};
2020

2121
/// Module for checking invariant TSC support and reading the timestamp counter
2222
pub mod invariant_tsc {
@@ -139,18 +139,27 @@ mod trace {
139139
use alloc::sync::{Arc, Weak};
140140
use core::fmt::Debug;
141141
use core::sync::atomic::{AtomicU64, Ordering};
142+
143+
use hyperlight_common::outb::OutBAction;
144+
use spin::Mutex;
142145
use tracing_core::field::{Field, Visit};
143146
use tracing_core::span::{Attributes, Id, Record};
144147
use tracing_core::subscriber::Subscriber;
145148
use tracing_core::{Event, Metadata};
146-
use spin::Mutex;
147149

148150
use super::*;
149151
use crate::invariant_tsc;
150152

151153
/// Weak reference to the guest state so we can manually trigger flush to host
152154
static GUEST_STATE: spin::Once<Weak<Mutex<GuestState>>> = spin::Once::new();
153155

156+
pub struct TraceBatchInfo {
157+
/// The timestamp counter at the start of the guest execution.
158+
pub guest_start_tsc: u64,
159+
/// Pointer to the spans in the guest memory.
160+
pub spans_ptr: u64,
161+
}
162+
154163
/// Visitor implementation to collect fields into a vector of key-value pairs
155164
struct FieldsVisitor<'a, const FK: usize, const FV: usize, const F: usize> {
156165
out: &'a mut hl::Vec<(hl::String<FK>, hl::String<FV>), F>,
@@ -207,6 +216,8 @@ mod trace {
207216
const FV: usize,
208217
const F: usize,
209218
> {
219+
/// Whether we need to cleanup the state on next access
220+
cleanup_needed: bool,
210221
/// The timestamp counter at the start of the guest execution.
211222
guest_start_tsc: u64,
212223
/// Next span ID to allocate
@@ -229,6 +240,7 @@ mod trace {
229240
{
230241
fn new(guest_start_tsc: u64) -> Self {
231242
Self {
243+
cleanup_needed: false,
232244
guest_start_tsc,
233245
next_id: AtomicU64::new(1),
234246
spans: hl::Vec::new(),
@@ -243,13 +255,91 @@ mod trace {
243255
(n, Id::from_u64(n))
244256
}
245257

258+
/// Cleanup internal state by removing closed spans and events
259+
/// This ensures that after a VM exit, we keep the spans that
260+
/// are still active (in the stack) and remove all other spans and events.
261+
fn clean(&mut self) {
262+
// used for computing the spans that need to be removed
263+
let mut ids: hl::Vec<u64, SP> = self.spans.iter().map(|s| s.id).collect();
264+
265+
for id in self.stack.iter() {
266+
let position = ids.iter().position(|s| *s == *id).unwrap();
267+
// remove the span id that is contained in the stack
268+
ids.remove(position);
269+
}
270+
271+
// Remove the spans with the remaining ids
272+
for id in ids.into_iter() {
273+
let spans = &mut self.spans;
274+
let position = spans.iter().position(|s| s.id == id).unwrap();
275+
spans.remove(position);
276+
}
277+
278+
// Remove the events from the remaining spans
279+
for s in self.spans.iter_mut() {
280+
s.events.clear();
281+
}
282+
}
283+
284+
#[inline(always)]
285+
fn verify_and_clean(&mut self) {
286+
if self.cleanup_needed {
287+
self.clean();
288+
self.cleanup_needed = false;
289+
}
290+
}
291+
246292
/// Triggers a VM exit to flush the current spans to the host.
247293
/// This also clears the internal state to start fresh.
248294
fn send_to_host(&mut self) {
295+
let guest_start_tsc = self.guest_start_tsc;
296+
let spans_ptr = self.spans.as_ptr() as u64;
297+
298+
unsafe {
299+
core::arch::asm!("out dx, al",
300+
// Port value for tracing
301+
in("dx") OutBAction::TraceBatch as u16,
302+
// Additional magic number to identify the action
303+
in("r8") OutBAction::TraceBatch as u64,
304+
in("r9") spans_ptr,
305+
in("r10") guest_start_tsc,
306+
);
307+
}
308+
309+
self.clean();
310+
}
311+
312+
/// Closes the trace by ending all spans
313+
/// NOTE: This expects an outb call to send the spans to the host.
314+
fn end_trace(&mut self) {
315+
for span in self.spans.iter_mut() {
316+
if span.end_tsc.is_none() {
317+
span.end_tsc = Some(invariant_tsc::read_tsc());
318+
}
319+
}
320+
321+
// Empty the stack
322+
while self.stack.pop().is_some() {
323+
// Pop all remaining spans from the stack
324+
}
325+
326+
// Mark for clearing when re-entering the VM because we might
327+
// not enter on the same place as we exited (e.g. halt)
328+
self.cleanup_needed = true;
329+
}
330+
331+
/// Returns information about the information needed by the host to read the spans.
332+
pub fn guest_trace_info(&mut self) -> TraceBatchInfo {
333+
self.cleanup_needed = true;
334+
TraceBatchInfo {
335+
guest_start_tsc: self.guest_start_tsc,
336+
spans_ptr: self.spans.as_ptr() as u64,
337+
}
249338
}
250339

251340
/// Create a new span and push it on the stack
252341
pub fn new_span(&mut self, attrs: &Attributes) -> Id {
342+
self.verify_and_clean();
253343
let (idn, id) = self.alloc_id();
254344

255345
let md = attrs.metadata();
@@ -293,6 +383,7 @@ mod trace {
293383

294384
/// Record an event in the current span (top of the stack)
295385
pub fn event(&mut self, event: &Event<'_>) {
386+
self.verify_and_clean();
296387
let stack = &mut self.stack;
297388
let parent_id = stack.last().copied().unwrap_or(0);
298389

@@ -430,4 +521,29 @@ mod trace {
430521
// Set global dispatcher
431522
let _ = tracing_core::dispatcher::set_global_default(tracing_core::Dispatch::new(sub));
432523
}
524+
525+
/// Ends the current trace by ending all active spans in the
526+
/// internal state and storing the end timestamps.
527+
///
528+
/// This expects an outb call to send the spans to the host.
529+
/// After calling this function, the internal state is marked
530+
/// for cleaning on the next access.
531+
pub fn end_trace() {
532+
if let Some(w) = GUEST_STATE.get() {
533+
if let Some(state) = w.upgrade() {
534+
state.lock().end_trace();
535+
}
536+
}
537+
}
538+
539+
/// Returns information about the current trace state needed by the host to read the spans.
540+
pub fn guest_trace_info() -> Option<TraceBatchInfo> {
541+
let mut res = None;
542+
if let Some(w) = GUEST_STATE.get() {
543+
if let Some(state) = w.upgrade() {
544+
res = Some(state.lock().guest_trace_info());
545+
}
546+
}
547+
res
548+
}
433549
}

src/hyperlight_host/src/sandbox/outb.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ pub(crate) fn handle_outb(
181181
eprint!("{}", ch);
182182
Ok(())
183183
}
184+
#[cfg(feature = "trace_guest")]
185+
OutBAction::TraceBatch => Ok(()),
184186
#[cfg(feature = "mem_profile")]
185187
OutBAction::TraceMemoryAlloc => {
186188
let regs = _hv.read_regs()?;

0 commit comments

Comments
 (0)