From 10710398741734d93992abb643e200cd56e94bee Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Thu, 23 Jul 2026 10:34:07 -0700 Subject: [PATCH 1/4] proc: fix arm64 crosscall2 SP restore for cgo stacktraces crosscall2 does SUB $(8*24), RSP but never spills SP. Linux/arm64 was restoring SP from the saved frame pointer, which only works when C frames keep a frame pointer; with newer GCC (Ubuntu 24.04 CI) that omits them, TestCgoStacktrace skips C frames after a cgo callback. Loading *(SP+8*24) is also wrong (that slot is the caller's stack contents). Restore the caller SP as SP+8*24, matching what crosscall2's epilogue adds back. --- pkg/proc/arm64_arch.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/proc/arm64_arch.go b/pkg/proc/arm64_arch.go index 29834407eb..27c01ded31 100644 --- a/pkg/proc/arm64_arch.go +++ b/pkg/proc/arm64_arch.go @@ -238,7 +238,7 @@ func arm64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) bool return true case "crosscall2": - // The offsets get from runtime/cgo/asm_arm64.s:10 + // The offsets get from runtime/cgo/asm_arm64.s bpoff := uint64(14) lroff := uint64(15) if producer := it.bi.Producer(); producer != "" && goversion.ProducerAfterOrEqual(producer, 1, 19) { @@ -246,7 +246,10 @@ func arm64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) bool bpoff = 22 lroff = 23 } - newsp, _ := readUintRaw(it.mem, it.regs.SP()+8*24, int64(it.bi.Arch.PtrSize())) + // crosscall2 does SUB $(8*24), RSP but does not spill SP. The caller's SP is + // therefore current SP + 8*24, not a value loaded from that address (which is + // the caller's stack contents). + newsp := it.regs.SP() + 8*24 newbp, _ := readUintRaw(it.mem, it.regs.SP()+8*bpoff, int64(it.bi.Arch.PtrSize())) newlr, _ := readUintRaw(it.mem, it.regs.SP()+8*lroff, int64(it.bi.Arch.PtrSize())) if it.regs.Reg(it.regs.BPRegNum) != nil { @@ -256,11 +259,7 @@ func arm64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) bool it.regs.AddReg(it.regs.BPRegNum, reg) } it.regs.Reg(it.regs.LRRegNum).Uint64Val = newlr - if linux { - it.regs.Reg(it.regs.SPRegNum).Uint64Val = newbp - } else { - it.regs.Reg(it.regs.SPRegNum).Uint64Val = newsp - } + it.regs.Reg(it.regs.SPRegNum).Uint64Val = newsp it.pc = newlr return true case "runtime.mstart": From f54d9fb1e69ecaeaf7b88faf0eb7de0bb5d220a2 Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Thu, 23 Jul 2026 10:44:57 -0700 Subject: [PATCH 2/4] proc: compute crosscall2 caller SP on riscv64, loong64, ppc64le Apply the same SP+framesize restore used on arm64: crosscall2 adjusts SP but does not spill it, so loading from SP+N reads caller stack contents. Also correct ppc64le BP/LR slot offsets from the host ABI save macros, and re-enable TestCgoStacktrace on linux/ppc64le. --- Documentation/backend_test_health.md | 3 +-- pkg/proc/loong64_arch.go | 6 ++++-- pkg/proc/ppc64le_arch.go | 18 +++++++++++++----- pkg/proc/proc_test.go | 1 - pkg/proc/riscv64_arch.go | 4 +++- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md index d504e1cea0..7d95e97b00 100644 --- a/Documentation/backend_test_health.md +++ b/Documentation/backend_test_health.md @@ -25,8 +25,7 @@ Tests skipped by each supported backend: * 1 broken * linux/loong64 skipped = 1 * 1 not working on linux/loong64 -* linux/ppc64le skipped = 3 - * 1 broken - cgo stacktraces +* linux/ppc64le skipped = 2 * 2 not working on linux/ppc64le when -gcflags=-N -l is passed * linux/ppc64le/native skipped = 1 * 1 broken in linux ppc64le diff --git a/pkg/proc/loong64_arch.go b/pkg/proc/loong64_arch.go index b46c770237..1bf5ba2129 100644 --- a/pkg/proc/loong64_arch.go +++ b/pkg/proc/loong64_arch.go @@ -202,8 +202,10 @@ func loong64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) boo return true case "crosscall2": - // The offsets get from runtime/cgo/asm_loong64.s:25 - newsp, _ := readUintRaw(it.mem, it.regs.SP()+8*23, int64(it.bi.Arch.PtrSize())) + // The offsets get from runtime/cgo/asm_loong64.s + // crosscall2 does ADDV $(-23*8), R3 but does not spill SP. The caller's SP is + // therefore current SP + 23*8, not a value loaded from that address. + newsp := it.regs.SP() + 8*23 newbp, _ := readUintRaw(it.mem, it.regs.SP()+8*4, int64(it.bi.Arch.PtrSize())) newlr, _ := readUintRaw(it.mem, it.regs.SP()+8*22, int64(it.bi.Arch.PtrSize())) if it.regs.Reg(it.regs.BPRegNum) != nil { diff --git a/pkg/proc/ppc64le_arch.go b/pkg/proc/ppc64le_arch.go index 00ed4df5af..968dca7a53 100644 --- a/pkg/proc/ppc64le_arch.go +++ b/pkg/proc/ppc64le_arch.go @@ -129,14 +129,22 @@ func ppc64leSwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) boo it.atend = true return true case "crosscall2": - //The offsets get from runtime/cgo/asm_ppc64x.s:10 - newsp, _ := readUintRaw(it.mem, it.regs.SP()+8*24, int64(it.bi.Arch.PtrSize())) - newbp, _ := readUintRaw(it.mem, it.regs.SP()+8*14, int64(it.bi.Arch.PtrSize())) - newlr, _ := readUintRaw(it.mem, it.regs.SP()+16, int64(it.bi.Arch.PtrSize())) + // Frame size from STACK_AND_SAVE_HOST_TO_GO_ABI(32) in + // runtime/cgo/abi_ppc64x.h: extra(32) + FIXED_FRAME(32) + + // SAVE_GPR(18*8) + SAVE_FPR(18*8) + SAVE_VR(12*16). + const frameSize = 32 + 32 + 18*8 + 18*8 + 12*16 + newsp := it.regs.SP() + frameSize + // R31 (frame pointer) is the last GPR saved by SAVE_GPR at + // extra+FIXED_FRAME+8*17. + bpoff := uint64(32 + 32 + 8*17) + newbp, _ := readUintRaw(it.mem, it.regs.SP()+bpoff, int64(it.bi.Arch.PtrSize())) + // LR is saved into the caller's frame at 16(SP) before the stack + // allocation (host ELFv2 ABI). + newlr, _ := readUintRaw(it.mem, newsp+16, int64(it.bi.Arch.PtrSize())) if it.regs.Reg(it.regs.BPRegNum) != nil { it.regs.Reg(it.regs.BPRegNum).Uint64Val = newbp } else { - reg, _ := it.readRegisterAt(it.regs.BPRegNum, it.regs.SP()+8*14) + reg, _ := it.readRegisterAt(it.regs.BPRegNum, it.regs.SP()+bpoff) it.regs.AddReg(it.regs.BPRegNum, reg) } it.regs.Reg(it.regs.LRRegNum).Uint64Val = newlr diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 59c59d8e91..be248ed9b4 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -3128,7 +3128,6 @@ func TestCgoStacktrace(t *testing.T) { } skipOn(t, "broken - cgo stacktraces", "386") skipOn(t, "broken - cgo stacktraces", "windows", "arm64") - skipOn(t, "broken - cgo stacktraces", "linux", "ppc64le") protest.MustHaveCgo(t) // Tests that: diff --git a/pkg/proc/riscv64_arch.go b/pkg/proc/riscv64_arch.go index 5c25ca87cc..f36a9c1e0e 100644 --- a/pkg/proc/riscv64_arch.go +++ b/pkg/proc/riscv64_arch.go @@ -226,7 +226,9 @@ func riscv64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) boo case "crosscall2": // The offsets get from runtime/cgo/asm_riscv64.s - newsp, _ := readUintRaw(it.mem, it.regs.SP()+8*29, int64(it.bi.Arch.PtrSize())) + // crosscall2 does ADD $(-8*29), X2 but does not spill SP. The caller's SP is + // therefore current SP + 8*29, not a value loaded from that address. + newsp := it.regs.SP() + 8*29 newbp, _ := readUintRaw(it.mem, it.regs.SP()+8*4, int64(it.bi.Arch.PtrSize())) newlr, _ := readUintRaw(it.mem, it.regs.SP()+8*16, int64(it.bi.Arch.PtrSize())) if it.regs.Reg(it.regs.BPRegNum) != nil { From fddd94e9dfbe969ce653d2567d4482ec370221b8 Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Mon, 27 Jul 2026 10:22:25 -0700 Subject: [PATCH 3/4] proc: enable TestCgoStacktrace on windows/arm64 The arm64 crosscall2 SP restore fix applies to all GOOS via arm64_arch.go, so re-enable the previously skipped windows/arm64 cgo stacktrace test. --- Documentation/backend_test_health.md | 4 ++-- pkg/proc/proc_test.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md index 7d95e97b00..04dab86de5 100644 --- a/Documentation/backend_test_health.md +++ b/Documentation/backend_test_health.md @@ -51,6 +51,6 @@ Tests skipped by each supported backend: * 1 broken * 2 not working on windows * 6 see https://github.com/go-delve/delve/issues/2768 -* windows/arm64 skipped = 3 - * 2 broken - cgo stacktraces +* windows/arm64 skipped = 2 + * 1 broken - cgo stacktraces * 1 flaky diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index be248ed9b4..cc8d38384e 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -3127,7 +3127,6 @@ func TestCgoStacktrace(t *testing.T) { } } skipOn(t, "broken - cgo stacktraces", "386") - skipOn(t, "broken - cgo stacktraces", "windows", "arm64") protest.MustHaveCgo(t) // Tests that: From 2460f795aee5cf1feeec8495e0ba567cc55f2792 Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Mon, 27 Jul 2026 11:07:56 -0700 Subject: [PATCH 4/4] proc: restore TestCgoStacktrace skip on windows/arm64 C frames on windows/arm64 use PE .pdata/.xdata from clang, not DWARF .debug_frame. Delve does not unwind via .pdata yet, so step 1 fails inside C before asmcgocall or crosscall2. That is unrelated to the arm64 crosscall2 SP restore this PR fixes. --- Documentation/backend_test_health.md | 4 ++-- pkg/proc/proc_test.go | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md index 04dab86de5..7d95e97b00 100644 --- a/Documentation/backend_test_health.md +++ b/Documentation/backend_test_health.md @@ -51,6 +51,6 @@ Tests skipped by each supported backend: * 1 broken * 2 not working on windows * 6 see https://github.com/go-delve/delve/issues/2768 -* windows/arm64 skipped = 2 - * 1 broken - cgo stacktraces +* windows/arm64 skipped = 3 + * 2 broken - cgo stacktraces * 1 flaky diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index cc8d38384e..b5669cc6e2 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -3127,6 +3127,10 @@ func TestCgoStacktrace(t *testing.T) { } } skipOn(t, "broken - cgo stacktraces", "386") + // C frames on windows/arm64 use PE .pdata/.xdata (clang), not DWARF + // .debug_frame; Delve does not unwind via .pdata yet. Unrelated to the + // arm64 crosscall2 SP restore. + skipOn(t, "broken - cgo stacktraces", "windows", "arm64") protest.MustHaveCgo(t) // Tests that: