mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-04 02:57:46 +00:00
WIP shadow-stack based mark/sweep collector
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
package compiler
|
||||
|
||||
// This file implements a compiler pass to move GC pointers to a "shadow stack"
|
||||
// that can easily be scanned by a garbage collector, even without platform
|
||||
// support.
|
||||
// For more information, see:
|
||||
// https://llvm.org/docs/GarbageCollection.html#the-shadow-stack-gc
|
||||
|
||||
import (
|
||||
"github.com/aykevl/go-llvm"
|
||||
)
|
||||
|
||||
// AddGCRoots moves pointer values to shadow stack frames when this function (or
|
||||
// any function it calls) may allocate something. This allows the GC to scan the
|
||||
// stack in a highly portable way.
|
||||
func (c *Compiler) AddGCRoots() {
|
||||
alloc := c.mod.NamedFunction("runtime.alloc")
|
||||
if alloc.IsNil() {
|
||||
return
|
||||
}
|
||||
|
||||
// Find all functions that do memory allocation.
|
||||
worklist := []llvm.Value{alloc}
|
||||
allocSet := make(map[llvm.Value]struct{})
|
||||
allocList := make([]llvm.Value, 0, 4)
|
||||
for len(worklist) != 0 {
|
||||
// Pick the topmost.
|
||||
f := worklist[len(worklist)-1]
|
||||
worklist = worklist[:len(worklist)-1]
|
||||
if _, ok := allocSet[f]; ok {
|
||||
continue // already added to list
|
||||
}
|
||||
// Add to set of allocating functions.
|
||||
allocSet[f] = struct{}{}
|
||||
allocList = append(allocList, f)
|
||||
|
||||
// Add all callees to the worklist.
|
||||
for _, use := range getUses(f) {
|
||||
if use.IsACallInst().IsNil() {
|
||||
// TODO: function pointers
|
||||
panic("allocating function " + f.Name() + " used as function pointer")
|
||||
}
|
||||
parent := use.InstructionParent().Parent()
|
||||
for i := 0; i < use.OperandsCount()-1; i++ {
|
||||
if use.Operand(i) == f {
|
||||
// TODO: function pointers
|
||||
panic("allocating function " + f.Name() + " used as function pointer in " + parent.Name())
|
||||
}
|
||||
}
|
||||
worklist = append(worklist, parent)
|
||||
}
|
||||
}
|
||||
|
||||
i8ptrPtrType := llvm.PointerType(c.i8ptrType, 0)
|
||||
gcrootType := llvm.FunctionType(c.ctx.VoidType(), []llvm.Type{i8ptrPtrType, c.i8ptrType}, false)
|
||||
gcroot := llvm.AddFunction(c.mod, "llvm.gcroot", gcrootType)
|
||||
|
||||
// Process every function that needs to save pointers to the shadow stack.
|
||||
for _, fn := range allocList {
|
||||
if fn == alloc {
|
||||
// runtime.alloc itself should not be treated this way, it is a
|
||||
// special case.
|
||||
continue
|
||||
}
|
||||
|
||||
// Check all instructions in this function and see whether the value
|
||||
// needs to be kept on the shadow stack.
|
||||
var values []llvm.Value // values to be kept in the shadow stack
|
||||
for bb := fn.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if !typeHasPointer(inst.Type()) {
|
||||
// This instruction does not result in a pointer value.
|
||||
continue
|
||||
}
|
||||
// Check whether any of the uses may occur after a call to
|
||||
// runtime.alloca. For example, if there are no call
|
||||
// instructions between the definition and the use, then the
|
||||
// pointer does not have to be stored in the shadow stack.
|
||||
for _, use := range getUses(inst) {
|
||||
if crossesAllocatingInst(inst, use, allocSet) {
|
||||
values = append(values, inst)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(values) == 0 {
|
||||
// The children of this function do allocations, but there is
|
||||
// nothing to keep in a stack frame for this function.
|
||||
continue
|
||||
}
|
||||
fn.SetGC("shadow-stack")
|
||||
|
||||
// Convert all values to be kept in the shadow stack to actually be in
|
||||
// the shadow stack.
|
||||
firstInst := fn.EntryBasicBlock().FirstInstruction()
|
||||
for _, value := range values {
|
||||
valueUses := getUses(value)
|
||||
c.builder.SetInsertPointBefore(firstInst)
|
||||
alloca := c.builder.CreateAlloca(value.Type(), "gcroot.value")
|
||||
c.builder.SetInsertPointBefore(llvm.NextInstruction(value))
|
||||
c.builder.CreateStore(value, alloca)
|
||||
metadata := c.gcTypeMetadata(alloca.Type().ElementType())
|
||||
allocaCast := alloca
|
||||
if alloca.Type() != i8ptrPtrType {
|
||||
allocaCast = c.builder.CreateBitCast(alloca, i8ptrPtrType, "")
|
||||
}
|
||||
c.builder.CreateCall(gcroot, []llvm.Value{allocaCast, metadata}, "")
|
||||
for _, use := range valueUses {
|
||||
c.builder.SetInsertPointBefore(use)
|
||||
load := c.builder.CreateLoad(alloca, "")
|
||||
for i := 0; i < use.OperandsCount(); i++ {
|
||||
if use.Operand(i) == value {
|
||||
use.SetOperand(i, load)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println(c.IR())
|
||||
}
|
||||
|
||||
// typeHasPointer returns true if (and only if) the given type contains a
|
||||
// pointer value.
|
||||
func typeHasPointer(typ llvm.Type) bool {
|
||||
switch typ.TypeKind() {
|
||||
case llvm.PointerTypeKind:
|
||||
return true
|
||||
case llvm.ArrayTypeKind, llvm.VectorTypeKind:
|
||||
return typeHasPointer(typ.ElementType())
|
||||
case llvm.StructTypeKind:
|
||||
return false
|
||||
for _, subtyp := range typ.StructElementTypes() {
|
||||
if typeHasPointer(subtyp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// gcTypeMetadata returns a pointer value to be used in the llvm.gcroot
|
||||
// intrinsic. It is either a null pointer or a number which is the number of
|
||||
// words in the stack slot for this value.
|
||||
func (c *Compiler) gcTypeMetadata(typ llvm.Type) llvm.Value {
|
||||
if typ.TypeKind() == llvm.PointerTypeKind {
|
||||
// Simple pointer. This is a common case, so signal this fact by setting
|
||||
// the pointer to null.
|
||||
return llvm.ConstPointerNull(c.i8ptrType)
|
||||
}
|
||||
if typ.TypeKind() == llvm.StructTypeKind {
|
||||
// Check for structs that only contain a pointer at the start.
|
||||
// We can pretend that such structs are a simple pointer, as the GC only
|
||||
// needs to read the first word.
|
||||
subTypes := typ.StructElementTypes()
|
||||
onlyFirstPointer := subTypes[0].TypeKind() == llvm.PointerTypeKind
|
||||
for _, subType := range subTypes[1:] {
|
||||
if typeHasPointer(subType) {
|
||||
onlyFirstPointer = false
|
||||
}
|
||||
}
|
||||
if onlyFirstPointer {
|
||||
// Types like string and slice.
|
||||
return llvm.ConstPointerNull(c.i8ptrType)
|
||||
}
|
||||
}
|
||||
allocaSize := c.targetData.TypeAllocSize(typ)
|
||||
pointerAlignment := uint64(c.targetData.PrefTypeAlignment(c.i8ptrType))
|
||||
numWords := allocaSize / pointerAlignment
|
||||
// TODO: only return the number until all pointers are included in this
|
||||
// struct, not more.
|
||||
metadata := llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, numWords, false), c.i8ptrType)
|
||||
return metadata
|
||||
}
|
||||
|
||||
// crossesAllocatingInst returns true if the given value may be used across a
|
||||
// call to runtime.alloc. This check is very conservative.
|
||||
func crossesAllocatingInst(from, to llvm.Value, allocSet map[llvm.Value]struct{}) bool {
|
||||
if from.InstructionParent() != to.InstructionParent() {
|
||||
// Don't try to check the CFG, conservatively assume there is an alloca
|
||||
// in between these instructions.
|
||||
return true
|
||||
}
|
||||
for inst := llvm.NextInstruction(from); inst != to; inst = llvm.NextInstruction(inst) {
|
||||
if inst.IsACallInst().IsNil() {
|
||||
// Not a call instruction thus not an alloca instruction.
|
||||
continue
|
||||
}
|
||||
if _, ok := allocSet[inst.CalledValue()]; ok {
|
||||
// This call is to a function that may do an allocation, or is even
|
||||
// runtime.alloc itself.
|
||||
// TODO: function pointers
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -52,6 +52,10 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro
|
||||
// Run TinyGo-specific interprocedural optimizations.
|
||||
c.OptimizeAllocs()
|
||||
c.OptimizeStringToBytes()
|
||||
|
||||
if c.selectGC() == "shadowstack" {
|
||||
c.AddGCRoots()
|
||||
}
|
||||
} else {
|
||||
// Must be run at any optimization level.
|
||||
c.LowerInterfaces()
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// +build gc.shadowstack
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:extern __data_start
|
||||
var dataStartSymbol unsafe.Pointer
|
||||
|
||||
//go:extern _edata
|
||||
var dataEndSymbol unsafe.Pointer
|
||||
|
||||
//go:extern __bss_start
|
||||
var bssStartSymbol unsafe.Pointer
|
||||
|
||||
//go:extern _ebss
|
||||
var bssEndSymbol unsafe.Pointer
|
||||
|
||||
var (
|
||||
dataStart = uintptr(unsafe.Pointer(&dataStartSymbol))
|
||||
dataEnd = uintptr(unsafe.Pointer(&dataEndSymbol))
|
||||
bssStart = uintptr(unsafe.Pointer(&bssStartSymbol))
|
||||
bssEnd = uintptr(unsafe.Pointer(&bssEndSymbol))
|
||||
)
|
||||
|
||||
// Constant value with metadata about a given function.
|
||||
type frameMap struct {
|
||||
numRoots int32
|
||||
numMeta int32
|
||||
meta [0]uintptr // unknown number of 'meta' pointers
|
||||
}
|
||||
|
||||
// Stack-allocated struct with live pointers.
|
||||
type stackEntry struct {
|
||||
next *stackEntry
|
||||
frame *frameMap
|
||||
roots [0]uintptr // unknown number of root pointers
|
||||
}
|
||||
|
||||
// Note: this symbol must be redefined to llvm_gc_root_chain by the linker.
|
||||
// For example, you can use this linker option:
|
||||
// --defsym=tinygo_gc_root_chain=llvm_gc_root_chain
|
||||
// The reason is that otherwise the shadow stack GC pass in LLVM tries to set an
|
||||
// initial value to llvm_gc_root_chain of a different type.
|
||||
//go:extern tinygo_gc_root_chain
|
||||
var gcRootChain *stackEntry
|
||||
|
||||
func init() {
|
||||
initHeap()
|
||||
}
|
||||
|
||||
func alloc(size uintptr) unsafe.Pointer {
|
||||
GC()
|
||||
return heapAlloc(size)
|
||||
}
|
||||
|
||||
func free(ptr unsafe.Pointer) {
|
||||
heapFree(ptr)
|
||||
}
|
||||
|
||||
// GC performs a garbage collection cycle.
|
||||
func GC() {
|
||||
if gcDebug {
|
||||
println("running collection cycle...")
|
||||
}
|
||||
|
||||
// Mark phase: mark all reachable objects, recursively.
|
||||
markRoots(globalsStart, globalsEnd)
|
||||
markStack()
|
||||
|
||||
// Sweep phase: free all non-marked objects and unmark marked objects for
|
||||
// the next collection cycle.
|
||||
sweep()
|
||||
|
||||
// Show how much has been sweeped, for debugging.
|
||||
if gcDebug {
|
||||
dumpHeap()
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all pointers from the stack using the LLVM shadow stack.
|
||||
//go:nobounds
|
||||
func markStack() {
|
||||
chain := gcRootChain
|
||||
// Traverse the linked-list of stack roots.
|
||||
for chain != nil {
|
||||
// Check each root.
|
||||
for i := int32(0); i < chain.frame.numRoots; i++ {
|
||||
root := chain.roots[i]
|
||||
size := uintptr(1)
|
||||
if i < chain.frame.numMeta {
|
||||
// This root has no metadata associated with it.
|
||||
// Therefore, it must be a single pointer.
|
||||
size = chain.frame.meta[i]
|
||||
}
|
||||
for i := uintptr(0); i < size; i++ {
|
||||
if looksLikePointer(root) {
|
||||
markRoot(root, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
chain = chain.next
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) {
|
||||
BuildTags: []string{goos, goarch},
|
||||
Compiler: commands["clang"],
|
||||
Linker: "cc",
|
||||
LDFlags: []string{"-no-pie"}, // WARNING: clang < 5.0 requires -nopie
|
||||
LDFlags: []string{"-Wl,--defsym=tinygo_gc_root_chain=llvm_gc_root_chain", "-no-pie"}, // WARNING: clang < 5.0 requires -nopie
|
||||
Objcopy: "objcopy",
|
||||
GDB: "gdb",
|
||||
GDBCmds: []string{"run"},
|
||||
|
||||
Reference in New Issue
Block a user