Skip to content

Commit ae381e7

Browse files
aykevldeadprogram
authored andcommitted
main: print source location when a panic happens in -monitor
The previous commit started printing the instruction address for runtime panics. This commit starts using this address to print the source location. Here is an example where this feature is very useful. There is a heap allocation in the Bluetooth package, but we don't know where exactly. Printing the instruction address of the panic is already useful, but what is even more useful is looking up this address in the DWARF debug information that's part of the binary: $ tinygo flash -target=circuitplay-bluefruit -monitor ./examples/heartrate Connected to /dev/ttyACM0. Press Ctrl-C to exit. tick 00:00.810 tick 00:01.587 tick 00:02.387 tick 00:03.244 panic: runtime error at 0x00027c4d: alloc in interrupt [tinygo: panic at /home/ayke/src/tinygo/bluetooth/adapter_sd.go:74:4] To be clear, this path isn't stored on the microcontroller. It's stored as part of the build, and `-monitor` just looks up the path from the panic message. Possible enhancements: - Print such an address for regular panics as well. I'm not sure that's so useful, as it's usually a lot easier to look up panics just by their message. - Use runtimePanicAt (instead of runtimePanic) in other locations, if that proves to be beneficial. - Print the TinyGo-generated output in some other color, to distinguish it from the regular console output. - Print more details when panicking (registers, stack values), and print an actual backtrace.
1 parent 3392827 commit ae381e7

File tree

4 files changed

+209
-7
lines changed

4 files changed

+209
-7
lines changed

main.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ func Flash(pkgName, port string, options *compileopts.Options) error {
532532
return fmt.Errorf("unknown flash method: %s", flashMethod)
533533
}
534534
if options.Monitor {
535-
return Monitor("", options)
535+
return Monitor(result.Executable, "", options)
536536
}
537537
return nil
538538
}
@@ -1720,7 +1720,7 @@ func main() {
17201720
os.Exit(1)
17211721
}
17221722
case "monitor":
1723-
err := Monitor(*port, options)
1723+
err := Monitor("", *port, options)
17241724
handleCompilerError(err)
17251725
case "targets":
17261726
dir := filepath.Join(goenv.Get("TINYGOROOT"), "targets")

monitor.go

+134-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
package main
22

