Skip to content

Commit 92dad03

Browse files
cshungCopilot
andcommitted
feat(gdb): share ARM64 debugger support across backends
Architecture adapters, software breakpoints, page-sized memory access and recovery handling are shared by backend layers. Guest debug vectors and common tests support ARM64. Backend layers control platform enablement. The commit-layer document defines dependencies and shared-update validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com>
1 parent 938f7e1 commit 92dad03

26 files changed

Lines changed: 1875 additions & 561 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
77
### Added
88
* ARM64 Linux guests can run on MSHV.
99

10+
### Fixed
11+
* GDB memory reads and writes translate each guest page on hosts with larger pages.
12+
* ARM64 GDB snapshot recovery handles cancellation and crash detach. Snapshot checkpointing rejects software breakpoints before replacing memory.
13+
1014
## [v0.19.0] - 2026-11-04
1115

1216
### Added

‎Justfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ test-no-surrogate target=default-target:
335335
# runs tests that exercise gdb debugging
336336
test-rust-gdb-debugging target=default-target features="":
337337
{{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }}
338-
{{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb::
338+
{{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb:: hypervisor::hyperlight_vm::debug::tests:: gdb_recovery_tests::
339339

340340
# rust test for crashdump
341341
test-rust-crashdump target=default-target features="":

‎docs/gdb-commit-layers.md‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# GDB commit layers
2+
3+
Each layer is one signed commit. Backend layers depend on shared GDB support,
4+
not on another backend's debugger.
5+
6+
```text
7+
main
8+
|-- A: shared GDB support
9+
| |-- B: WHP ARM64 GDB
10+
| `-- C: KVM ARM64 GDB
11+
`-- D: ARM64 MSHV backend and CI
12+
`-- E: cherry-pick of A
13+
`-- F: MSHV ARM64 GDB
14+
```
15+
16+
| Layer | Branch | Parent | Contents |
17+
|---|---|---|---|
18+
| A | `cshung/gdb-shared` | `main` | Architecture adapters, protocol handling, software breakpoints, page-sized memory access, guest debug vectors, snapshot recovery, common tests |
19+
| B | `cshung/whp-aarch64-gdb` | A | WHP register access, HVC exits, debug registers, cache synchronization, platform enablement and tests |
20+
| C | `cshung/kvm-aarch64-gdb` | A | KVM debug exits, debug registers, platform enablement and CI |
21+
| D | `cshung/mshv-aarch64` | `main` | MSHV VM implementation, register access, reset, runner configuration and bounded CI scheduling |
22+
| E | `cshung/mshv-aarch64-gdb-shared` | D | A's shared patch |
23+
| F | `cshung/mshv-aarch64-gdb` | E | MSHV HVC exits, exception context, debug registers, platform enablement and tests |
24+
25+
A retains the existing x86_64 GDB platform guard. B, C and F enable their ARM64
26+
backend. D and E provide an MSHV backend without ARM64 GDB enablement.
27+
F rejects KVM debugging until C is integrated.
28+
29+
## Updating layers
30+
31+
* Update A for shared behavior. Rebuild B and C on A, cherry-pick A onto D as E,
32+
then rebuild F on E. Validate all three backend tips before publication.
33+
* Update B or C for a backend-only debugger change. The sibling branch is independent.
34+
* Update D for MSHV backend or CI changes. Rebuild E and F on D.
35+
* Update F for an MSHV-only debugger change.
36+
* Keep A and E patch-equivalent. E's parent and commit ID differ from A's.
37+
* Squash each layer to one commit. Keep historical tips in local archive branches
38+
before rewriting published refs. Push with an explicit lease on each old remote tip.
39+
40+
Run formatting and Clippy on each affected tip. Rebuild guests for guest changes.
41+
Run the common debugger tests, backend tests and real GDB client tests on WHP,
42+
KVM and MSHV for shared updates. Validate both the default Linux driver selection
43+
and the backend-only feature configuration. A history-only rewrite can reuse
44+
runtime results when the corresponding source files are identical.
45+
46+
## Pull requests
47+
48+
A has a shared-support PR. B uses #1907, C uses #2016, and D uses #1626.
49+
F has an MSHV GDB PR. E is a cherry-pick, not a second shared-support PR.
50+
51+
The branches live in `cshung/hyperlight`. Native GitHub stacks require head
52+
branches in the upstream repository. These fork PRs target upstream `main` and
53+
link their prerequisites and backend-only comparisons in their descriptions.
54+
Their full PR diffs include unmerged prerequisites.

‎src/hyperlight_common/src/arch/aarch64/exn.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
use crate::vmem::bits;
55

6+
/// HVC immediate for forwarding guest exceptions to the Hyper-V debugger.
7+
pub const DEBUG_EXCEPTION_HVC: u16 = 0x485a;
8+
69
const ESR_EC_UNKNOWN: u64 = 0b000000; // udf generates this
710
const ESR_EC_DATA_ABORT_LOWER_EL: u64 = 0b100100;
811
const ESR_EC_DATA_ABORT_SAME_EL: u64 = 0b100101;

‎src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,24 @@ context_restore:\n
104104
q_off = const offset_of!(ExceptionContext, q),
105105
);
106106

107+
// Only synchronous exceptions have a syndrome for debug forwarding.
108+
// The host resumes debug events at ERET and other synchronous exceptions at the prologue.
107109
macro_rules! vbar_entry {
108110
($et:literal, $ef:literal) => {
109111
concat!(
110112
"
113+
.if {ExceptionType_",
114+
$et,
115+
"} == {ExceptionType_Synchronous}\n
116+
stp x0, x1, [sp, #-16]!\n
117+
mrs x0, mdscr_el1\n
118+
tbz x0, #13, 1f\n
119+
ldp x0, x1, [sp], #16\n
120+
hvc #{debug_hvc}\n
121+
eret\n
122+
1:\n
123+
ldp x0, x1, [sp], #16\n
124+
.endif\n
111125
sub sp, sp, #{ctx_size}\n
112126
str x30, [sp, #{x30_off}]\n
113127
bl context_save\n
@@ -129,6 +143,8 @@ macro_rules! vbar_entry {
129143
global_asm!("
130144
.balign 0x800\n
131145
.global vbar\n
146+
.global hyperlight_debug_vectors\n
147+
hyperlight_debug_vectors:\n
132148
vbar:\n",
133149
vbar_entry!("Synchronous", "CurrentSP0"),
134150
vbar_entry!("IRQ", "CurrentSP0"),
@@ -148,6 +164,7 @@ vbar:\n",
148164
vbar_entry!("SError", "LowerAArch32"),
149165
ctx_size = const size_of::<ExceptionContext>(),
150166
x30_off = const offset_of!(ExceptionContext, x) + 15 * 0x010,
167+
debug_hvc = const hyperlight_common::arch::exn::DEBUG_EXCEPTION_HVC,
151168
handler = sym super::handle::handle_exception,
152169
ExceptionType_Synchronous = const ExceptionType::Synchronous as u64,
153170
ExceptionType_IRQ = const ExceptionType::Irq as u64,

‎src/hyperlight_host/examples/guest-debugging/main.rs‎

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ fn main() -> hyperlight_host::Result<()> {
3434
.host_function("Sleep5Secs", sleep_5_secs)
3535
.build()?;
3636

37-
// Call guest function
37+
#[cfg(target_arch = "x86_64")]
3838
multi_use_sandbox_dbg
3939
.call::<()>("UseSSE2Registers", ())
4040
.unwrap();
41+
#[cfg(target_arch = "aarch64")]
42+
multi_use_sandbox_dbg.call::<()>("NoOp", ()).unwrap();
4143

4244
let message =
4345
"Hello, World! I am executing inside of a VM with debugger attached :)\n".to_string();
@@ -291,6 +293,205 @@ mod tests {
291293
}
292294

293295
#[test]
296+
#[cfg(target_arch = "aarch64")]
297+
#[serial]
298+
fn test_gdb_bootstrap_step_preserves_live_stack() {
299+
let (out_file_path, cmd_file_path, manifest_dir) =
300+
gdb_test_paths("gdb-aarch64-bootstrap-stack");
301+
let cmd = format!(
302+
"set osabi none
303+
file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
304+
target remote :8080
305+
set pagination off
306+
set logging file {out_file_path}
307+
set logging enabled on
308+
set language c
309+
set $entry = $pc
310+
set $instruction = *(unsigned int *)$entry
311+
set *(unsigned int *)$entry = 0xd503201f
312+
set $bootstrap_sp = $sp
313+
set $canary = $sp - 16
314+
set $saved = *(unsigned long long *)$canary
315+
set *(unsigned long long *)$canary = 0x123456789abcdef0
316+
set $sp = $sp - 32
317+
stepi
318+
if *(unsigned long long *)$canary != 0x123456789abcdef0
319+
echo Bootstrap debug step overwrote the live stack\\n
320+
quit 1
321+
end
322+
set *(unsigned long long *)$canary = $saved
323+
set $sp = $bootstrap_sp
324+
set *(unsigned int *)$entry = $instruction
325+
set $pc = $entry
326+
echo Bootstrap debug step preserved the live stack\\n
327+
set logging enabled off
328+
detach
329+
quit
330+
"
331+
);
332+
let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, |contents| {
333+
contents.contains("Bootstrap debug step preserved the live stack")
334+
});
335+
cleanup(&out_file_path, &cmd_file_path);
336+
assert!(result.is_ok(), "{}", result.unwrap_err());
337+
}
338+
339+
#[test]
340+
#[cfg(target_arch = "aarch64")]
341+
#[serial]
342+
fn test_gdb_continue_over_sw_breakpoint() {
343+
let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-aarch64-continue");
344+
let cmd = format!(
345+
"file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
346+
target remote :8080
347+
set pagination off
348+
set logging file {out_file_path}
349+
set logging enabled on
350+
break simpleguest::no_op
351+
commands 1
352+
continue
353+
end
354+
break simpleguest::print_output
355+
commands 2
356+
echo Continued over ARM64 software breakpoint\\n
357+
set logging enabled off
358+
detach
359+
quit
360+
end
361+
continue
362+
"
363+
);
364+
#[cfg(windows)]
365+
let cmd = format!("set osabi none\n{cmd}");
366+
let checker =
367+
|contents: String| contents.contains("Continued over ARM64 software breakpoint");
368+
let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker);
369+
370+
cleanup(&out_file_path, &cmd_file_path);
371+
assert!(result.is_ok(), "{}", result.unwrap_err());
372+
}
373+
374+
#[test]
375+
#[cfg(target_arch = "aarch64")]
376+
#[serial]
377+
fn test_gdb_hardware_breakpoint_and_step() {
378+
let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-aarch64-hardware");
379+
let cmd = format!(
380+
"set osabi none
381+
file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
382+
target remote :8080
383+
set pagination off
384+
set logging file {out_file_path}
385+
set logging enabled on
386+
set $entry_pc = $pc
387+
stepi
388+
if $pc != $entry_pc + 4
389+
echo Entry step did not execute one instruction\\n
390+
quit 1
391+
end
392+
hbreak simpleguest::no_op
393+
continue
394+
set $return_pc = $x30
395+
stepi
396+
if $pc != $return_pc
397+
echo Hardware RET step missed its destination\\n
398+
quit 1
399+
end
400+
echo Hardware breakpoint and entry/RET stepping passed\\n
401+
set logging enabled off
402+
detach
403+
quit
404+
"
405+
);
406+
let checker = |contents: String| {
407+
contents.contains("Hardware breakpoint and entry/RET stepping passed")
408+
};
409+
let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker);
410+
cleanup(&out_file_path, &cmd_file_path);
411+
assert!(result.is_ok(), "{}", result.unwrap_err());
412+
}
413+
414+
#[test]
415+
#[cfg(target_arch = "aarch64")]
416+
#[serial]
417+
fn test_gdb_code_writes_and_self_loop() {
418+
let (out_file_path, cmd_file_path, manifest_dir) =
419+
gdb_test_paths("gdb-aarch64-code-writes");
420+
let cmd = format!(
421+
"set osabi none
422+
file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
423+
target remote :8080
424+
set pagination off
425+
set logging file {out_file_path}
426+
set logging enabled on
427+
hbreak simpleguest::no_op
428+
continue
429+
delete 1
430+
set language c
431+
set $loop_pc = $pc
432+
set $instruction = *(unsigned int *)$pc
433+
set *(unsigned int *)$pc = 0x14000000
434+
stepi
435+
if $pc != $loop_pc
436+
echo Self-loop step missed its destination\\n
437+
quit 1
438+
end
439+
set *(unsigned int *)$pc = $instruction
440+
set $return_pc = $x30
441+
stepi
442+
if $pc != $return_pc
443+
echo Restored RET missed its destination\\n
444+
quit 1
445+
end
446+
echo Code writes and self-loop stepping passed\\n
447+
set logging enabled off
448+
detach
449+
quit
450+
"
451+
);
452+
let checker =
453+
|contents: String| contents.contains("Code writes and self-loop stepping passed");
454+
let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker);
455+
cleanup(&out_file_path, &cmd_file_path);
456+
assert!(result.is_ok(), "{}", result.unwrap_err());
457+
}
458+
459+
#[test]
460+
#[cfg(target_arch = "aarch64")]
461+
#[serial]
462+
fn test_gdb_single_step() {
463+
let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-aarch64-step");
464+
let cmd = format!(
465+
"file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
466+
target remote :8080
467+
set pagination off
468+
set logging file {out_file_path}
469+
set logging enabled on
470+
break simpleguest::no_op
471+
stepi
472+
echo Stepped over ARM64 linear instruction\\n
473+
continue
474+
stepi
475+
echo Stepped over ARM64 RET\\n
476+
set logging enabled off
477+
detach
478+
quit
479+
"
480+
);
481+
#[cfg(windows)]
482+
let cmd = format!("set osabi none\n{cmd}");
483+
let checker = |contents: String| {
484+
contents.contains("Stepped over ARM64 linear instruction")
485+
&& contents.contains("Stepped over ARM64 RET")
486+
};
487+
let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker);
488+
489+
cleanup(&out_file_path, &cmd_file_path);
490+
assert!(result.is_ok(), "{}", result.unwrap_err());
491+
}
492+
493+
#[test]
494+
#[cfg(target_arch = "x86_64")]
294495
#[serial]
295496
fn test_gdb_sse_check() {
296497
let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-sse");

0 commit comments

Comments
 (0)