Skip to content

Commit 622d0eb

Browse files
aykevldeadprogram
authored andcommitted
compiler: implement nil checks
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.
1 parent b7cdf8c commit 622d0eb

File tree

3 files changed

+58
-0
lines changed

3 files changed

+58
-0
lines changed

compiler/asserts.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package compiler
2+
3+
// This file implements functions that do certain safety checks that are
4+
// required by the Go programming language.
5+
6+
import (
7+
"tinygo.org/x/go-llvm"
8+
)
9+
10+
// emitNilCheck checks whether the given pointer is nil, and panics if it is. It
11+
// has no effect in well-behaved programs, but makes sure no uncaught nil
12+
// pointer dereferences exist in valid Go code.
13+
func (c *Compiler) emitNilCheck(frame *Frame, ptr llvm.Value, blockPrefix string) {
14+
// Check whether this is a nil pointer.
15+
faultBlock := c.ctx.AddBasicBlock(frame.fn.LLVMFn, blockPrefix+".nil")
16+
nextBlock := c.ctx.AddBasicBlock(frame.fn.LLVMFn, blockPrefix+".next")
17+
frame.blockExits[frame.currentBlock] = nextBlock // adjust outgoing block for phi nodes
18+
19+
// Compare against nil.
20+
nilptr := llvm.ConstPointerNull(ptr.Type())
21+
isnil := c.builder.CreateICmp(llvm.IntEQ, ptr, nilptr, "")
22+
c.builder.CreateCondBr(isnil, faultBlock, nextBlock)
23+
24+
// Fail: this is a nil pointer, exit with a panic.
25+
c.builder.SetInsertPointAtEnd(faultBlock)
26+
c.createRuntimeCall("nilpanic", nil, "")
27+
c.builder.CreateUnreachable()
28+
29+
// Ok: this is a valid pointer.
30+
c.builder.SetInsertPointAtEnd(nextBlock)
31+
}

compiler/compiler.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,14 @@ func (c *Compiler) Compile(mainPath string) error {
344344
c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.ExternalLinkage)
345345
c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.ExternalLinkage)
346346

347+
// Tell the optimizer that runtime.alloc is an allocator, meaning that it
348+
// returns values that are never null and never alias to an existing value.
349+
for _, name := range []string{"noalias", "nonnull"} {
350+
attrKind := llvm.AttributeKindID(name)
351+
attr := c.ctx.CreateEnumAttribute(attrKind, 0)
352+
c.mod.NamedFunction("runtime.alloc").AddAttributeAtIndex(0, attr)
353+
}
354+
347355
// see: https://reviews.llvm.org/D18355
348356
if c.Debug {
349357
c.mod.AddNamedMetadataOperand("llvm.module.flags",
@@ -1400,6 +1408,7 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon) (llvm.Value, e
14001408
// closure: {context, function pointer}
14011409
context := c.builder.CreateExtractValue(value, 0, "")
14021410
value = c.builder.CreateExtractValue(value, 1, "")
1411+
c.emitNilCheck(frame, value, "fpcall")
14031412
return c.parseFunctionCall(frame, instr.Args, value, context, false)
14041413
}
14051414
}
@@ -1578,6 +1587,11 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
15781587
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
15791588
llvm.ConstInt(c.ctx.Int32Type(), uint64(expr.Field), false),
15801589
}
1590+
// Check for nil pointer before calculating the address, from the spec:
1591+
// > For an operand x of type T, the address operation &x generates a
1592+
// > pointer of type *T to x. [...] If the evaluation of x would cause a
1593+
// > run-time panic, then the evaluation of &x does too.
1594+
c.emitNilCheck(frame, val, "gep")
15811595
return c.builder.CreateGEP(val, indices, ""), nil
15821596
case *ssa.Function:
15831597
fn := c.ir.GetFunction(expr)
@@ -1637,6 +1651,13 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
16371651
case *types.Array:
16381652
bufptr = val
16391653
buflen = llvm.ConstInt(c.uintptrType, uint64(typ.Len()), false)
1654+
// Check for nil pointer before calculating the address, from
1655+
// the spec:
1656+
// > For an operand x of type T, the address operation &x
1657+
// > generates a pointer of type *T to x. [...] If the
1658+
// > evaluation of x would cause a run-time panic, then the
1659+
// > evaluation of &x does too.
1660+
c.emitNilCheck(frame, bufptr, "gep")
16401661
default:
16411662
return llvm.Value{}, c.makeError(expr.Pos(), "todo: indexaddr: "+typ.String())
16421663
}
@@ -2695,6 +2716,7 @@ func (c *Compiler) parseUnOp(frame *Frame, unop *ssa.UnOp) (llvm.Value, error) {
26952716
fn := c.mod.NamedFunction(name)
26962717
return c.builder.CreateBitCast(fn, c.i8ptrType, ""), nil
26972718
} else {
2719+
c.emitNilCheck(frame, x, "deref")
26982720
load := c.builder.CreateLoad(x, "")
26992721
if c.ir.IsVolatile(valType) {
27002722
// Volatile load, for memory-mapped registers.

src/runtime/panic.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ func _recover() interface{} {
2727
return nil
2828
}
2929

30+
// Panic when trying to dereference a nil pointer.
31+
func nilpanic() {
32+
runtimePanic("nil pointer dereference")
33+
}
34+
3035
// Check for bounds in *ssa.Index, *ssa.IndexAddr and *ssa.Lookup.
3136
func lookupBoundsCheck(length uintptr, index int) {
3237
if index < 0 || index >= int(length) {

0 commit comments

Comments
 (0)