Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
runDefers := p.returnNeedsImplicitRunDefers(v)
if runDefers {
p.recordPanicLocation(b, v.Pos())
p.emitPCLineLabel(b, p.deferRunPos(v.Pos()))
b.RunDefers()
}
var results []llssa.Expr
Expand Down Expand Up @@ -1746,10 +1747,16 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
p.call(b, llssa.Go, &v.Call)
case *ssa.RunDefers:
p.recordPanicLocation(b, v.Pos())
p.emitPCLineLabel(b, p.deferRunPos(v.Pos()))
b.RunDefers()
case *ssa.Panic:
arg := p.compileValue(b, v.X)
p.recordPanicLocation(b, v.Pos())
// panic is not a Call instruction, so callEx's statement anchor
// does not cover it; the panic snapshot attributes the panicking
// frame to this pc (issue5856 wants the panic line, not the
// nearest call's).
p.emitPCLineLabel(b, v.Pos())
b.Panic(arg)
case *ssa.Send:
ch := p.compileValue(b, v.Chan)
Expand Down Expand Up @@ -1909,6 +1916,25 @@ func (p *context) functionHasExplicitStackDeferSeen(fn *ssa.Function, seen map[*
return false
}

// deferRunPos is where gc attributes a deferred function's caller frame:
// the function's closing brace — defers run at function exit, not at the
// defer statement (goroot issue14646, issue5856).
func (p *context) deferRunPos(fallback token.Pos) token.Pos {
if p.goFn != nil {
switch syntax := p.goFn.Syntax().(type) {
case *ast.FuncDecl:
if syntax.Body != nil && syntax.Body.Rbrace.IsValid() {
return syntax.Body.Rbrace
}
case *ast.FuncLit:
if syntax.Body != nil && syntax.Body.Rbrace.IsValid() {
return syntax.Body.Rbrace
}
}
}
return fallback
}

func (p *context) returnNeedsImplicitRunDefers(ret *ssa.Return) bool {
fn := ret.Parent()
if fn == nil || fn.Synthetic != "" || ret.Block() == fn.Recover {
Expand Down
5 changes: 5 additions & 0 deletions internal/crosscompile/crosscompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le
"-target", targetTriple,
"-Qunused-arguments",
"-Wno-unused-command-line-argument",
// Keep frame pointers in C code too: the runtime's physical
// unwinder walks fault-site chains through C frames (Go keeps
// them via the "frame-pointer"="non-leaf" attribute; x86-64 C
// would omit them at -O by default).
"-fno-omit-frame-pointer",
}
if ltoMode.Enabled() {
export.CCFLAGS = append(export.CCFLAGS, ltoMode.ClangFlag())
Expand Down
11 changes: 0 additions & 11 deletions runtime/internal/lib/runtime/_wrap/runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,3 @@ int llgo_maxprocs()
return 1;
#endif
}

__attribute__((noinline)) void *llgo_framepointer(void)
{
#if defined(__GNUC__) || defined(__clang__)
/* Read the saved caller FP before this helper's frame becomes invalid. */
void **frame = (void **)__builtin_frame_address(0);
return frame ? *frame : 0;
#else
return 0;
#endif
}
4 changes: 2 additions & 2 deletions runtime/internal/lib/runtime/extern.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
ensureRuntimePCLN()
if fpUnwindAvailable() {
var pcs [1]uintptr
if fpCallers(skip+1, pcs[:]) >= 1 {
if callersWithPanicSplice(skip+1, pcs[:]) >= 1 {
// Caller returns the call-instruction PC (matching Go), whereas
// Callers returns return PCs. Keeping the adjusted value matters
// when the return address equals the next function's entry.
Expand All @@ -53,7 +53,7 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
func Callers(skip int, pc []uintptr) int {
ensureRuntimePCLN()
if fpUnwindAvailable() {
if n := fpCallers(skip, pc); n > 0 {
if n := callersWithPanicSplice(skip, pc); n > 0 {
return n
}
}
Expand Down
1 change: 1 addition & 0 deletions runtime/internal/lib/runtime/fault_unwind_llgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func onFault(pc, fp uintptr, sig int32) {
n += m
}
faultN = int32(n)
rtdebug.StoreFaultPCs(faultPCs[:n])
}
// Capture done: re-arm the recursion guard before this fault turns
// into an ordinary (recoverable) panic.
Expand Down
11 changes: 11 additions & 0 deletions runtime/internal/lib/runtime/symtab.go
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,17 @@ func refinePCSymbolLine(sym pcSymbol, pc uintptr) pcSymbol {
lineSym.pc = pc
return mergePCLineSymbol(sym, lineSym)
}
// No same-function statement anchor covers pc. Mid-function pcs of a
// record with no evidence can actually belong to a foreign (C)
// function linked between Go functions — nearest-below cannot see the
// hole and would misattribute the frame to the preceding Go function.
// One dladdr cross-check on this cold, cache-backed path: a closer
// symbol wins. Exact entries and anchored functions never get here.
if pc != sym.entry {
if ai := addrInfoSymbol(pc); ai.ok && ai.entry > sym.entry {
return ai
}
}
return sym
}

Expand Down
15 changes: 15 additions & 0 deletions runtime/internal/lib/runtime/unwind_baremetal_llgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build baremetal && !wasm

package runtime

func fpCallers(_ int, _ []uintptr) int {
return 0
}

func fpUnwindAvailable() bool {
return false
}

func callersWithPanicSplice(_ int, _ []uintptr) int {
return 0
}
209 changes: 208 additions & 1 deletion runtime/internal/lib/runtime/unwind_llgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,209 @@ func c_framepointer() unsafe.Pointer
func init() {
rtdebug.PanicTraceback = panicTraceback
rtdebug.PanicRecovered = clearFaultTraceback
rtdebug.PanicPCSnapshot = capturePanicPCs
rtdebug.RecoverMark = recoverMark
}

// recoverMark records the recovering deferred frame (and one above, for
// wrapper-reached recover) so the panic snapshot stays spliceable while
// that frame is live. After siglongjmp the frame-pointer chain two levels
// up can point into a stale/reused stack region that is sometimes
// unmapped; probe each slot before dereferencing — an unguarded read here
// self-faults ~7% of the time, converting to a nil-deref panic that
// corrupts the value the recover was extracting (goroot reflectmake flake).
func recoverMark() {
// Record this function's frame address: it sits below the recovering
// deferred frame, and the liveness gate tests interval containment, so
// the exact level does not matter.
fp := callerFramePointer()
if fp == 0 {
return
}
rtdebug.MarkPanicRecoverFPs(fp, 0)
}

// capturePanicPCs runs at panic time, before any longjmp unwinding, and
// stores the physical pc chain for later splicing (see spliceCallers).
func capturePanicPCs() {
if !fpUnwindAvailable() {
return
}
var pcs [64]uintptr
n := fpCallers(0, pcs[:])
rtdebug.StorePanicPCs(pcs[:n])
}

// panicSplicePCs returns the snapshot when it is observable: either the
// panic is still in flight, or the deferred frame that recovered it is
// still live on the physical chain (gc keeps panic frames on the stack
// exactly that long).
func panicSplicePCs() []uintptr {
pcs := rtdebug.PanicPCs()
if len(pcs) == 0 {
return nil
}
if rtdebug.PanicActive() {
return pcs
}
mark, _ := rtdebug.PanicRecoverFPs()
if mark == 0 {
return nil
}
// The mark is a frame address recorded inside Recover's call chain;
// the recovering deferred frame is live iff the mark still lies
// within the current chain's span. Interval containment instead of
// exact equality: the hook's own frame depth differs across
// platforms (stub/wrapper layers), but any frame's [fp, parent fp)
// range straddling the mark proves the region is still stack, not
// reused heap or a dead extent.
fp := callerFramePointer()
for i := 0; fp != 0 && i < maxPanicSpliceFrames; i++ {
if !memReadable(fp) {
break
}
prev := *(*uintptr)(unsafe.Pointer(fp))
if fp <= mark && (prev > mark || prev == 0) {
return pcs
}
if prev <= fp || prev-fp > maxFPStride || prev&(unsafe.Sizeof(uintptr(0))-1) != 0 {
break
}
fp = prev
}
return nil
}

const maxPanicSpliceFrames = 4096

// callerFramePointer returns the frame of its Go caller. llgo_framepointer
// returns this helper's frame; consume its saved link immediately, before
// another call can reuse the slot.
//
//go:noinline
func callerFramePointer() uintptr {
fp := uintptr(c_framepointer())
if fp == 0 {
return 0
}
return *(*uintptr)(unsafe.Pointer(fp))
}

// trimPlumbingPCs drops leading pcs attributed to the LLGo runtime core
// (panic machinery, the capture path) and cuts the tail at the first pc
// outside the program text — fault snapshots are captured without the
// text bound (see fpWalkFrom).
func trimPlumbingPCs(pcs []uintptr) []uintptr {
initRuntimeFuncPCFrames()
head := 0
for head < len(pcs) {
sym := frameSymbol(pcs[head] - 1)
if sym.function != "" && (hasPrefix(sym.function, "github.com/goplus/llgo/runtime/internal/") ||
sym.function == "runtime.capturePanicPCs" || sym.function == "runtime.onFault" ||
sym.function == "runtime.fpWalkFrom") {
head++
continue
}
break
}
// Keep the innermost panic-machinery frame: gc's logical stack has
// runtime.gopanic between the deferred function and the panic site,
// and fixed Caller depths (issue5856's Caller(2)) count it. Fault
// snapshots start at the fault pc and trim nothing — gc's walkers
// skip runtime frames by name there, not by depth.
if head > 0 {
head--
}
pcs = pcs[head:]
if rtdebug.PanicPCsAreFault() {
// Fault snapshots come from a genuine interrupted context and the
// chain-discipline guards already bounded the walk; keep unnamed
// frames (linux dladdr cannot name non-dynamic C symbols — they
// display as raw pcs, like gc does for unknown frames).
return pcs
}
for i := 0; i < len(pcs); i++ {
if prebuiltTextContains(pcs[i]) {
continue
}
// Outside the Go text range: C frames in this binary (and its
// libraries) still resolve to a symbol via dladdr — keep those;
// cut at the first pc nothing can name (wild slots past the last
// FP-disciplined frame).
if frameSymbol(pcs[i]-1).function == "" {
return pcs[:i]
}
}
return pcs
}

// spliceCallers rebuilds the caller view a deferred function should see
// during (or right after recovering) a panic: its own live frames, then the
// panic-site chain from the snapshot. The junction is the first live frame
// whose function also appears in the snapshot — the defer owner; the
// snapshot side wins there because the live copy\'s pc points at the
// longjmp resume site, not at the call that panicked.
func spliceCallers(cur []uintptr) []uintptr {
snap := panicSplicePCs()
if len(snap) == 0 {
return cur
}
snap = trimPlumbingPCs(snap)
if len(snap) == 0 {
return cur
}
// The junction is the first live frame whose function also appears in
// the snapshot — the defer owner (or the panicking function itself when
// defer and panic share a frame). Everything from there down is
// replaced by the whole snapshot: it already contains the owner and its
// callers, with the owner's pc on the panic path instead of the longjmp
// resume site.
for i := 0; i < len(cur); i++ {
entry := frameSymbol(cur[i] - 1).entry
if entry == 0 {
continue
}
for j := 0; j < len(snap); j++ {
if frameSymbol(snap[j]-1).entry == entry {
out := make([]uintptr, 0, i+len(snap))
out = append(out, cur[:i]...)
out = append(out, snap...)
return out
}
}
}
return cur
}

// callersWithPanicSplice is Callers with panic-frame splicing. With no
// snapshot stored (the overwhelmingly common case) it degrades to the
// plain walk at the cost of one TLS load. Otherwise the raw walk runs
// unskipped so splicing sees the junction frame, then the requested skip
// applies to the spliced view (matching gc, whose skip counts the logical
// panic-inclusive stack).
//
//go:noinline
func callersWithPanicSplice(skip int, pc []uintptr) int {
if len(pc) == 0 {
return 0
}
if len(rtdebug.PanicPCs()) == 0 {
// One frame deeper than the extern.go call sites used to be.
return fpCallers(skip+1, pc)
}
var raw [128]uintptr
n := fpCallers(1, raw[:])
if n <= 0 {
return 0
}
view := spliceCallers(raw[:n])
if skip < 0 {
skip = 0
}
if skip >= len(view) {
return 0
}
return copy(pc, view[skip:])
}

func hasPrefix(s, prefix string) bool {
Expand Down Expand Up @@ -52,8 +255,12 @@ func panicTraceback(skip int) bool {
if n <= 0 {
return false
}
// A stored panic snapshot (Go panic or hardware fault, including
// faults inside C code) carries the frames the longjmp unwinding
// already removed; splice them in like Callers does.
view := spliceCallers(pcs[:n])
print("goroutine 1 [running]:\n")
frames := CallersFrames(pcs[:n])
frames := CallersFrames(view)
skippingPlumbing := true
for {
frame, more := frames.Next()
Expand Down
6 changes: 6 additions & 0 deletions runtime/internal/lib/runtime/unwind_wasm_llgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ func fpCallers(skip int, pc []uintptr) int {
func fpUnwindAvailable() bool {
return false
}

// callersWithPanicSplice is unreachable while fpUnwindAvailable is false,
// but it must remain in the selected source set for extern.go to type-check.
func callersWithPanicSplice(_ int, _ []uintptr) int {
return 0
}
13 changes: 13 additions & 0 deletions runtime/internal/runtime/_wrap/fp.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* llgo_framepointer lives in the runtime core (not the public runtime
* package): Recover() records the recovering frame through it, and
* programs that never import "runtime" still link the core. */
__attribute__((noinline)) void *llgo_framepointer(void)
{
#if defined(__GNUC__) || defined(__clang__)
/* Read the saved caller FP before this helper's frame becomes invalid. */
void **frame = (void **)__builtin_frame_address(0);
return frame ? *frame : 0;
#else
return 0;
#endif
}
Loading
Loading