blob: 083220f4492954948e18357587f3af523d7fe296 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// GC checkmarks
//
// In a concurrent garbage collector, one worries about failing to mark
// a live object due to mutations without write barriers or bugs in the
// collector implementation. As a sanity check, the GC has a 'checkmark'
// mode that retraverses the object graph with the world stopped, to make
// sure that everything that should be marked is marked.
package runtime
import (
"internal/goarch"
"internal/runtime/atomic"
"internal/runtime/sys"
"unsafe"
)
// A checkmarksMap stores the GC marks in "checkmarks" mode. It is a
// per-arena bitmap with a bit for every word in the arena. The mark
// is stored on the bit corresponding to the first word of the marked
// allocation.
type checkmarksMap struct {
_ sys.NotInHeap
b [heapArenaBytes / goarch.PtrSize / 8]uint8
}
// If useCheckmark is true, marking of an object uses the checkmark
// bits instead of the standard mark bits.
var useCheckmark = false
// startCheckmarks prepares for the checkmarks phase.
//
// The world must be stopped.
func startCheckmarks() {
assertWorldStopped()
// Clear all checkmarks.
clearCheckmarks := func(ai arenaIdx) {
arena := mheap_.arenas[ai.l1()][ai.l2()]
bitmap := arena.checkmarks
if bitmap == nil {
// Allocate bitmap on first use.
bitmap = (*checkmarksMap)(persistentalloc(unsafe.Sizeof(*bitmap), 0, &memstats.gcMiscSys))
if bitmap == nil {
throw("out of memory allocating checkmarks bitmap")
}
arena.checkmarks = bitmap
} else {
// Otherwise clear the existing bitmap.
clear(bitmap.b[:])
}
}
for _, ai := range mheap_.heapArenas {
clearCheckmarks(ai)
}
for _, ai := range mheap_.userArenaArenas {
clearCheckmarks(ai)
}
// Enable checkmarking.
useCheckmark = true
}
// endCheckmarks ends the checkmarks phase.
func endCheckmarks() {
if !gcIsMarkDone() {
throw("GC work not flushed")
}
useCheckmark = false
}
// setCheckmark throws if marking object is a checkmarks violation,
// and otherwise sets obj's checkmark. It returns true if obj was
// already checkmarked.
func setCheckmark(obj, base, off uintptr, mbits markBits) bool {
if !mbits.isMarked() {
printlock()
print("runtime: checkmarks found unexpected unmarked object obj=", hex(obj), "\n")
print("runtime: found obj at *(", hex(base), "+", hex(off), ")\n")
// Dump the source (base) object
gcDumpObject("base", base, off)
// Dump the object
gcDumpObject("obj", obj, ^uintptr(0))
getg().m.traceback = 2
throw("checkmark found unmarked object")
}
bytep, mask := getCheckmark(obj)
if bytep == nil {
return false
}
if atomic.Load8(bytep)&mask != 0 {
// Already checkmarked.
return true
}
atomic.Or8(bytep, mask)
return false
}
func getCheckmark(obj uintptr) (bytep *byte, mask uint8) {
ai := arenaIndex(obj)
arena := mheap_.arenas[ai.l1()][ai.l2()]
if arena == nil {
// Non-heap pointer.
return nil, 0
}
wordIdx := (obj - alignDown(obj, heapArenaBytes)) / goarch.PtrSize
arenaWord := wordIdx / 8
mask = byte(1 << (wordIdx % 8))
bytep = &arena.checkmarks.b[arenaWord]
return bytep, mask
}
// runCheckmark runs a full non-parallel, stop-the-world mark using
// checkmark bits, to check that we didn't forget to mark anything
// during the concurrent mark process.
//
// The world must be stopped to call runCheckmark.
func runCheckmark(prepareRootSet func(*gcWork)) {
assertWorldStopped()
// Turn off gcwaiting because that will force
// gcDrain to return early if this goroutine
// happens to have its preemption flag set.
// This is fine because the world is stopped.
// Restore it after we're done just to be safe.
sched.gcwaiting.Store(false)
startCheckmarks()
gcResetMarkState()
gcw := &getg().m.p.ptr().gcw
prepareRootSet(gcw)
gcDrain(gcw, 0)
wbBufFlush1(getg().m.p.ptr())
gcw.dispose()
endCheckmarks()
sched.gcwaiting.Store(true)
}
// checkFinalizersAndCleanups uses checkmarks to check for potential issues
// with the program's use of cleanups and finalizers.
func checkFinalizersAndCleanups() {
assertWorldStopped()
const (
reportCycle = 1 << iota
reportTiny
)
// Find the arena and page index into that arena for this shard.
type report struct {
issues int
ptr uintptr
sp *special
}
var reports [50]report
var nreports int
var more bool
var lastTinyBlock uintptr
forEachSpecial(func(p uintptr, s *mspan, sp *special) bool {
// N.B. The tiny block specials are sorted first in the specials list.
if sp.kind == _KindSpecialTinyBlock {
lastTinyBlock = s.base() + sp.offset
return true
}
// We only care about finalizers and cleanups.
if sp.kind != _KindSpecialFinalizer && sp.kind != _KindSpecialCleanup {
return true
}
// Run a checkmark GC using this cleanup and/or finalizer as a root.
if debug.checkfinalizers > 1 {
print("Scan trace for cleanup/finalizer on ", hex(p), ":\n")
}
runCheckmark(func(gcw *gcWork) {
switch sp.kind {
case _KindSpecialFinalizer:
gcScanFinalizer((*specialfinalizer)(unsafe.Pointer(sp)), s, gcw)
case _KindSpecialCleanup:
gcScanCleanup((*specialCleanup)(unsafe.Pointer(sp)), gcw)
}
})
if debug.checkfinalizers > 1 {
println()
}
// Now check to see if the object the special is attached to was marked.
// The roots above do not directly mark p, so if it is marked, then p
// must be reachable from the finalizer and/or cleanup, preventing
// reclamation.
bytep, mask := getCheckmark(p)
if bytep == nil {
return true
}
var issues int
if atomic.Load8(bytep)&mask != 0 {
issues |= reportCycle
}
if p >= lastTinyBlock && p < lastTinyBlock+maxTinySize {
issues |= reportTiny
}
if issues != 0 {
if nreports >= len(reports) {
more = true
return false
}
reports[nreports] = report{issues, p, sp}
nreports++
}
return true
})
if nreports > 0 {
lastPtr := uintptr(0)
println("WARNING: LIKELY CLEANUP/FINALIZER ISSUES")
println()
for _, r := range reports[:nreports] {
var ctx *specialCheckFinalizer
var kind string
if r.sp.kind == _KindSpecialFinalizer {
kind = "finalizer"
ctx = getCleanupContext(r.ptr, 0)
} else {
kind = "cleanup"
ctx = getCleanupContext(r.ptr, ((*specialCleanup)(unsafe.Pointer(r.sp))).id)
}
// N.B. reports is sorted 'enough' that cleanups/finalizers on the same pointer will
// appear consecutively because the specials list is sorted.
if lastPtr != r.ptr {
if lastPtr != 0 {
println()
}
print("Value of type ", toRType(ctx.ptrType).string(), " at ", hex(r.ptr), "\n")
if r.issues&reportCycle != 0 {
if r.sp.kind == _KindSpecialFinalizer {
println(" is reachable from finalizer")
} else {
println(" is reachable from cleanup or cleanup argument")
}
}
if r.issues&reportTiny != 0 {
println(" is in a tiny block with other (possibly long-lived) values")
}
if r.issues&reportTiny != 0 && r.issues&reportCycle != 0 {
if r.sp.kind == _KindSpecialFinalizer {
println(" may be in the same tiny block as finalizer")
} else {
println(" may be in the same tiny block as cleanup or cleanup argument")
}
}
}
println()
println("Has", kind, "at", hex(uintptr(unsafe.Pointer(r.sp))))
funcInfo := findfunc(ctx.funcPC)
if funcInfo.valid() {
file, line := funcline(funcInfo, ctx.funcPC)
print(" ", funcname(funcInfo), "()\n")
print(" ", file, ":", line, " +", hex(ctx.funcPC-funcInfo.entry()), "\n")
} else {
print(" <bad pc ", hex(ctx.funcPC), ">\n")
}
println("created at: ")
createInfo := findfunc(ctx.createPC)
if createInfo.valid() {
file, line := funcline(createInfo, ctx.createPC)
print(" ", funcname(createInfo), "()\n")
print(" ", file, ":", line, " +", hex(ctx.createPC-createInfo.entry()), "\n")
} else {
print(" <bad pc ", hex(ctx.createPC), ">\n")
}
lastPtr = r.ptr
}
println()
if more {
println("... too many potential issues ...")
}
throw("detected possible issues with cleanups and/or finalizers")
}
}
// forEachSpecial is an iterator over all specials.
//
// Used by debug.checkfinalizers.
//
// The world must be stopped.
func forEachSpecial(yield func(p uintptr, s *mspan, sp *special) bool) {
assertWorldStopped()
// Find the arena and page index into that arena for this shard.
for _, ai := range mheap_.markArenas {
ha := mheap_.arenas[ai.l1()][ai.l2()]
// Construct slice of bitmap which we'll iterate over.
for i := range ha.pageSpecials[:] {
// Find set bits, which correspond to spans with specials.
specials := atomic.Load8(&ha.pageSpecials[i])
if specials == 0 {
continue
}
for j := uint(0); j < 8; j++ {
if specials&(1<<j) == 0 {
continue
}
// Find the span for this bit.
//
// This value is guaranteed to be non-nil because having
// specials implies that the span is in-use, and since we're
// currently marking we can be sure that we don't have to worry
// about the span being freed and re-used.
s := ha.spans[uint(i)*8+j]
// Lock the specials to prevent a special from being
// removed from the list while we're traversing it.
for sp := s.specials; sp != nil; sp = sp.next {
if !yield(s.base()+sp.offset, s, sp) {
return
}
}
}
}
}
}