
During a run of interp, some memory (for example, memory allocated through runtime.alloc) may not have a known LLVM type. This memory is alllocated by creating an i8 array. This does not necessarily work, as i8 has no alignment requirements while the allocated object may have allocation requirements. Therefore, the resulting global may have an alignment that is too loose. This works on some microcontrollers but notably does not work on a Cortex-M0 or Cortex-M0+, as all load/store operations must be aligned. This commit fixes this by setting the alignment of untyped memory to the maximum alignment. The determination of "maximum alignment" is not great but should get the job done on most architectures.
147 строки
4,9 КиБ
Go
147 строки
4,9 КиБ
Go
// Package interp is a partial evaluator of code run at package init time. See
|
|
// the README in this package for details.
|
|
package interp
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
// Enable extra checks, which should be disabled by default.
|
|
// This may help track down bugs by adding a few more sanity checks.
|
|
const checks = true
|
|
|
|
// runner contains all state related to one interp run.
|
|
type runner struct {
|
|
mod llvm.Module
|
|
targetData llvm.TargetData
|
|
builder llvm.Builder
|
|
pointerSize uint32 // cached pointer size from the TargetData
|
|
i8ptrType llvm.Type // often used type so created in advance
|
|
maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result
|
|
debug bool // log debug messages
|
|
pkgName string // package name of the currently executing package
|
|
functionCache map[llvm.Value]*function // cache of compiled functions
|
|
objects []object // slice of objects in memory
|
|
globals map[llvm.Value]int // map from global to index in objects slice
|
|
start time.Time
|
|
callsExecuted uint64
|
|
}
|
|
|
|
// Run evaluates runtime.initAll function as much as possible at compile time.
|
|
// Set debug to true if it should print output while running.
|
|
func Run(mod llvm.Module, debug bool) error {
|
|
r := runner{
|
|
mod: mod,
|
|
targetData: llvm.NewTargetData(mod.DataLayout()),
|
|
debug: debug,
|
|
functionCache: make(map[llvm.Value]*function),
|
|
objects: []object{{}},
|
|
globals: make(map[llvm.Value]int),
|
|
start: time.Now(),
|
|
}
|
|
r.pointerSize = uint32(r.targetData.PointerSize())
|
|
r.i8ptrType = llvm.PointerType(mod.Context().Int8Type(), 0)
|
|
r.maxAlign = r.targetData.PrefTypeAlignment(r.i8ptrType) // assume pointers are maximally aligned (this is not always the case)
|
|
|
|
initAll := mod.NamedFunction("runtime.initAll")
|
|
bb := initAll.EntryBasicBlock()
|
|
|
|
// Create a builder, to insert instructions that could not be evaluated at
|
|
// compile time.
|
|
r.builder = mod.Context().NewBuilder()
|
|
defer r.builder.Dispose()
|
|
|
|
// Create a dummy alloca in the entry block that we can set the insert point
|
|
// to. This is necessary because otherwise we might be removing the
|
|
// instruction (init call) that we are removing after successful
|
|
// interpretation.
|
|
r.builder.SetInsertPointBefore(bb.FirstInstruction())
|
|
dummy := r.builder.CreateAlloca(r.mod.Context().Int8Type(), "dummy")
|
|
r.builder.SetInsertPointBefore(dummy)
|
|
defer dummy.EraseFromParentAsInstruction()
|
|
|
|
// Get a list if init calls. A runtime.initAll might look something like this:
|
|
// func initAll() {
|
|
// unsafe.init()
|
|
// machine.init()
|
|
// runtime.init()
|
|
// }
|
|
// This function gets a list of these call instructions.
|
|
var initCalls []llvm.Value
|
|
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
|
if inst == dummy {
|
|
continue
|
|
}
|
|
if !inst.IsAReturnInst().IsNil() {
|
|
break // ret void
|
|
}
|
|
if inst.IsACallInst().IsNil() || inst.CalledValue().IsAFunction().IsNil() {
|
|
return errorAt(inst, "interp: expected all instructions in "+initAll.Name()+" to be direct calls")
|
|
}
|
|
initCalls = append(initCalls, inst)
|
|
}
|
|
|
|
// Run initializers for each package. Once the package initializer is
|
|
// finished, the call to the package initializer can be removed.
|
|
for _, call := range initCalls {
|
|
initName := call.CalledValue().Name()
|
|
if !strings.HasSuffix(initName, ".init") {
|
|
return errorAt(call, "interp: expected all instructions in "+initAll.Name()+" to be *.init() calls")
|
|
}
|
|
r.pkgName = initName[:len(initName)-len(".init")]
|
|
fn := call.CalledValue()
|
|
if r.debug {
|
|
fmt.Fprintln(os.Stderr, "call:", fn.Name())
|
|
}
|
|
_, mem, callErr := r.run(r.getFunction(fn), nil, nil, " ")
|
|
if callErr != nil {
|
|
if isRecoverableError(callErr.Err) {
|
|
if r.debug {
|
|
fmt.Fprintln(os.Stderr, "not interpretring", r.pkgName, "because of error:", callErr.Err)
|
|
}
|
|
mem.revert()
|
|
continue
|
|
}
|
|
return callErr
|
|
}
|
|
call.EraseFromParentAsInstruction()
|
|
for index, obj := range mem.objects {
|
|
r.objects[index] = obj
|
|
}
|
|
}
|
|
r.pkgName = ""
|
|
|
|
// Update all global variables in the LLVM module.
|
|
mem := memoryView{r: &r}
|
|
for _, obj := range r.objects {
|
|
if obj.llvmGlobal.IsNil() {
|
|
continue
|
|
}
|
|
if obj.buffer == nil {
|
|
continue
|
|
}
|
|
initializer := obj.buffer.toLLVMValue(obj.llvmGlobal.Type().ElementType(), &mem)
|
|
if checks && initializer.Type() != obj.llvmGlobal.Type().ElementType() {
|
|
panic("initializer type mismatch")
|
|
}
|
|
obj.llvmGlobal.SetInitializer(initializer)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getFunction returns the compiled version of the given LLVM function. It
|
|
// compiles the function if necessary and caches the result.
|
|
func (r *runner) getFunction(llvmFn llvm.Value) *function {
|
|
if fn, ok := r.functionCache[llvmFn]; ok {
|
|
return fn
|
|
}
|
|
fn := r.compileFunction(llvmFn)
|
|
r.functionCache[llvmFn] = fn
|
|
return fn
|
|
}
|