Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 38 additions & 92 deletions builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ type packageAction struct {
EmbeddedFiles map[string]string // hash of all the //go:embed files in the package
Imports map[string]string // map from imported package to action ID hash
OptLevel string // LLVM optimization level (O0, O1, O2, Os, Oz)
UndefinedGlobals []string // globals that are left as external globals (no initializer)
UndefinedGlobals map[string]string // globals set via -ldflags -X (name -> value), for cache key
}

// Build performs a single package to executable Go build. It takes in a package
Expand Down Expand Up @@ -268,11 +268,11 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
for _, pkg := range lprogram.Sorted() {
pkg := pkg // necessary to avoid a race condition

var undefinedGlobals []string
for name := range globalValues[pkg.Pkg.Path()] {
undefinedGlobals = append(undefinedGlobals, name)
// Collect -X globals for this package (name -> value)
undefinedGlobals := globalValues[pkg.Pkg.Path()]
if undefinedGlobals == nil {
undefinedGlobals = map[string]string{}
}
sort.Strings(undefinedGlobals)

// Make compile jobs to load files to be embedded in the output binary.
var actionIDDependencies []*compileJob
Expand Down Expand Up @@ -444,30 +444,39 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
}
}

// Erase all globals that are part of the undefinedGlobals list.
// This list comes from the -ldflags="-X pkg.foo=val" option.
// Instead of setting the value directly in the AST (which would
// mean the value, which may be a secret, is stored in the build
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a common use case for -ldflags. It is a major implication of this change, have to think about it. I am looking to see if we can use some of this proposed change and still prevent this from being cached...

// cache), the global itself is left external (undefined) and is
// only set at the end of the compilation.
for _, name := range undefinedGlobals {
globalName := pkg.Pkg.Path() + "." + name
global := mod.NamedGlobal(globalName)
if global.IsNil() {
return errors.New("global not found: " + globalName)
}
globalType := global.GlobalValueType()
if globalType.TypeKind() != llvm.StructTypeKind || globalType.StructName() != "runtime._string" {
// Verify this is indeed a string. This is needed so
// that makeGlobalsModule can just create the right
// globals of string type without checking.
return fmt.Errorf("%s: not a string", globalName)
// Set -X global values before interp runs.
// This ensures that dependent init code (e.g., var VersionLen = len(Version))
// sees the correct -X value during interpretation.
// The -X global names are passed to interp so it skips stores to them.
undefinedGlobalNames := make(map[string]struct{}, len(undefinedGlobals))
if len(undefinedGlobals) > 0 {
targetData := machine.CreateTargetData()
uintptrType := mod.Context().IntType(targetData.PointerSize() * 8)
targetData.Dispose()
for name, value := range undefinedGlobals {
globalName := pkg.Pkg.Path() + "." + name
undefinedGlobalNames[globalName] = struct{}{}
global := mod.NamedGlobal(globalName)
if global.IsNil() {
return errors.New("global not found: " + globalName)
}
globalType := global.GlobalValueType()
if globalType.TypeKind() != llvm.StructTypeKind || globalType.StructName() != "runtime._string" {
// Verify this is indeed a string.
return fmt.Errorf("%s: not a string", globalName)
}
// Set the -X value as the initializer.
// interp will skip stores to this global, preserving this value.
bufInitializer := mod.Context().ConstString(value, false)
buf := llvm.AddGlobal(mod, bufInitializer.Type(), globalName+".str")
buf.SetInitializer(bufInitializer)
buf.SetAlignment(1)
buf.SetUnnamedAddr(true)
buf.SetLinkage(llvm.PrivateLinkage)
length := llvm.ConstInt(uintptrType, uint64(len(value)), false)
initializer := llvm.ConstNamedStruct(globalType, []llvm.Value{buf, length})
global.SetInitializer(initializer)
}
name := global.Name()
newGlobal := llvm.AddGlobal(mod, globalType, name+".tmp")
global.ReplaceAllUsesWith(newGlobal)
global.EraseFromParentAsGlobal()
newGlobal.SetName(name)
}

// Try to interpret package initializers at compile time.
Expand All @@ -477,7 +486,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
if pkgInit.IsNil() {
panic("init not found for " + pkg.Pkg.Path())
}
err := interp.RunFunc(pkgInit, config.Options.InterpTimeout, config.DumpSSA())
err := interp.RunFunc(pkgInit, undefinedGlobalNames, config.Options.InterpTimeout, config.DumpSSA())
if err != nil {
return err
}
Expand Down Expand Up @@ -569,15 +578,6 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
}
}

// Insert values from -ldflags="-X ..." into the IR.
// This is a separate module, so that the "runtime._string" type
// doesn't need to match precisely. LLVM tends to rename that type
// sometimes, leading to errors. But linking in a separate module
// works fine. See:
// https://github.com/tinygo-org/tinygo/issues/4810
globalsMod := makeGlobalsModule(ctx, globalValues, machine)
llvm.LinkModules(mod, globalsMod)

