Skip to content

Commit

Permalink
compiler: implement nil checks
Browse files Browse the repository at this point in the history
This commit implements nil checks for all platforms. These nil checks
can be optimized on systems with a MMU, but since a major target is
systems without MMU, keep it this way for now.

It implements three checks:
  * Nil checks before dereferencing a pointer.
  * Nil checks before calculating an address (*ssa.FieldAddr and
    *ssa.IndexAddr)
  * Nil checks before calling a function pointer.

The first check has by far the biggest impact, with around 5% increase
in code size. The other checks only trigger in only some test cases and
have a minimal impact on code size.
This first nil check is also the one that is easiest to avoid on systems
with MMU, if necessary.
  • Loading branch information
aykevl authored and deadprogram committed Mar 8, 2019
1 parent b7cdf8c commit 622d0eb
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 0 deletions.
31 changes: 31 additions & 0 deletions compiler/asserts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package compiler

// This file implements functions that do certain safety checks that are
// required by the Go programming language.

import (
"tinygo.org/x/go-llvm"
)

// emitNilCheck checks whether the given pointer is nil, and panics if it is. It
// has no effect in well-behaved programs, but makes sure no uncaught nil
// pointer dereferences exist in valid Go code.
func (c *Compiler) emitNilCheck(frame *Frame, ptr llvm.Value, blockPrefix string) {
// Check whether this is a nil pointer.
faultBlock := c.ctx.AddBasicBlock(frame.fn.LLVMFn, blockPrefix+".nil")
nextBlock := c.ctx.AddBasicBlock(frame.fn.LLVMFn, blockPrefix+".next")
frame.blockExits[frame.currentBlock] = nextBlock // adjust outgoing block for phi nodes

// Compare against nil.
nilptr := llvm.ConstPointerNull(ptr.Type())
isnil := c.builder.CreateICmp(llvm.IntEQ, ptr, nilptr, "")
c.builder.CreateCondBr(isnil, faultBlock, nextBlock)

// Fail: this is a nil pointer, exit with a panic.
c.builder.SetInsertPointAtEnd(faultBlock)
c.createRuntimeCall("nilpanic", nil, "")
c.builder.CreateUnreachable()

// Ok: this is a valid pointer.
c.builder.SetInsertPointAtEnd(nextBlock)
}
22 changes: 22 additions & 0 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,14 @@ func (c *Compiler) Compile(mainPath string) error {
c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.ExternalLinkage)
c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.ExternalLinkage)

// Tell the optimizer that runtime.alloc is an allocator, meaning that it
// returns values that are never null and never alias to an existing value.
for _, name := range []string{"noalias", "nonnull"} {
attrKind := llvm.AttributeKindID(name)
attr := c.ctx.CreateEnumAttribute(attrKind, 0)
c.mod.NamedFunction("runtime.alloc").AddAttributeAtIndex(0, attr)
}

// see: https://reviews.llvm.org/D18355
if c.Debug {
c.mod.AddNamedMetadataOperand("llvm.module.flags",
Expand Down Expand Up @@ -1400,6 +1408,7 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon) (llvm.Value, e
// closure: {context, function pointer}
context := c.builder.CreateExtractValue(value, 0, "")
value = c.builder.CreateExtractValue(value, 1, "")
c.emitNilCheck(frame, value, "fpcall")
return c.parseFunctionCall(frame, instr.Args, value, context, false)
}
}
Expand Down Expand Up @@ -1578,6 +1587,11 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
llvm.ConstInt(c.ctx.Int32Type(), uint64(expr.Field), false),
}
// Check for nil pointer before calculating the address, from the spec:
// > For an operand x of type T, the address operation &x generates a
// > pointer of type *T to x. [...] If the evaluation of x would cause a
// > run-time panic, then the evaluation of &x does too.
c.emitNilCheck(frame, val, "gep")
return c.builder.CreateGEP(val, indices, ""), nil
case *ssa.Function:
fn := c.ir.GetFunction(expr)
Expand Down Expand Up @@ -1637,6 +1651,13 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
case *types.Array:
bufptr = val
buflen = llvm.ConstInt(c.uintptrType, uint64(typ.Len()), false)
// Check for nil pointer before calculating the address, from
// the spec:
// > For an operand x of type T, the address operation &x
// > generates a pointer of type *T to x. [...] If the
// > evaluation of x would cause a run-time panic, then the
// > evaluation of &x does too.
c.emitNilCheck(frame, bufptr, "gep")
default:
return llvm.Value{}, c.makeError(expr.Pos(), "todo: indexaddr: "+typ.String())
}
Expand Down Expand Up @@ -2695,6 +2716,7 @@ func (c *Compiler) parseUnOp(frame *Frame, unop *ssa.UnOp) (llvm.Value, error) {
fn := c.mod.NamedFunction(name)
return c.builder.CreateBitCast(fn, c.i8ptrType, ""), nil
} else {
c.emitNilCheck(frame, x, "deref")
load := c.builder.CreateLoad(x, "")
if c.ir.IsVolatile(valType) {
// Volatile load, for memory-mapped registers.
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/panic.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func _recover() interface{} {
return nil
}

// Panic when trying to dereference a nil pointer.
func nilpanic() {
runtimePanic("nil pointer dereference")
}

// Check for bounds in *ssa.Index, *ssa.IndexAddr and *ssa.Lookup.
func lookupBoundsCheck(length uintptr, index int) {
if index < 0 || index >= int(length) {
Expand Down

0 comments on commit 622d0eb

Please sign in to comment.