Repository: go-delve/delve

Issue:
TestCgoStacktrace* skipped on linux/loong64
On linux/loong64, the test cases TestCgoStacktrace and TestCgoStacktrace2 are currently skipped instead of passing.

Run the following command:
```
GOFLAGS="-tags=exp.linuxloong64" go test -v -count=1 -run=TestCgoStacktrace ./pkg/proc
```
Output:
```
=== RUN   TestCgoStacktrace
    proc_test.go:76: skipped on linux/loong64: broken - cgo stacktraces
--- SKIP: TestCgoStacktrace (0.00s)
=== RUN   TestCgoStacktrace2
    proc_test.go:76: skipped on loong64: broken
--- SKIP: TestCgoStacktrace2 (0.00s)
```
Notes:
- Since Go 1.19 and later, linux/loong64 fully supports cgo.
- Therefore, delve should also support cgo stacktraces on this platform, and these tests should not be skipped.

Discussion:

Solution patch:
diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md
index d20f75ef..8ac0d508 100644
--- a/Documentation/backend_test_health.md
+++ b/Documentation/backend_test_health.md
@@ -23,8 +23,7 @@ Tests skipped by each supported backend:
 	* 2 not working on linux/386
 * linux/386/pie skipped = 1
 	* 1 broken
-* linux/loong64 skipped = 2
-	* 1 broken - cgo stacktraces
+* linux/loong64 skipped = 1
 	* 1 not working on linux/loong64
 * linux/ppc64le skipped = 3
 	* 1 broken - cgo stacktraces
@@ -36,8 +35,8 @@ Tests skipped by each supported backend:
 * linux/riscv64 skipped = 2
 	* 1 broken - cgo stacktraces
 	* 1 not working on linux/riscv64
-* loong64 skipped = 9
-	* 2 broken
+* loong64 skipped = 8
+	* 1 broken
 	* 1 broken - global variable symbolication
 	* 6 not implemented
 * pie skipped = 2
diff --git a/pkg/proc/loong64_arch.go b/pkg/proc/loong64_arch.go
index dc2e5699..eb88f2f7 100644
--- a/pkg/proc/loong64_arch.go
+++ b/pkg/proc/loong64_arch.go
@@ -89,6 +89,8 @@ func loong64FixFrameUnwindContext(fctxt *frame.FrameContext, pc uint64, bi *Bina
 	return fctxt
 }
 
