Skip to content

Commit 2e76cd3

Browse files
aykevldeadprogram
authored andcommitted
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.
1 parent 2eb3978 commit 2e76cd3

File tree

9 files changed

+220
-44
lines changed

9 files changed

+220
-44
lines changed

builder/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
779779
}
780780
err = link(config.Target.Linker, ldflags...)
781781
if err != nil {
782-
return &commandError{"failed to link", result.Executable, err}
782+
return err
783783
}
784784

785785
var calculatedStacks []string

builder/error.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ func (e *MultiError) Error() string {
1515

1616
// newMultiError returns a *MultiError if there is more than one error, or
1717
// returns that error directly when there is only one. Passing an empty slice
18-
// will lead to a panic.
18+
// will return nil (because there is no error).
1919
// The importPath may be passed if this error is for a single package.
2020
func newMultiError(errs []error, importPath string) error {
2121
switch len(errs) {
2222
case 0:
23-
panic("attempted to create empty MultiError")
23+
return nil
2424
case 1:
2525
return errs[0]
2626
default:

builder/tools.go

Lines changed: 134 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package builder
22

33
import (
4+
"bytes"
5+
"fmt"
6+
"go/scanner"
7+
"go/token"
48
"os"
59
"os/exec"
6-
7-
"github.com/tinygo-org/tinygo/goenv"
10+
"regexp"
11+
"strconv"
12+
"strings"
813
)
914

1015
// runCCompiler invokes a C compiler with the given arguments.
@@ -23,22 +28,135 @@ func runCCompiler(flags ...string) error {
2328

2429
// link invokes a linker with the given name and flags.
2530
func link(linker string, flags ...string) error {
26-
if hasBuiltinTools && (linker == "ld.lld" || linker == "wasm-ld") {
27-
// Run command with internal linker.
28-
cmd := exec.Command(os.Args[0], append([]string{linker}, flags...)...)
29-
cmd.Stdout = os.Stdout
30-
cmd.Stderr = os.Stderr
31-
return cmd.Run()
31+
// We only support LLD.
32+
if linker != "ld.lld" && linker != "wasm-ld" {
33+
return fmt.Errorf("unexpected: linker %s should be ld.lld or wasm-ld", linker)
3234
}
3335

34-
// Fall back to external command.
35-
if _, ok := commands[linker]; ok {
36-
return execCommand(linker, flags...)
36+
var cmd *exec.Cmd
37+
if hasBuiltinTools {
38+
cmd = exec.Command(os.Args[0], append([]string{linker}, flags...)...)
39+
} else {
40+
name, err := LookupCommand(linker)
41+
if err != nil {
42+
return err
43+
}
44+
cmd = exec.Command(name, flags...)
3745
}
38-
39-
cmd := exec.Command(linker, flags...)
46+
var buf bytes.Buffer
4047
cmd.Stdout = os.Stdout
41-
cmd.Stderr = os.Stderr
42-
cmd.Dir = goenv.Get("TINYGOROOT")
43-
return cmd.Run()
48+
cmd.Stderr = &buf
49+
err := cmd.Run()
50+
if err != nil {
51+
if buf.Len() == 0 {
52+
// The linker failed but ther was no output.
53+
// Therefore, show some output anyway.
54+
return fmt.Errorf("failed to run linker: %w", err)
55+
}
56+
return parseLLDErrors(buf.String())
57+
}
58+
return nil
59+
}
60+
61+
// Split LLD errors into individual erros (including errors that continue on the
62+
// next line, using a ">>>" prefix). If possible, replace the raw errors with a
63+
// more user-friendly version (and one that's more in a Go style).
64+
func parseLLDErrors(text string) error {
65+
// Split linker output in separate error messages.
66+
lines := strings.Split(text, "\n")
67+
var errorLines []string // one or more line (belonging to a single error) per line
68+
for _, line := range lines {
69+
line = strings.TrimRight(line, "\r") // needed for Windows
70+
if len(errorLines) != 0 && strings.HasPrefix(line, ">>> ") {
71+
errorLines[len(errorLines)-1] += "\n" + line
72+
continue
73+
}
74+
if line == "" {
75+
continue
76+
}
77+
errorLines = append(errorLines, line)
78+
}
79+
80+
// Parse error messages.
81+
var linkErrors []error
82+
var flashOverflow, ramOverflow uint64
83+
for _, message := range errorLines {
84+
parsedError := false
85+
86+
// Check for undefined symbols.
87+
// This can happen in some cases like with CGo and //go:linkname tricker.
88+
if matches := regexp.MustCompile(`^ld.lld: error: undefined symbol: (.*)\n`).FindStringSubmatch(message); matches != nil {
89+
symbolName := matches[1]
90+
for _, line := range strings.Split(message, "\n") {
91+
matches := regexp.MustCompile(`referenced by .* \(((.*):([0-9]+))\)`).FindStringSubmatch(line)
92+
if matches != nil {
93+
parsedError = true
94+
line, _ := strconv.Atoi(matches[3])
95+
// TODO: detect common mistakes like -gc=none?
96+
linkErrors = append(linkErrors, scanner.Error{
97+
Pos: token.Position{
98+
Filename: matches[2],
99+
Line: line,
100+
},
101+
Msg: "linker could not find symbol " + symbolName,
102+
})
103+
}
104+
}
105+
}
106+
107+
// Check for flash/RAM overflow.
108+
if matches := regexp.MustCompile(`^ld.lld: error: section '(.*?)' will not fit in region '(.*?)': overflowed by ([0-9]+) bytes$`).FindStringSubmatch(message); matches != nil {
109+
region := matches[2]
110+
n, err := strconv.ParseUint(matches[3], 10, 64)
111+
if err != nil {
112+
// Should not happen at all (unless it overflows an uint64 for some reason).
113+
continue
114+
}
115+
116+
// Check which area overflowed.
117+
// Some chips use differently named memory areas, but these are by
118+
// far the most common.
119+
switch region {
120+
case "FLASH_TEXT":
121+
if n > flashOverflow {
122+
flashOverflow = n
123+
}
124+
parsedError = true
125+
case "RAM":
126+
if n > ramOverflow {
127+
ramOverflow = n
128+
}
129+
parsedError = true
130+
}
131+
}
132+
133+
// If we couldn't parse the linker error: show the error as-is to
134+
// the user.
135+
if !parsedError {
136+
linkErrors = append(linkErrors, LinkerError{message})
137+
}
138+
}
139+
140+
if flashOverflow > 0 {
141+
linkErrors = append(linkErrors, LinkerError{
142+
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),
143+
})
144+
}
145+
if ramOverflow > 0 {
146+
linkErrors = append(linkErrors, LinkerError{
147+
Msg: fmt.Sprintf("program uses too much static RAM on this chip (RAM overflowed by %d bytes)", ramOverflow),
148+
})
149+
}
150+
151+
return newMultiError(linkErrors, "")
152+
}
153+
154+
// LLD linker error that could not be parsed or doesn't refer to a source
155+
// location.
156+
type LinkerError struct {
157+
Msg string
158+
}
159+
160+
func (e LinkerError) Error() string {
161+
return e.Msg
44162
}

errors_test.go

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,62 @@ import (
77
"regexp"
88
"strings"
99
"testing"
10-
"time"
1110

1211
"github.com/tinygo-org/tinygo/compileopts"
1312
"github.com/tinygo-org/tinygo/diagnostics"
1413
)
1514

1615
// Test the error messages of the TinyGo compiler.
1716
func TestErrors(t *testing.T) {
18-
for _, name := range []string{
19-
"cgo",
20-
"compiler",
21-
"interp",
22-
"loader-importcycle",
23-
"loader-invaliddep",
24-
"loader-invalidpackage",
25-
"loader-nopackage",
26-
"optimizer",
27-
"syntax",
28-
"types",
17+
// TODO: nicely formatted error messages for:
18+
// - duplicate symbols in ld.lld (currently only prints bitcode file)
19+
type errorTest struct {
20+
name string
21+
target string
22+
}
23+
for _, tc := range []errorTest{
24+
{name: "cgo"},
25+
{name: "compiler"},
26+
{name: "interp"},
27+
{name: "linker-flashoverflow", target: "cortex-m-qemu"},
28+
{name: "linker-ramoverflow", target: "cortex-m-qemu"},
29+
{name: "linker-undefined", target: "darwin/arm64"},
30+
{name: "linker-undefined", target: "linux/amd64"},
31+
//{name: "linker-undefined", target: "windows/amd64"}, // TODO: no source location
32+
{name: "linker-undefined", target: "cortex-m-qemu"},
33+
//{name: "linker-undefined", target: "wasip1"}, // TODO: no source location
34+
{name: "loader-importcycle"},
35+
{name: "loader-invaliddep"},
36+
{name: "loader-invalidpackage"},
37+
{name: "loader-nopackage"},
38+
{name: "optimizer"},
39+
{name: "syntax"},
40+
{name: "types"},
2941
} {
42+
name := tc.name
43+
if tc.target != "" {
44+
name += "#" + tc.target
45+
}
46+
target := tc.target
47+
if target == "" {
48+
target = "wasip1"
49+
}
3050
t.Run(name, func(t *testing.T) {
31-
testErrorMessages(t, "./testdata/errors/"+name+".go")
51+
options := optionsFromTarget(target, sema)
52+
testErrorMessages(t, "./testdata/errors/"+tc.name+".go", &options)
3253
})
3354
}
3455
}
3556

36-
func testErrorMessages(t *testing.T, filename string) {
57+
func testErrorMessages(t *testing.T, filename string, options *compileopts.Options) {
58+
t.Parallel()
59+
3760
// Parse expected error messages.
3861
expected := readErrorMessages(t, filename)
3962

4063
// Try to build a binary (this should fail with an error).
4164
tmpdir := t.TempDir()
42-
err := Build(filename, tmpdir+"/out", &compileopts.Options{
43-
Target: "wasip1",
44-
Semaphore: sema,
45-
InterpTimeout: 180 * time.Second,
46-
Debug: true,
47-
VerifyIR: true,
48-
Opt: "z",
49-
})
65+
err := Build(filename, tmpdir+"/out", options)
5066
if err == nil {
5167
t.Fatal("expected to get a compiler error")
5268
}

main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1467,7 +1467,8 @@ func main() {
14671467
case "clang", "ld.lld", "wasm-ld":
14681468
err := builder.RunTool(command, os.Args[2:]...)
14691469
if err != nil {
1470-
fmt.Fprintln(os.Stderr, err)
1470+
// The tool should have printed an error message already.
1471+
// Don't print another error message here.
14711472
os.Exit(1)
14721473
}
14731474
os.Exit(0)

main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"bytes"
99
"errors"
1010
"flag"
11-
"fmt"
1211
"io"
1312
"os"
1413
"os/exec"
@@ -696,7 +695,8 @@ func TestMain(m *testing.M) {
696695
// Invoke a specific tool.
697696
err := builder.RunTool(os.Args[1], os.Args[2:]...)
698697
if err != nil {
699-
fmt.Fprintln(os.Stderr, err)
698+
// The tool should have printed an error message already.
699+
// Don't print another error message here.
700700
os.Exit(1)
701701
}
702702
os.Exit(0)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import "unsafe"
4+
5+
const (
6+
a = "0123456789abcdef" // 16 bytes
7+
b = a + a + a + a + a + a + a + a // 128 bytes
8+
c = b + b + b + b + b + b + b + b // 1024 bytes
9+
d = c + c + c + c + c + c + c + c // 8192 bytes
10+
e = d + d + d + d + d + d + d + d // 65536 bytes
11+
f = e + e + e + e + e + e + e + e // 524288 bytes
12+
)
13+
14+
var s = f
15+
16+
func main() {
17+
println(unsafe.StringData(s))
18+
}
19+
20+
// ERROR: program too large for this chip (flash overflowed by {{[0-9]+}} bytes)
21+
// ERROR: optimization guide: https://tinygo.org/docs/guides/optimizing-binaries/

testdata/errors/linker-ramoverflow.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package main
2+
3+
var b [64 << 10]byte // 64kB
4+
5+
func main() {
6+
println("ptr:", &b[0])
7+
}
8+
9+
// ERROR: program uses too much static RAM on this chip (RAM overflowed by {{[0-9]+}} bytes)

testdata/errors/linker-undefined.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package main
2+
3+
func foo()
4+
5+
func main() {
6+
foo()
7+
foo()
8+
}
9+
10+
// ERROR: linker-undefined.go:6: linker could not find symbol {{_?}}main.foo
11+
// ERROR: linker-undefined.go:7: linker could not find symbol {{_?}}main.foo

0 commit comments

Comments
 (0)