33
import (
4+
"debug/dwarf"
5+
"debug/elf"
6+
"debug/macho"
7+
"debug/pe"
8+
"errors"
49
"fmt"
10+
"go/token"
11+
"io"
512
"os"
613
"os/signal"
14+
"regexp"
15+
"strconv"
716
"time"
817

918
"github.com/mattn/go-tty"
@@ -13,7 +22,7 @@ import (
1322
)
1423

1524
// Monitor connects to the given port and reads/writes the serial port.
16-
func Monitor(port string, options *compileopts.Options) error {
25+
func Monitor(executable, port string, options *compileopts.Options) error {
1726
config, err := builder.NewConfig(options)
1827
if err != nil {
1928
return err
@@ -74,17 +83,31 @@ func Monitor(port string, options *compileopts.Options) error {
7483

7584
go func() {
7685
buf := make([]byte, 100*1024)
86+
var line []byte
7787
for {
7888
n, err := p.Read(buf)
7989
if err != nil {
8090
errCh <- fmt.Errorf("read error: %w", err)
8191
return
8292
}
83-
84-
if n == 0 {
85-
continue
93+
start := 0
94+
for i, c := range buf[:n] {
95+
if c == '\n' {
96+
os.Stdout.Write(buf[start : i+1])
97+
start = i + 1
98+
address := extractPanicAddress(line)
99+
if address != 0 {
100+
loc, err := addressToLine(executable, address)
101+
if err == nil && loc.IsValid() {
102+
fmt.Printf("[tinygo: panic at %s]\n", loc.String())
103+
}
104+
}
105+
line = line[:0]
106+
} else {
107+
line = append(line, c)
108+
}
86109
}
87-
fmt.Printf("%v", string(buf[:n]))
110+
os.Stdout.Write(buf[start:n])
88111
}
89112
}()
90113

@@ -104,3 +127,109 @@ func Monitor(port string, options *compileopts.Options) error {
104127

105128
return <-errCh
106129
}
130+
131+
var addressMatch = regexp.MustCompile(`^panic: runtime error at 0x([0-9a-f]+): `)
132+
133+
// Extract the address from the "panic: runtime error at" message.
134+
func extractPanicAddress(line []byte) uint64 {
135+
matches := addressMatch.FindSubmatch(line)
136+
if matches != nil {
137+
address, err := strconv.ParseUint(string(matches[1]), 16, 64)
138+
if err == nil {
139+
return address
140+
}
141+
}
142+
return 0
143+
}
144+
145+
// Convert an address in the binary to a source address location.
146+
func addressToLine(executable string, address uint64) (token.Position, error) {
147+
data, err := readDWARF(executable)
148+
if err != nil {
149+
return token.Position{}, err
150+
}
151+
r := data.Reader()
152+
153+
for {
154+
e, err := r.Next()
155+
if err != nil {
156+
return token.Position{}, err
157+
}
158+
if e == nil {
159+
break
160+
}
161+
switch e.Tag {
162+
case dwarf.TagCompileUnit:
163+
r.SkipChildren()
164+
lr, err := data.LineReader(e)
165+
if err != nil {
166+
return token.Position{}, err
167+
}
168+
var lineEntry = dwarf.LineEntry{
169+
EndSequence: true,
170+
}
171+
for {
172+
// Read the next .debug_line entry.
173+
prevLineEntry := lineEntry
174+
err := lr.Next(&lineEntry)
175+
if err != nil {
176+
if err == io.EOF {
177+
break
178+
}
179+
return token.Position{}, err
180+
}
181+
182+
if prevLineEntry.EndSequence && lineEntry.Address == 0 {
183+
// Tombstone value. This symbol has been removed, for
184+
// example by the --gc-sections linker flag. It is still
185+
// here in the debug information because the linker can't
186+
// just remove this reference.
187+
// Read until the next EndSequence so that this sequence is
188+
// skipped.
189+
// For more details, see (among others):
190+
// https://reviews.llvm.org/D84825
191+
for {
192+
err := lr.Next(&lineEntry)
193+
if err != nil {
194+
return token.Position{}, err
195+
}
196+
if lineEntry.EndSequence {
197+
break
198+
}
199+
}
200+
}
201+
202+
if !prevLineEntry.EndSequence {
203+
// The chunk describes the code from prevLineEntry to
204+
// lineEntry.
205+
if prevLineEntry.Address <= address && lineEntry.Address > address {
206+
return token.Position{
207+
Filename: prevLineEntry.File.Name,
208+
Line: prevLineEntry.Line,
209+
Column: prevLineEntry.Column,
210+
}, nil
211+
}
212+
}
213+
}
214+
}
215+
}
216+
217+
return token.Position{}, nil // location not found
218+
}
219+
220+
// Read the DWARF debug information from a given file (in various formats).
221+
func readDWARF(executable string) (*dwarf.Data, error) {
222+
f, err := os.Open(executable)
223+
if err != nil {
224+
return nil, err
225+
}
226+
if file, err := elf.NewFile(f); err == nil {
227+
return file.DWARF()
228+
} else if file, err := macho.NewFile(f); err == nil {
229+
return file.DWARF()
230+
} else if file, err := pe.NewFile(f); err == nil {
231+
return file.DWARF()
232+
} else {
233+
return nil, errors.New("unknown binary format")
234+
}
235+
}

monitor_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"os/exec"
6+
"path/filepath"
7+
"runtime"
8+
"testing"
9+
"time"
10+
11+
"github.com/tinygo-org/tinygo/builder"
12+
"github.com/tinygo-org/tinygo/compileopts"
13+
)
14+
15+
func TestTraceback(t *testing.T) {
16+
if runtime.GOOS != "linux" {
17+
// We care about testing the ELF format, which is only used on Linux
18+
// (not on MacOS or Windows).
19+
t.Skip("Test only works on Linux")
20+
}
21+
22+
// Build a small binary that only panics.
23+
tmpdir := t.TempDir()
24+
config, err := builder.NewConfig(&compileopts.Options{
25+
GOOS: runtime.GOOS,
26+
GOARCH: runtime.GOARCH,
27+
Opt: "z",
28+
InterpTimeout: time.Minute,
29+
Debug: true,
30+
})
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
result, err := builder.Build("testdata/trivialpanic.go", ".elf", tmpdir, config)
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
39+
// Run this binary, and capture the output.
40+
buf := &bytes.Buffer{}
41+
cmd := exec.Command(result.Binary)
42+
cmd.Stdout = buf
43+
cmd.Stderr = buf
44+
cmd.Run() // this will return an error because of the panic, ignore it
45+
46+
// Extract the "runtime error at" address.
47+
line := bytes.TrimSpace(buf.Bytes())
48+
address := extractPanicAddress(line)
49+
if address == 0 {
50+
t.Fatalf("could not extract panic address from %#v", string(line))
51+
}
52+
53+
// Look up the source location for this address.
54+
location, err := addressToLine(result.Executable, address)
55+
if err != nil {
56+
t.Fatal("could not read source location:", err)
57+
}
58+
59+
// Verify that the source location is as expected.
60+
if filepath.Base(location.Filename) != "trivialpanic.go" {
61+
t.Errorf("expected path to end with trivialpanic.go, got %#v", location.Filename)
62+
}
63+
if location.Line != 6 {
64+
t.Errorf("expected panic location to be line 6, got line %d", location.Line)
65+
}
66+
}

testdata/trivialpanic.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package main
2+
3+
var n *int
4+
5+
func main() {
6+
println(*n) // this will panic
7+
}

0 commit comments

Comments
 (0)