+const loong64cgocallSPOffsetSaveSlot = 0x8
+
 func loong64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) bool {
 	if it.frame.Current.Fn == nil {
 		if it.systemstack && it.g != nil && it.top {
@@ -101,26 +103,121 @@ func loong64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) boo
 		return false
 	}
 	switch it.frame.Current.Fn.Name {
-	case "runtime.goexit", "runtime.rt0_go", "runtime.mcall":
+	case "runtime.goexit", "runtime.rt0_go":
 		// Look for "top of stack" functions.
 		it.atend = true
 		return true
 
-	default:
-		if it.systemstack && it.top && it.g != nil && strings.HasPrefix(it.frame.Current.Fn.Name, "runtime.") && it.frame.Current.Fn.Name != "runtime.throw" && it.frame.Current.Fn.Name != "runtime.fatalthrow" {
-			// The runtime switches to the system stack in multiple places.
-			// This usually happens through a call to runtime.systemstack but there
-			// are functions that switch to the system stack manually (for example
-			// runtime.morestack).
-			// Since we are only interested in printing the system stack for cgo
-			// calls we switch directly to the goroutine stack if we detect that the
-			// function at the top of the stack is a runtime function.
-			if err := it.switchToGoroutineStack(); err != nil {
-				it.err = err
-				return false
-			}
+	case "runtime.mcall":
+		if it.systemstack && it.g != nil {
+			it.switchToGoroutineStack()
+			return true
+		}
+		it.atend = true
+		return true
+
+	case "runtime.asmcgocall":
+		if it.top || !it.systemstack {
+			return false
+		}
+		// This function is called by a goroutine to execute a C function and
+		// switches from the goroutine stack to the system stack.
+		// Since we are unwinding the stack from callee to caller we have to switch
+		// from the system stack to the goroutine stack.
+		oldsp := it.regs.SP()
+		off, _ := readIntRaw(it.mem, oldsp+loong64cgocallSPOffsetSaveSlot, int64(it.bi.Arch.PtrSize()))
+		newsp := uint64(int64(it.stackhi) - off)
+
+		// The runtime.asmcgocall prologue contains: addi.d $sp, $sp, -8,
+		// hence we require newsp + 8 at this point
+		it.regs.Reg(it.regs.SPRegNum).Uint64Val = newsp + 8
+
+		// runtime.asmcgocall can also be called from inside the system stack,
+		// in that case no stack switch actually happens
+		if it.regs.SP() == oldsp {
+			return false
+		}
+
+		it.top = false
+		it.systemstack = false
+		// The return value is stored in the LR register which is saved at -8(SP).
+		addrret := uint64(int64(it.regs.SP()) - int64(it.bi.Arch.PtrSize()))
+		it.frame.Ret, _ = readUintRaw(it.mem, addrret, int64(it.bi.Arch.PtrSize()))
+		it.pc = it.frame.Ret
+		return true
+
+	case "runtime.cgocallback_gofunc", "runtime.cgocallback":
+		// For a detailed description of how this works read the long comment at
+		// the start of $GOROOT/src/runtime/cgocall.go and the source code of
+		// runtime.cgocallback_gofunc in $GOROOT/src/runtime/asm_loong64.s
+		//
+		// When a C functions calls back into go it will eventually call into
+		// runtime.cgocallback_gofunc which is the function that does the stack
+		// switch from the system stack back into the goroutine stack
+		// Since we are going backwards on the stack here we see the transition
+		// as goroutine stack -> system stack.
+		if it.top || it.systemstack {
+			return false
+		}
+
+		it.loadG0SchedSP()
+		if it.g0_sched_sp <= 0 {
+			return false
+		}
+		// entering the system stack
+		it.regs.Reg(it.regs.SPRegNum).Uint64Val = it.g0_sched_sp
+		// reads the previous value of g0.sched.sp that runtime.cgocallback_gofunc saved on the stack
+		it.g0_sched_sp, _ = readUintRaw(it.mem, it.regs.SP()+loong64cgocallSPOffsetSaveSlot, int64(it.bi.Arch.PtrSize()))
+		it.top = false
+		callFrameRegs, ret, retaddr := it.advanceRegs()
+		frameOnSystemStack := it.newStackframe(ret, retaddr)
+		it.pc = frameOnSystemStack.Ret
+		it.regs = callFrameRegs
+		it.systemstack = true
+		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()))
+		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 {
+			it.regs.Reg(it.regs.BPRegNum).Uint64Val = newbp
+		} else {
+			reg, _ := it.readRegisterAt(it.regs.BPRegNum, it.regs.SP()+8*4)
+			it.regs.AddReg(it.regs.BPRegNum, reg)
+		}
+		it.regs.Reg(it.regs.LRRegNum).Uint64Val = newlr
+		it.regs.Reg(it.regs.SPRegNum).Uint64Val = newsp
+		it.pc = newlr
+		return true
+
+	case "runtime.mstart":
+		// Calls to runtime.systemstack will switch to the systemstack then:
+		// 1. alter the goroutine stack so that it looks like systemstack_switch
+		//    was called
+		// 2. alter the system stack so that it looks like the bottom-most frame
+		//    belongs to runtime.mstart
+		// If we find a runtime.mstart frame on the system stack of a goroutine
+		// parked on runtime.systemstack_switch we assume runtime.systemstack was
+		// called and continue tracing from the parked position.
+
+		if it.top || !it.systemstack || it.g == nil {
+			return false
+		}
+		if fn := it.bi.PCToFunc(it.g.PC); fn == nil || fn.Name != "runtime.systemstack_switch" {
+			return false
+		}
+
+		it.switchToGoroutineStack()
+		return true
+
+	case "runtime.newstack", "runtime.systemstack":
+		if it.systemstack && it.g != nil {
+			it.switchToGoroutineStack()
 			return true
 		}
+		return false
 	}
 
 	return false



Test patch:
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 364de772..ccad2b88 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -2961,7 +2961,6 @@ func TestCgoStacktrace(t *testing.T) {
 	skipOn(t, "broken - cgo stacktraces", "windows", "arm64")
 	skipOn(t, "broken - cgo stacktraces", "linux", "ppc64le")
 	skipOn(t, "broken - cgo stacktraces", "linux", "riscv64")
-	skipOn(t, "broken - cgo stacktraces", "linux", "loong64")
 	if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 21) {
 		skipOn(t, "broken - cgo stacktraces", "windows", "arm64")
 	}
@@ -4200,7 +4199,6 @@ func TestCgoStacktrace2(t *testing.T) {
 	skipOn(t, "broken - cgo stacktraces", "darwin", "arm64")
 	skipOn(t, "broken", "ppc64le")
 	skipOn(t, "broken", "riscv64")
-	skipOn(t, "broken", "loong64")
 	protest.MustHaveCgo(t)
 	// If a panic happens during cgo execution the stacktrace should show the C
 	// function that caused the problem.