llvm-project/llgo/cmd/gllgo/gllgo.go

838 lines
20 KiB
Go

//===- gllgo.go - gccgo-like driver for llgo ------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This is llgo's driver. It has a gccgo-like interface in order to easily
// interoperate with the "go" command and the libgo build system.
//
//===----------------------------------------------------------------------===//
package main
/*
#include "config.h"
*/
import "C"
import (
"errors"
"fmt"
"go/scanner"
"go/token"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"llvm.org/llgo/debug"
"llvm.org/llgo/driver"
"llvm.org/llgo/irgen"
"llvm.org/llvm/bindings/go/llvm"
)
const LibDirSuffix = C.LLVM_LIBDIR_SUFFIX
func report(err error) {
if list, ok := err.(scanner.ErrorList); ok {
for _, e := range list {
fmt.Fprintf(os.Stderr, "%s\n", e)
}
} else if err != nil {
fmt.Fprintf(os.Stderr, "gllgo: error: %s\n", err)
}
}
func llvmVersion() string {
return strings.Replace(llvm.Version, "svn", "", 1)
}
func displayVersion() {
fmt.Printf("llgo version %s (%s)\n\n", llvmVersion(), irgen.GoVersion())
os.Exit(0)
}
func initCompiler(opts *driverOptions) (*irgen.Compiler, error) {
importPaths := make([]string, len(opts.importPaths)+len(opts.libPaths))
copy(importPaths, opts.importPaths)
copy(importPaths[len(opts.importPaths):], opts.libPaths)
if opts.prefix != "" {
importPaths = append(importPaths, filepath.Join(opts.prefix, "lib"+LibDirSuffix, "go", "llgo-"+llvmVersion()))
}
copts := irgen.CompilerOptions{
TargetTriple: opts.triple,
GenerateDebug: opts.generateDebug,
DebugPrefixMaps: opts.debugPrefixMaps,
DumpSSA: opts.dumpSSA,
GccgoPath: opts.gccgoPath,
GccgoABI: opts.gccgoPath != "",
ImportPaths: importPaths,
SanitizerAttribute: opts.sanitizer.getAttribute(),
}
if opts.dumpTrace {
copts.Logger = log.New(os.Stderr, "", 0)
}
return irgen.NewCompiler(copts)
}
type actionKind int
const (
actionAssemble = actionKind(iota)
actionCompile
actionLink
actionPrint
)
type action struct {
kind actionKind
inputs []string
}
type sanitizerOptions struct {
blacklist string
crtPrefix string
address, thread, memory, dataflow bool
}
func (san *sanitizerOptions) resourcePath() string {
return filepath.Join(san.crtPrefix, "lib"+LibDirSuffix, "clang", llvmVersion())
}
func (san *sanitizerOptions) isPIEDefault() bool {
return san.thread || san.memory || san.dataflow
}
func (san *sanitizerOptions) addPasses(mpm, fpm llvm.PassManager) {
switch {
case san.address:
mpm.AddAddressSanitizerModulePass()
fpm.AddAddressSanitizerFunctionPass()
case san.thread:
mpm.AddThreadSanitizerPass()
case san.memory:
mpm.AddMemorySanitizerLegacyPassPass()
case san.dataflow:
blacklist := san.blacklist
if blacklist == "" {
blacklist = filepath.Join(san.resourcePath(), "dfsan_abilist.txt")
}
mpm.AddDataFlowSanitizerPass([]string{blacklist})
}
}
func (san *sanitizerOptions) libPath(triple, sanitizerName string) string {
s := strings.Split(triple, "-")
return filepath.Join(san.resourcePath(), "lib", s[2], "libclang_rt."+sanitizerName+"-"+s[0]+".a")
}
func (san *sanitizerOptions) addLibsForSanitizer(flags []string, triple, sanitizerName string) []string {
return append(flags, san.libPath(triple, sanitizerName),
"-Wl,--no-as-needed", "-lpthread", "-lrt", "-lm", "-ldl")
}
func (san *sanitizerOptions) addLibs(triple string, flags []string) []string {
switch {
case san.address:
flags = san.addLibsForSanitizer(flags, triple, "asan")
case san.thread:
flags = san.addLibsForSanitizer(flags, triple, "tsan")
case san.memory:
flags = san.addLibsForSanitizer(flags, triple, "msan")
case san.dataflow:
flags = san.addLibsForSanitizer(flags, triple, "dfsan")
}
return flags
}
func (san *sanitizerOptions) getAttribute() llvm.Attribute {
var attrKind uint
switch {
case san.address:
attrKind = llvm.AttributeKindID("sanitize_address")
case san.thread:
attrKind = llvm.AttributeKindID("sanitize_thread")
case san.memory:
attrKind = llvm.AttributeKindID("sanitize_memory")
default:
attrKind = 0
}
ctx := llvm.GlobalContext()
return ctx.CreateEnumAttribute(attrKind, 0)
}
type driverOptions struct {
actions []action
output string
bprefix string
debugPrefixMaps []debug.PrefixMap
dumpSSA bool
dumpTrace bool
emitIR bool
gccgoPath string
generateDebug bool
importPaths []string
libPaths []string
llvmArgs []string
lto bool
optLevel int
pic bool
pieLink bool
pkgpath string
plugins []string
prefix string
sanitizer sanitizerOptions
sizeLevel int
staticLibgcc bool
staticLibgo bool
staticLink bool
triple string
}
func getInstPrefix() (string, error) {
path, err := exec.LookPath(os.Args[0])
if err != nil {
return "", err
}
path, err = filepath.EvalSymlinks(path)
if err != nil {
return "", err
}
prefix := filepath.Join(path, "..", "..")
return prefix, nil
}
func parseArguments(args []string) (opts driverOptions, err error) {
var goInputs, otherInputs []string
hasOtherNonFlagInputs := false
noPrefix := false
actionKind := actionLink
opts.triple = llvm.DefaultTargetTriple()
for len(args) > 0 {
consumedArgs := 1
switch {
case !strings.HasPrefix(args[0], "-"):
if strings.HasSuffix(args[0], ".go") {
goInputs = append(goInputs, args[0])
} else {
hasOtherNonFlagInputs = true
otherInputs = append(otherInputs, args[0])
}
case strings.HasPrefix(args[0], "-Wl,"), strings.HasPrefix(args[0], "-l"), strings.HasPrefix(args[0], "--sysroot="):
// TODO(pcc): Handle these correctly.
otherInputs = append(otherInputs, args[0])
case args[0] == "-B":
if len(args) == 1 {
return opts, errors.New("missing argument after '-B'")
}
opts.bprefix = args[1]
consumedArgs = 2
case args[0] == "-D":
if len(args) == 1 {
return opts, errors.New("missing argument after '-D'")
}
otherInputs = append(otherInputs, args[0], args[1])
consumedArgs = 2
case strings.HasPrefix(args[0], "-D"):
otherInputs = append(otherInputs, args[0])
case args[0] == "-I":
if len(args) == 1 {
return opts, errors.New("missing argument after '-I'")
}
opts.importPaths = append(opts.importPaths, args[1])
consumedArgs = 2
case strings.HasPrefix(args[0], "-I"):
opts.importPaths = append(opts.importPaths, args[0][2:])
case args[0] == "-isystem":
if len(args) == 1 {
return opts, errors.New("missing argument after '-isystem'")
}
otherInputs = append(otherInputs, args[0], args[1])
consumedArgs = 2
case args[0] == "-L":
if len(args) == 1 {
return opts, errors.New("missing argument after '-L'")
}
opts.libPaths = append(opts.libPaths, args[1])
consumedArgs = 2
case strings.HasPrefix(args[0], "-L"):
opts.libPaths = append(opts.libPaths, args[0][2:])
case args[0] == "-O0":
opts.optLevel = 0
case args[0] == "-O1", args[0] == "-O":
opts.optLevel = 1
case args[0] == "-O2":
opts.optLevel = 2
case args[0] == "-Os":
opts.optLevel = 2
opts.sizeLevel = 1
case args[0] == "-O3":
opts.optLevel = 3
case args[0] == "-S":
actionKind = actionAssemble
case args[0] == "-c":
actionKind = actionCompile
case strings.HasPrefix(args[0], "-fcompilerrt-prefix="):
opts.sanitizer.crtPrefix = args[0][20:]
case strings.HasPrefix(args[0], "-fdebug-prefix-map="):
split := strings.SplitN(args[0][19:], "=", 2)
if len(split) < 2 {
return opts, fmt.Errorf("argument '%s' must be of form '-fdebug-prefix-map=SOURCE=REPLACEMENT'", args[0])
}
opts.debugPrefixMaps = append(opts.debugPrefixMaps, debug.PrefixMap{split[0], split[1]})
case args[0] == "-fdump-ssa":
opts.dumpSSA = true
case args[0] == "-fdump-trace":
opts.dumpTrace = true
case strings.HasPrefix(args[0], "-fgccgo-path="):
opts.gccgoPath = args[0][13:]
case strings.HasPrefix(args[0], "-fgo-pkgpath="):
opts.pkgpath = args[0][13:]
case strings.HasPrefix(args[0], "-fgo-relative-import-path="):
// TODO(pcc): Handle this.
case strings.HasPrefix(args[0], "-fstack-protector"):
// TODO(axw) set ssp function attributes. This can be useful
// even for Go, if it interfaces with code written in a non-
// memory safe language (e.g. via cgo).
case strings.HasPrefix(args[0], "-W"):
// Go doesn't do warnings. Ignore.
case args[0] == "-fload-plugin":
if len(args) == 1 {
return opts, errors.New("missing argument after '-fload-plugin'")
}
opts.plugins = append(opts.plugins, args[1])
consumedArgs = 2
case args[0] == "-fno-toplevel-reorder":
// This is a GCC-specific code generation option. Ignore.
case args[0] == "-emit-llvm":
opts.emitIR = true
case args[0] == "-flto":
opts.lto = true
case args[0] == "-fPIC":
opts.pic = true
case strings.HasPrefix(args[0], "-fsanitize-blacklist="):
opts.sanitizer.blacklist = args[0][21:]
// TODO(pcc): Enforce mutual exclusion between sanitizers.
case args[0] == "-fsanitize=address":
opts.sanitizer.address = true
case args[0] == "-fsanitize=thread":
opts.sanitizer.thread = true
case args[0] == "-fsanitize=memory":
opts.sanitizer.memory = true
case args[0] == "-fsanitize=dataflow":
opts.sanitizer.dataflow = true
case args[0] == "-g":
opts.generateDebug = true
case args[0] == "-mllvm":
if len(args) == 1 {
return opts, errors.New("missing argument after '-mllvm'")
}
opts.llvmArgs = append(opts.llvmArgs, args[1])
consumedArgs = 2
case strings.HasPrefix(args[0], "-m"), args[0] == "-funsafe-math-optimizations", args[0] == "-ffp-contract=off":
// TODO(pcc): Handle code generation options.
case args[0] == "-no-prefix":
noPrefix = true
case args[0] == "-o":
if len(args) == 1 {
return opts, errors.New("missing argument after '-o'")
}
opts.output = args[1]
consumedArgs = 2
case args[0] == "-pie":
opts.pieLink = true
case args[0] == "-dumpversion",
args[0] == "-print-libgcc-file-name",
args[0] == "-print-multi-os-directory",
args[0] == "--version":
actionKind = actionPrint
opts.output = args[0]
case args[0] == "-static":
opts.staticLink = true
case args[0] == "-static-libgcc":
opts.staticLibgcc = true
case args[0] == "-static-libgo":
opts.staticLibgo = true
default:
return opts, fmt.Errorf("unrecognized command line option '%s'", args[0])
}
args = args[consumedArgs:]
}
if actionKind != actionPrint && len(goInputs) == 0 && !hasOtherNonFlagInputs {
return opts, errors.New("no input files")
}
if !noPrefix {
opts.prefix, err = getInstPrefix()
if err != nil {
return opts, err
}
}
if opts.sanitizer.crtPrefix == "" {
opts.sanitizer.crtPrefix = opts.prefix
}
if opts.sanitizer.isPIEDefault() {
// This should really only be turning on -fPIE, but this isn't
// easy to do from Go, and -fPIC is a superset of it anyway.
opts.pic = true
opts.pieLink = true
}
switch actionKind {
case actionLink:
if len(goInputs) != 0 {
opts.actions = []action{action{actionCompile, goInputs}}
}
opts.actions = append(opts.actions, action{actionLink, otherInputs})
case actionCompile, actionAssemble:
if len(goInputs) != 0 {
opts.actions = []action{action{actionKind, goInputs}}
}
case actionPrint:
opts.actions = []action{action{actionKind, nil}}
}
if opts.output == "" && len(opts.actions) != 0 {
switch actionKind {
case actionCompile, actionAssemble:
base := filepath.Base(goInputs[0])
base = base[0 : len(base)-3]
if actionKind == actionCompile {
opts.output = base + ".o"
} else {
opts.output = base + ".s"
}
case actionLink:
opts.output = "a.out"
}
}
return opts, nil
}
func runPasses(opts *driverOptions, tm llvm.TargetMachine, m llvm.Module) {
fpm := llvm.NewFunctionPassManagerForModule(m)
defer fpm.Dispose()
mpm := llvm.NewPassManager()
defer mpm.Dispose()
pmb := llvm.NewPassManagerBuilder()
defer pmb.Dispose()
pmb.SetOptLevel(opts.optLevel)
pmb.SetSizeLevel(opts.sizeLevel)
tm.AddAnalysisPasses(mpm)
tm.AddAnalysisPasses(fpm)
mpm.AddVerifierPass()
fpm.AddVerifierPass()
pmb.Populate(mpm)
pmb.PopulateFunc(fpm)
if opts.optLevel == 0 {
// Remove references (via the descriptor) to dead functions,
// for compatibility with other compilers.
mpm.AddGlobalDCEPass()
}
opts.sanitizer.addPasses(mpm, fpm)
fpm.InitializeFunc()
for fn := m.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
fpm.RunFunc(fn)
}
fpm.FinalizeFunc()
mpm.Run(m)
}
func getMetadataSectionInlineAsm(name string) string {
// ELF: creates a non-allocated excluded section.
return ".section \"" + name + "\", \"e\"\n"
}
func getDataInlineAsm(data []byte) string {
edata := make([]byte, 0, len(data)*4+10)
edata = append(edata, ".ascii \""...)
for i := range data {
switch data[i] {
case '\000':
edata = append(edata, "\\000"...)
continue
case '\n':
edata = append(edata, "\\n"...)
continue
case '"', '\\':
edata = append(edata, '\\')
}
edata = append(edata, data[i])
}
edata = append(edata, "\"\n"...)
return string(edata)
}
// Get the lib path to the standard libraries for the given driver options.
// This is normally 'lib' but can vary for cross compilation, LTO, sanitizers
// etc.
func getLibDir(opts *driverOptions) string {
lib := "lib" + LibDirSuffix
switch {
case opts.lto:
return filepath.Join(lib, "llvm-lto.0")
case opts.sanitizer.address:
return filepath.Join(lib, "llvm-asan.0")
case opts.sanitizer.thread:
return filepath.Join(lib, "llvm-tsan.0")
case opts.sanitizer.memory:
return filepath.Join(lib, "llvm-msan.0")
case opts.sanitizer.dataflow:
return filepath.Join(lib, "llvm-dfsan.0")
default:
return lib
}
}
func performAction(opts *driverOptions, kind actionKind, inputs []string, output string) error {
switch kind {
case actionPrint:
switch opts.output {
case "-dumpversion":
fmt.Println("llgo-" + llvmVersion())
return nil
case "-print-libgcc-file-name":
cmd := exec.Command(opts.bprefix+"gcc", "-print-libgcc-file-name")
out, err := cmd.CombinedOutput()
os.Stdout.Write(out)
return err
case "-print-multi-os-directory":
fmt.Println(filepath.Join("..", getLibDir(opts)))
return nil
case "--version":
displayVersion()
return nil
default:
panic("unexpected print command")
}
case actionCompile, actionAssemble:
compiler, err := initCompiler(opts)
if err != nil {
return err
}
fset := token.NewFileSet()
files, err := driver.ParseFiles(fset, inputs)
if err != nil {
return err
}
module, err := compiler.Compile(fset, files, opts.pkgpath)
if err != nil {
return err
}
defer module.Dispose()
target, err := llvm.GetTargetFromTriple(opts.triple)
if err != nil {
return err
}
optLevel := [...]llvm.CodeGenOptLevel{
llvm.CodeGenLevelNone,
llvm.CodeGenLevelLess,
llvm.CodeGenLevelDefault,
llvm.CodeGenLevelAggressive,
}[opts.optLevel]
relocMode := llvm.RelocStatic
if opts.pic {
relocMode = llvm.RelocPIC
}
tm := target.CreateTargetMachine(opts.triple, "", "", optLevel,
relocMode, llvm.CodeModelDefault)
defer tm.Dispose()
runPasses(opts, tm, module.Module)
var file *os.File
if output == "-" {
file = os.Stdout
} else {
file, err = os.Create(output)
if err != nil {
return err
}
defer file.Close()
}
switch {
case !opts.lto && !opts.emitIR:
if module.ExportData != nil {
asm := getMetadataSectionInlineAsm(".go_export")
asm += getDataInlineAsm(module.ExportData)
module.Module.SetInlineAsm(asm)
}
fileType := llvm.AssemblyFile
if kind == actionCompile {
fileType = llvm.ObjectFile
}
mb, err := tm.EmitToMemoryBuffer(module.Module, fileType)
if err != nil {
return err
}
defer mb.Dispose()
bytes := mb.Bytes()
_, err = file.Write(bytes)
return err
case opts.lto:
bcmb := llvm.WriteBitcodeToMemoryBuffer(module.Module)
defer bcmb.Dispose()
// This is a bit of a hack. We just want an object file
// containing some metadata sections. This might be simpler
// if we had bindings for the MC library, but for now we create
// a fresh module containing only inline asm that creates the
// sections.
outmodule := llvm.NewModule("")
defer outmodule.Dispose()
asm := getMetadataSectionInlineAsm(".llvmbc")
asm += getDataInlineAsm(bcmb.Bytes())
if module.ExportData != nil {
asm += getMetadataSectionInlineAsm(".go_export")
asm += getDataInlineAsm(module.ExportData)
}
outmodule.SetInlineAsm(asm)
fileType := llvm.AssemblyFile
if kind == actionCompile {
fileType = llvm.ObjectFile
}
mb, err := tm.EmitToMemoryBuffer(outmodule, fileType)
if err != nil {
return err
}
defer mb.Dispose()
bytes := mb.Bytes()
_, err = file.Write(bytes)
return err
case kind == actionCompile:
err := llvm.WriteBitcodeToFile(module.Module, file)
return err
case kind == actionAssemble:
_, err := file.WriteString(module.Module.String())
return err
default:
panic("unexpected action kind")
}
case actionLink:
// TODO(pcc): Teach this to do LTO.
args := []string{"-o", output}
if opts.pic {
args = append(args, "-fPIC")
}
if opts.pieLink {
args = append(args, "-pie")
}
if opts.staticLink {
args = append(args, "-static")
}
if opts.staticLibgcc {
args = append(args, "-static-libgcc")
}
for _, p := range opts.libPaths {
args = append(args, "-L", p)
}
for _, p := range opts.importPaths {
args = append(args, "-I", p)
}
args = append(args, inputs...)
var linkerPath string
if opts.gccgoPath == "" {
// TODO(pcc): See if we can avoid calling gcc here.
// We currently rely on it to find crt*.o and compile
// any C source files passed as arguments.
linkerPath = opts.bprefix + "gcc"
if opts.prefix != "" {
libdir := filepath.Join(opts.prefix, getLibDir(opts))
args = append(args, "-L", libdir)
if !opts.staticLibgo {
args = append(args, "-Wl,-rpath,"+libdir)
}
}
args = append(args, "-lgobegin-llgo")
if opts.staticLibgo {
args = append(args, "-Wl,-Bstatic", "-lgo-llgo", "-Wl,-Bdynamic", "-lpthread", "-lm")
} else {
args = append(args, "-lgo-llgo", "-lm")
}
} else {
linkerPath = opts.gccgoPath
if opts.staticLibgo {
args = append(args, "-static-libgo")
}
}
args = opts.sanitizer.addLibs(opts.triple, args)
cmd := exec.Command(linkerPath, args...)
out, err := cmd.CombinedOutput()
if err != nil {
os.Stderr.Write(out)
}
return err
default:
panic("unexpected action kind")
}
}
func performActions(opts *driverOptions) error {
var extraInput string
for _, plugin := range opts.plugins {
err := llvm.LoadLibraryPermanently(plugin)
if err != nil {
return err
}
}
llvm.ParseCommandLineOptions(append([]string{"llgo"}, opts.llvmArgs...), "llgo (LLVM option parsing)\n")
for i, action := range opts.actions {
var output string
if i == len(opts.actions)-1 {
output = opts.output
} else {
tmpfile, err := ioutil.TempFile("", "llgo")
if err != nil {
return err
}
output = tmpfile.Name() + ".o"
tmpfile.Close()
err = os.Remove(tmpfile.Name())
if err != nil {
return err
}
defer os.Remove(output)
}
inputs := action.inputs
if extraInput != "" {
inputs = append([]string{extraInput}, inputs...)
}
err := performAction(opts, action.kind, inputs, output)
if err != nil {
return err
}
extraInput = output
}
return nil
}
func main() {
llvm.InitializeAllTargets()
llvm.InitializeAllTargetMCs()
llvm.InitializeAllTargetInfos()
llvm.InitializeAllAsmParsers()
llvm.InitializeAllAsmPrinters()
opts, err := parseArguments(os.Args[1:])
if err != nil {
report(err)
os.Exit(1)
}
err = performActions(&opts)
if err != nil {
report(err)
os.Exit(1)
}
}