builder: interpret linker error messages

This shows nicely formatted error messages for missing symbol names and
for out-of-flash, out-of-RAM conditions (on microcontrollers with
limited flash/RAM).

Unfortunately the missing symbol name errors aren't available on Windows
and WebAssembly because the linker doesn't report source locations yet.
This is something that I could perhaps improve in LLD.
This commit is contained in:
Ayke van Laethem
2024-07-10 13:42:21 +02:00
committed by Ron Evans
parent 2eb39785fe
commit 2e76cd3687
9 changed files with 220 additions and 44 deletions
+1 -1
View File
@@ -779,7 +779,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
} }
err = link(config.Target.Linker, ldflags...) err = link(config.Target.Linker, ldflags...)
if err != nil { if err != nil {
return &commandError{"failed to link", result.Executable, err} return err
} }
var calculatedStacks []string var calculatedStacks []string
+2 -2
View File
@@ -15,12 +15,12 @@ func (e *MultiError) Error() string {
// newMultiError returns a *MultiError if there is more than one error, or // newMultiError returns a *MultiError if there is more than one error, or
// returns that error directly when there is only one. Passing an empty slice // returns that error directly when there is only one. Passing an empty slice
// will lead to a panic. // will return nil (because there is no error).
// The importPath may be passed if this error is for a single package. // The importPath may be passed if this error is for a single package.
func newMultiError(errs []error, importPath string) error { func newMultiError(errs []error, importPath string) error {
switch len(errs) { switch len(errs) {
case 0: case 0:
panic("attempted to create empty MultiError") return nil
case 1: case 1:
return errs[0] return errs[0]
default: default:
+134 -16
View File
@@ -1,10 +1,15 @@
package builder package builder
import ( import (
"bytes"
"fmt"
"go/scanner"
"go/token"
"os" "os"
"os/exec" "os/exec"
"regexp"
"github.com/tinygo-org/tinygo/goenv" "strconv"
"strings"
) )
// runCCompiler invokes a C compiler with the given arguments. // runCCompiler invokes a C compiler with the given arguments.
@@ -23,22 +28,135 @@ func runCCompiler(flags ...string) error {
// link invokes a linker with the given name and flags. // link invokes a linker with the given name and flags.
func link(linker string, flags ...string) error { func link(linker string, flags ...string) error {
if hasBuiltinTools && (linker == "ld.lld" || linker == "wasm-ld") { // We only support LLD.
// Run command with internal linker. if linker != "ld.lld" && linker != "wasm-ld" {
cmd := exec.Command(os.Args[0], append([]string{linker}, flags...)...) return fmt.Errorf("unexpected: linker %s should be ld.lld or wasm-ld", linker)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
} }
// Fall back to external command. var cmd *exec.Cmd
if _, ok := commands[linker]; ok { if hasBuiltinTools {
return execCommand(linker, flags...) cmd = exec.Command(os.Args[0], append([]string{linker}, flags...)...)
} else {
name, err := LookupCommand(linker)
if err != nil {
return err
}
cmd = exec.Command(name, flags...)
} }
var buf bytes.Buffer
cmd := exec.Command(linker, flags...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = &buf
cmd.Dir = goenv.Get("TINYGOROOT") err := cmd.Run()
return cmd.Run() if err != nil {
if buf.Len() == 0 {
// The linker failed but ther was no output.
// Therefore, show some output anyway.
return fmt.Errorf("failed to run linker: %w", err)
}
return parseLLDErrors(buf.String())
}
return nil
}
// Split LLD errors into individual erros (including errors that continue on the
// next line, using a ">>>" prefix). If possible, replace the raw errors with a
// more user-friendly version (and one that's more in a Go style).
func parseLLDErrors(text string) error {
// Split linker output in separate error messages.
lines := strings.Split(text, "\n")
var errorLines []string // one or more line (belonging to a single error) per line
for _, line := range lines {
line = strings.TrimRight(line, "\r") // needed for Windows
if len(errorLines) != 0 && strings.HasPrefix(line, ">>> ") {
errorLines[len(errorLines)-1] += "\n" + line
continue
}
if line == "" {
continue
}
errorLines = append(errorLines, line)
}
// Parse error messages.
var linkErrors []error
var flashOverflow, ramOverflow uint64
for _, message := range errorLines {
parsedError := false
// Check for undefined symbols.
// This can happen in some cases like with CGo and //go:linkname tricker.
if matches := regexp.MustCompile(`^ld.lld: error: undefined symbol: (.*)\n`).FindStringSubmatch(message); matches != nil {
symbolName := matches[1]
for _, line := range strings.Split(message, "\n") {
matches := regexp.MustCompile(`referenced by .* \(((.*):([0-9]+))\)`).FindStringSubmatch(line)
if matches != nil {
parsedError = true
line, _ := strconv.Atoi(matches[3])
// TODO: detect common mistakes like -gc=none?
linkErrors = append(linkErrors, scanner.Error{
Pos: token.Position{
Filename: matches[2],
Line: line,
},
Msg: "linker could not find symbol " + symbolName,
})
}
}
}
// Check for flash/RAM overflow.
if matches := regexp.MustCompile(`^ld.lld: error: section '(.*?)' will not fit in region '(.*?)': overflowed by ([0-9]+) bytes$`).FindStringSubmatch(message); matches != nil {
region := matches[2]
n, err := strconv.ParseUint(matches[3], 10, 64)
if err != nil {
// Should not happen at all (unless it overflows an uint64 for some reason).
continue
}
// Check which area overflowed.
// Some chips use differently named memory areas, but these are by
// far the most common.
switch region {
case "FLASH_TEXT":
if n > flashOverflow {
flashOverflow = n
}
parsedError = true
case "RAM":
if n > ramOverflow {
ramOverflow = n
}
parsedError = true
}
}
// If we couldn't parse the linker error: show the error as-is to
// the user.
if !parsedError {
linkErrors = append(linkErrors, LinkerError{message})
}
}
if flashOverflow > 0 {
linkErrors = append(linkErrors, LinkerError{
Msg: fmt.Sprintf("program too large for this chip (flash overflowed by %d bytes)\n\toptimization guide: https://tinygo.org/docs/guides/optimizing-binaries/", flashOverflow),
})
}
if ramOverflow > 0 {
linkErrors = append(linkErrors, LinkerError{
Msg: fmt.Sprintf("program uses too much static RAM on this chip (RAM overflowed by %d bytes)", ramOverflow),
})
}
return newMultiError(linkErrors, "")
}
// LLD linker error that could not be parsed or doesn't refer to a source
// location.
type LinkerError struct {
Msg string
}
func (e LinkerError) Error() string {
return e.Msg
} }
+38 -22
View File
@@ -7,7 +7,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
"time"
"github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/compileopts"
"github.com/tinygo-org/tinygo/diagnostics" "github.com/tinygo-org/tinygo/diagnostics"
@@ -15,38 +14,55 @@ import (
// Test the error messages of the TinyGo compiler. // Test the error messages of the TinyGo compiler.
func TestErrors(t *testing.T) { func TestErrors(t *testing.T) {
for _, name := range []string{ // TODO: nicely formatted error messages for:
"cgo", // - duplicate symbols in ld.lld (currently only prints bitcode file)
"compiler", type errorTest struct {
"interp", name string
"loader-importcycle", target string
"loader-invaliddep", }
"loader-invalidpackage", for _, tc := range []errorTest{
"loader-nopackage", {name: "cgo"},
"optimizer", {name: "compiler"},
"syntax", {name: "interp"},
"types", {name: "linker-flashoverflow", target: "cortex-m-qemu"},
{name: "linker-ramoverflow", target: "cortex-m-qemu"},
{name: "linker-undefined", target: "darwin/arm64"},
{name: "linker-undefined", target: "linux/amd64"},
//{name: "linker-undefined", target: "windows/amd64"}, // TODO: no source location
{name: "linker-undefined", target: "cortex-m-qemu"},
//{name: "linker-undefined", target: "wasip1"}, // TODO: no source location
{name: "loader-importcycle"},
{name: "loader-invaliddep"},
{name: "loader-invalidpackage"},
{name: "loader-nopackage"},
{name: "optimizer"},
{name: "syntax"},
{name: "types"},
} { } {
name := tc.name
if tc.target != "" {
name += "#" + tc.target
}
target := tc.target
if target == "" {
target = "wasip1"
}
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
testErrorMessages(t, "./testdata/errors/"+name+".go") options := optionsFromTarget(target, sema)
testErrorMessages(t, "./testdata/errors/"+tc.name+".go", &options)
}) })
} }
} }
func testErrorMessages(t *testing.T, filename string) { func testErrorMessages(t *testing.T, filename string, options *compileopts.Options) {
t.Parallel()
// Parse expected error messages. // Parse expected error messages.
expected := readErrorMessages(t, filename) expected := readErrorMessages(t, filename)
// Try to build a binary (this should fail with an error). // Try to build a binary (this should fail with an error).
tmpdir := t.TempDir() tmpdir := t.TempDir()
err := Build(filename, tmpdir+"/out", &compileopts.Options{ err := Build(filename, tmpdir+"/out", options)
Target: "wasip1",
Semaphore: sema,
InterpTimeout: 180 * time.Second,
Debug: true,
VerifyIR: true,
Opt: "z",
})
if err == nil { if err == nil {
t.Fatal("expected to get a compiler error") t.Fatal("expected to get a compiler error")
} }
+2 -1
View File
@@ -1467,7 +1467,8 @@ func main() {
case "clang", "ld.lld", "wasm-ld": case "clang", "ld.lld", "wasm-ld":
err := builder.RunTool(command, os.Args[2:]...) err := builder.RunTool(command, os.Args[2:]...)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) // The tool should have printed an error message already.
// Don't print another error message here.
os.Exit(1) os.Exit(1)
} }
os.Exit(0) os.Exit(0)
+2 -2
View File
@@ -8,7 +8,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"flag" "flag"
"fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@@ -696,7 +695,8 @@ func TestMain(m *testing.M) {
// Invoke a specific tool. // Invoke a specific tool.
err := builder.RunTool(os.Args[1], os.Args[2:]...) err := builder.RunTool(os.Args[1], os.Args[2:]...)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) // The tool should have printed an error message already.
// Don't print another error message here.
os.Exit(1) os.Exit(1)
} }
os.Exit(0) os.Exit(0)
+21
View File
@@ -0,0 +1,21 @@
package main
import "unsafe"
const (
a = "0123456789abcdef" // 16 bytes
b = a + a + a + a + a + a + a + a // 128 bytes
c = b + b + b + b + b + b + b + b // 1024 bytes
d = c + c + c + c + c + c + c + c // 8192 bytes
e = d + d + d + d + d + d + d + d // 65536 bytes
f = e + e + e + e + e + e + e + e // 524288 bytes
)
var s = f
func main() {
println(unsafe.StringData(s))
}
// ERROR: program too large for this chip (flash overflowed by {{[0-9]+}} bytes)
// ERROR: optimization guide: https://tinygo.org/docs/guides/optimizing-binaries/
+9
View File
@@ -0,0 +1,9 @@
package main
var b [64 << 10]byte // 64kB
func main() {
println("ptr:", &b[0])
}
// ERROR: program uses too much static RAM on this chip (RAM overflowed by {{[0-9]+}} bytes)
+11
View File
@@ -0,0 +1,11 @@
package main
func foo()
func main() {
foo()
foo()
}
// ERROR: linker-undefined.go:6: linker could not find symbol {{_?}}main.foo
// ERROR: linker-undefined.go:7: linker could not find symbol {{_?}}main.foo