diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..23eef84 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + - os: macos-latest + target: aarch64-apple-darwin + - os: windows-latest + target: x86_64-pc-windows-msvc + - os: windows-11-arm + target: aarch64-pc-windows-msvc + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Build + run: cargo build --target ${{ matrix.target }} + + - name: Run tests + run: cargo test --target ${{ matrix.target }} diff --git a/Cargo.lock b/Cargo.lock index 22dbb68..dcf101e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -370,6 +379,15 @@ dependencies = [ "adler2", ] +[[package]] +name = "msvc-demangler" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4c25a3bb7d880e8eceab4822f3141ad0700d20f025991c1f03bd3d00219a5fc" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -739,6 +757,8 @@ version = "12.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42bcacd080282a72e795864660b148392af7babd75691d5ae9a3b77e29c98c77" dependencies = [ + "cpp_demangle", + "msvc-demangler", "rustc-demangle", "symbolic-common", ] diff --git a/src/lib.rs b/src/lib.rs index f9e05c6..eea0821 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,10 +2,10 @@ pub mod vlq; -#[cfg(all(target_os = "windows", target_arch = "x86_64"))] +#[cfg(all(target_os = "windows", any(target_arch = "x86_64", target_arch = "aarch64")))] mod win64; -#[cfg(all(target_os = "windows", target_arch = "x86_64"))] +#[cfg(all(target_os = "windows", any(target_arch = "x86_64", target_arch = "aarch64")))] pub use win64::trace; #[cfg(unix)] diff --git a/src/win64.rs b/src/win64.rs index 877c4dc..7d96c5d 100644 --- a/src/win64.rs +++ b/src/win64.rs @@ -1,6 +1,6 @@ // Copyright 2025 Divy Srivastava // -// https://github.com/littledivy/resym/blob/59db8fce68e8cf786b22dd37979afec70ddc363d/src/win64.rs +// Windows x86_64 and aarch64 stack unwinding support using RtlVirtualUnwind. use crate::vlq::vlq_encode; @@ -10,12 +10,14 @@ type DWORDLONG = u64; type HMODULE = *mut u8; type BOOL = i32; +#[cfg(target_arch = "x86_64")] #[repr(C)] struct M128A { low: u64, high: i64, } +#[cfg(target_arch = "x86_64")] #[repr(C, align(16))] struct CONTEXT { P1Home: DWORDLONG, @@ -73,6 +75,87 @@ struct CONTEXT { LastExceptionToRip: DWORDLONG, LastExceptionFromRip: DWORDLONG, } + +#[cfg(target_arch = "x86_64")] +impl CONTEXT { + #[inline(always)] + fn ip(&self) -> DWORDLONG { + self.Rip + } + + #[inline(always)] + fn sp(&self) -> DWORDLONG { + self.Rsp + } +} + +#[cfg(target_arch = "aarch64")] +#[repr(C)] +struct ARM64_NT_NEON128 { + low: u64, + high: i64, +} + +#[cfg(target_arch = "aarch64")] +#[repr(C, align(16))] +struct CONTEXT { + ContextFlags: DWORD, + Cpsr: DWORD, + X0: DWORDLONG, + X1: DWORDLONG, + X2: DWORDLONG, + X3: DWORDLONG, + X4: DWORDLONG, + X5: DWORDLONG, + X6: DWORDLONG, + X7: DWORDLONG, + X8: DWORDLONG, + X9: DWORDLONG, + X10: DWORDLONG, + X11: DWORDLONG, + X12: DWORDLONG, + X13: DWORDLONG, + X14: DWORDLONG, + X15: DWORDLONG, + X16: DWORDLONG, + X17: DWORDLONG, + X18: DWORDLONG, + X19: DWORDLONG, + X20: DWORDLONG, + X21: DWORDLONG, + X22: DWORDLONG, + X23: DWORDLONG, + X24: DWORDLONG, + X25: DWORDLONG, + X26: DWORDLONG, + X27: DWORDLONG, + X28: DWORDLONG, + Fp: DWORDLONG, + Lr: DWORDLONG, + Sp: DWORDLONG, + Pc: DWORDLONG, + V: [ARM64_NT_NEON128; 32], + Fpcr: DWORD, + Fpsr: DWORD, + Bcr: [DWORD; 8], + Bvr: [DWORDLONG; 8], + Wcr: [DWORD; 2], + Wvr: [DWORDLONG; 2], +} + +#[cfg(target_arch = "aarch64")] +impl CONTEXT { + #[inline(always)] + fn ip(&self) -> DWORDLONG { + self.Pc + } + + #[inline(always)] + fn sp(&self) -> DWORDLONG { + self.Sp + } +} + extern "system" { fn GetModuleHandleExW( dwFlags: DWORD, @@ -105,7 +188,8 @@ pub fn trace() -> String { RtlCaptureContext(&mut context); loop { - let ip = context.Rip; + let ip = context.ip(); + let sp = context.sp(); let mut base = 0; let fn_entry = RtlLookupFunctionEntry(ip, &mut base, std::ptr::null_mut()); @@ -137,7 +221,12 @@ pub fn trace() -> String { &mut est_frame, std::ptr::null_mut(), ); - if context.Rip == 0 { + + // RtlVirtualUnwind indicates end of stack differently per arch: + // - x86_64: sets instruction pointer to 0 + // - aarch64: leaves context unchanged (check if ip and sp are same) + let new_ip = context.ip(); + if new_ip == 0 || (new_ip == ip && context.sp() == sp) { break; } }