// Create runtime.initAll function that calls the runtime
// initializer of each package.
llvmInitFn := mod.NamedFunction("runtime.initAll")
Expand Down Expand Up @@ -1226,60 +1226,6 @@ func optimizeProgram(mod llvm.Module, config *compileopts.Config) error {
return nil
}

func makeGlobalsModule(ctx llvm.Context, globals map[string]map[string]string, machine llvm.TargetMachine) llvm.Module {
mod := ctx.NewModule("cmdline-globals")
targetData := machine.CreateTargetData()
defer targetData.Dispose()
mod.SetDataLayout(targetData.String())

stringType := ctx.StructCreateNamed("runtime._string")
uintptrType := ctx.IntType(targetData.PointerSize() * 8)
stringType.StructSetBody([]llvm.Type{
llvm.PointerType(ctx.Int8Type(), 0),
uintptrType,
}, false)

var pkgPaths []string
for pkgPath := range globals {
pkgPaths = append(pkgPaths, pkgPath)
}
sort.Strings(pkgPaths)
for _, pkgPath := range pkgPaths {
pkg := globals[pkgPath]
var names []string
for name := range pkg {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
value := pkg[name]
globalName := pkgPath + "." + name

// Create a buffer for the string contents.
bufInitializer := mod.Context().ConstString(value, false)
buf := llvm.AddGlobal(mod, bufInitializer.Type(), ".string")
buf.SetInitializer(bufInitializer)
buf.SetAlignment(1)
buf.SetUnnamedAddr(true)
buf.SetLinkage(llvm.PrivateLinkage)

// Create the string value, which is a {ptr, len} pair.
length := llvm.ConstInt(uintptrType, uint64(len(value)), false)
initializer := llvm.ConstNamedStruct(stringType, []llvm.Value{
buf,
length,
})

// Create the string global.
global := llvm.AddGlobal(mod, stringType, globalName)
global.SetInitializer(initializer)
global.SetAlignment(targetData.PrefTypeAlignment(stringType))
}
}

return mod
}

// functionStackSizes keeps stack size information about a single function
// (usually a goroutine).
type functionStackSize struct {
Expand Down
37 changes: 20 additions & 17 deletions interp/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@ 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
dataPtrType llvm.Type // often used type so created in advance
uintptrType llvm.Type // equivalent to uintptr in Go
maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result
byteOrder binary.ByteOrder // big-endian or little-endian
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
timeout time.Duration
callsExecuted uint64
mod llvm.Module
targetData llvm.TargetData
builder llvm.Builder
pointerSize uint32 // cached pointer size from the TargetData
dataPtrType llvm.Type // often used type so created in advance
uintptrType llvm.Type // equivalent to uintptr in Go
maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result
byteOrder binary.ByteOrder // big-endian or little-endian
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
timeout time.Duration
undefinedGlobals map[string]struct{} // globals set via -X, skip stores to these
callsExecuted uint64
}

func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner {
Expand Down Expand Up @@ -204,11 +205,13 @@ func Run(mod llvm.Module, timeout time.Duration, debug bool) error {

// RunFunc evaluates a single package initializer at compile time.
// Set debug to true if it should print output while running.
func RunFunc(fn llvm.Value, timeout time.Duration, debug bool) error {
// undefinedGlobals contains globals set via -X ldflags; stores to these are skipped.
func RunFunc(fn llvm.Value, undefinedGlobals map[string]struct{}, timeout time.Duration, debug bool) error {
// Create and initialize *runner object.
mod := fn.GlobalParent()
r := newRunner(mod, timeout, debug)
defer r.dispose()
r.undefinedGlobals = undefinedGlobals
initName := fn.Name()
if !strings.HasSuffix(initName, ".init") {
return errorAt(fn, "interp: unexpected function name (expected *.init)")
Expand Down
14 changes: 14 additions & 0 deletions interp/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,20 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
if err != nil {
return nil, mem, r.errorAt(inst, err)
}
// Skip stores to -X globals. These globals already have the correct
// value set via -ldflags, and we don't want the init code to
// overwrite it with the source code's default value.
if r.undefinedGlobals != nil {
obj := mem.get(ptr.index())
if !obj.llvmGlobal.IsNil() {
if _, isUndefined := r.undefinedGlobals[obj.llvmGlobal.Name()]; isUndefined {
if r.debug {
fmt.Fprintln(os.Stderr, indent+"skip store to -X global:", obj.llvmGlobal.Name())
}
continue
}
}
}
if inst.llvmInst.IsVolatile() || inst.llvmInst.Ordering() != llvm.AtomicOrderingNotAtomic || mem.hasExternalLoadOrStore(ptr) {
err := r.runAtRuntime(fn, inst, locals, &mem, indent)
if err != nil {
Expand Down
Loading