Files
tinygo/compiler/gc-pass-shadowstack.go
2019-01-17 21:01:15 +01:00

200 lines
6.6 KiB
Go

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
}