mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
7cbbfd6fc5
Implements `putchar` when `mgbadebug` build tag is set which outputs text through the mGBA debugging output interface. mGBA allows the games running in the emulator to output debug text the process to do so is: 1. set 0x4FFF780 to value of 0xC0DE 2. write text in memory 0x4FFF600 to 0x4FFF700 3. set 0x4FFF700 to the desired log level value from 0x100 (fatal) to 0x104 (debug) Once this is done mGBA will output the text in its logs view as well as through stdout. (Setting the log level to fatal also produces a dialog box with the text) The text output in the CLI is prefixed with `[DEBUG] GBA Debug: ` so I modified the regex used for address matching in panic messages to be able to read the address when the output line doesn't start with `panic`.
425 lines
10 KiB
Go
425 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"debug/dwarf"
|
|
"debug/elf"
|
|
"debug/macho"
|
|
"debug/pe"
|
|
"errors"
|
|
"fmt"
|
|
"go/token"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattn/go-tty"
|
|
"github.com/tinygo-org/tinygo/compileopts"
|
|
|
|
"go.bug.st/serial"
|
|
"go.bug.st/serial/enumerator"
|
|
)
|
|
|
|
// Monitor connects to the given port and reads/writes the serial port.
|
|
func Monitor(executable, port string, config *compileopts.Config) error {
|
|
const timeout = time.Second * 3
|
|
var exit func() // function to be called before exiting
|
|
var serialConn io.ReadWriter
|
|
|
|
if config.Options.Serial == "rtt" {
|
|
// Use the RTT interface, which is documented (in part) here:
|
|
// https://wiki.segger.com/RTT
|
|
|
|
// Try to find the "_SEGGER_RTT" symbol (machine.rttSerialInstance)
|
|
// symbol, which is the RTT control block.
|
|
file, err := elf.Open(executable)
|
|
if err != nil {
|
|
return fmt.Errorf("could not open ELF file to determine RTT control block: %w", err)
|
|
}
|
|
defer file.Close()
|
|
symbols, err := file.Symbols()
|
|
if err != nil {
|
|
return fmt.Errorf("could not read ELF symbol table to determine RTT control block: %w", err)
|
|
}
|
|
var address uint64
|
|
for _, symbol := range symbols {
|
|
if symbol.Name == "_SEGGER_RTT" {
|
|
address = symbol.Value
|
|
break
|
|
}
|
|
}
|
|
if address == 0 {
|
|
return fmt.Errorf("could not find RTT control block in ELF file")
|
|
}
|
|
|
|
// Start an openocd process in the background.
|
|
args, err := config.OpenOCDConfiguration()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
args = append(args,
|
|
"-c", fmt.Sprintf("rtt setup 0x%x 16 \"SEGGER RTT\"", address),
|
|
"-c", "init",
|
|
"-c", "rtt server start 0 0")
|
|
cmd := executeCommand(config.Options, "openocd", args...)
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Stdout = os.Stdout
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cmd.Process.Kill()
|
|
exit = func() {
|
|
// Make sure the openocd process is terminated at exit.
|
|
// This does not happen through the defer above when exiting through
|
|
// os.Exit.
|
|
cmd.Process.Kill()
|
|
}
|
|
|
|
// Read the stderr, which logs various important messages we need.
|
|
r := bufio.NewReader(stderr)
|
|
var telnet net.Conn
|
|
var timeoutAt time.Time
|
|
for {
|
|
// Read the next line from the openocd process.
|
|
lineBytes, err := r.ReadBytes('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
line := string(lineBytes)
|
|
|
|
if line == "Info : rtt: No control block found\n" {
|
|
// Message that is sent back when OpenOCD can't find the control
|
|
// block after a 'rtt start' message.
|
|
if time.Now().After(timeoutAt) {
|
|
return fmt.Errorf("RTT timeout (could not locate RTT control block at 0x%08x)", address)
|
|
}
|
|
time.Sleep(time.Millisecond * 100)
|
|
telnet.Write([]byte("rtt start\r\n"))
|
|
} else if strings.HasPrefix(line, "Info : Listening on port") {
|
|
// We need two different ports for controlling OpenOCD
|
|
// (typically port 4444) and the RTT channel 0 socket (arbitrary
|
|
// port).
|
|
var port int
|
|
var protocol string
|
|
fmt.Sscanf(line, "Info : Listening on port %d for %s connections\n", &port, &protocol)
|
|
if protocol == "telnet" && telnet == nil {
|
|
// Connect to the "telnet" command line interface.
|
|
telnet, err = net.Dial("tcp4", fmt.Sprintf("localhost:%d", port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Tell OpenOCD to start scanning for the RTT control block.
|
|
telnet.Write([]byte("rtt start\r\n"))
|
|
// Also make sure we will time out if the control block just
|
|
// can't be found.
|
|
timeoutAt = time.Now().Add(timeout)
|
|
} else if protocol == "rtt" {
|
|
// Connect to the RTT channel, for both stdin and stdout.
|
|
conn, err := net.Dial("tcp4", fmt.Sprintf("localhost:%d", port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serialConn = conn
|
|
}
|
|
} else if strings.HasPrefix(line, "Info : rtt: Control block found at") {
|
|
// Connection established!
|
|
break
|
|
}
|
|
}
|
|
} else { // -serial=uart or -serial=usb
|
|
var err error
|
|
wait := 300
|
|
for i := 0; i <= wait; i++ {
|
|
port, err = getDefaultPort(port, config.Target.SerialPort)
|
|
if err != nil {
|
|
if i < wait {
|
|
time.Sleep(10 * time.Millisecond)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
|
|
br := config.Options.BaudRate
|
|
if br <= 0 {
|
|
br = 115200
|
|
}
|
|
|
|
wait = 300
|
|
var p serial.Port
|
|
for i := 0; i <= wait; i++ {
|
|
p, err = serial.Open(port, &serial.Mode{BaudRate: br})
|
|
if err != nil {
|
|
if i < wait {
|
|
time.Sleep(10 * time.Millisecond)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
serialConn = p
|
|
break
|
|
}
|
|
defer p.Close()
|
|
}
|
|
|
|
tty, err := tty.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tty.Close()
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, os.Interrupt)
|
|
defer signal.Stop(sig)
|
|
|
|
go func() {
|
|
<-sig
|
|
tty.Close()
|
|
if exit != nil {
|
|
exit()
|
|
}
|
|
os.Exit(0)
|
|
}()
|
|
|
|
fmt.Printf("Connected to %s. Press Ctrl-C to exit.\n", port)
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
go func() {
|
|
buf := make([]byte, 100*1024)
|
|
writer := newOutputWriter(os.Stdout, executable)
|
|
for {
|
|
n, err := serialConn.Read(buf)
|
|
if err != nil {
|
|
errCh <- fmt.Errorf("read error: %w", err)
|
|
return
|
|
}
|
|
writer.Write(buf[:n])
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
r, err := tty.ReadRune()
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
if r == 0 {
|
|
continue
|
|
}
|
|
serialConn.Write([]byte(string(r)))
|
|
}
|
|
}()
|
|
|
|
return <-errCh
|
|
}
|
|
|
|
// SerialPortInfo is a structure that holds information about the port and its
|
|
// associated TargetSpec.
|
|
type SerialPortInfo struct {
|
|
Name string
|
|
IsUSB bool
|
|
VID string
|
|
PID string
|
|
Target string
|
|
Spec *compileopts.TargetSpec
|
|
}
|
|
|
|
// ListSerialPort returns serial port information and any detected TinyGo
|
|
// target.
|
|
func ListSerialPorts() ([]SerialPortInfo, error) {
|
|
maps, err := compileopts.GetTargetSpecs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
portsList, err := enumerator.GetDetailedPortsList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serialPortInfo := []SerialPortInfo{}
|
|
for _, p := range portsList {
|
|
info := SerialPortInfo{
|
|
Name: p.Name,
|
|
IsUSB: p.IsUSB,
|
|
VID: p.VID,
|
|
PID: p.PID,
|
|
}
|
|
vid := strings.ToLower(p.VID)
|
|
pid := strings.ToLower(p.PID)
|
|
for k, v := range maps {
|
|
usbInterfaces := v.SerialPort
|
|
for _, s := range usbInterfaces {
|
|
parts := strings.Split(s, ":")
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
if vid == strings.ToLower(parts[0]) && pid == strings.ToLower(parts[1]) {
|
|
info.Target = k
|
|
info.Spec = v
|
|
}
|
|
}
|
|
}
|
|
serialPortInfo = append(serialPortInfo, info)
|
|
}
|
|
|
|
return serialPortInfo, nil
|
|
}
|
|
|
|
var addressMatch = regexp.MustCompile(`panic: runtime error at 0x([0-9a-f]+): `)
|
|
|
|
// Extract the address from the "panic: runtime error at" message.
|
|
func extractPanicAddress(line []byte) uint64 {
|
|
matches := addressMatch.FindSubmatch(line)
|
|
if matches != nil {
|
|
address, err := strconv.ParseUint(string(matches[1]), 16, 64)
|
|
if err == nil {
|
|
return address
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Convert an address in the binary to a source address location.
|
|
func addressToLine(executable string, address uint64) (token.Position, error) {
|
|
data, err := readDWARF(executable)
|
|
if err != nil {
|
|
return token.Position{}, err
|
|
}
|
|
r := data.Reader()
|
|
|
|
for {
|
|
e, err := r.Next()
|
|
if err != nil {
|
|
return token.Position{}, err
|
|
}
|
|
if e == nil {
|
|
break
|
|
}
|
|
switch e.Tag {
|
|
case dwarf.TagCompileUnit:
|
|
r.SkipChildren()
|
|
lr, err := data.LineReader(e)
|
|
if err != nil {
|
|
return token.Position{}, err
|
|
}
|
|
var lineEntry = dwarf.LineEntry{
|
|
EndSequence: true,
|
|
}
|
|
for {
|
|
// Read the next .debug_line entry.
|
|
prevLineEntry := lineEntry
|
|
err := lr.Next(&lineEntry)
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return token.Position{}, err
|
|
}
|
|
|
|
if prevLineEntry.EndSequence && lineEntry.Address == 0 {
|
|
// Tombstone value. This symbol has been removed, for
|
|
// example by the --gc-sections linker flag. It is still
|
|
// here in the debug information because the linker can't
|
|
// just remove this reference.
|
|
// Read until the next EndSequence so that this sequence is
|
|
// skipped.
|
|
// For more details, see (among others):
|
|
// https://reviews.llvm.org/D84825
|
|
for {
|
|
err := lr.Next(&lineEntry)
|
|
if err != nil {
|
|
return token.Position{}, err
|
|
}
|
|
if lineEntry.EndSequence {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !prevLineEntry.EndSequence {
|
|
// The chunk describes the code from prevLineEntry to
|
|
// lineEntry.
|
|
if prevLineEntry.Address <= address && lineEntry.Address > address {
|
|
return token.Position{
|
|
Filename: prevLineEntry.File.Name,
|
|
Line: prevLineEntry.Line,
|
|
Column: prevLineEntry.Column,
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return token.Position{}, nil // location not found
|
|
}
|
|
|
|
// Read the DWARF debug information from a given file (in various formats).
|
|
func readDWARF(executable string) (*dwarf.Data, error) {
|
|
f, err := os.Open(executable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if file, err := elf.NewFile(f); err == nil {
|
|
return file.DWARF()
|
|
} else if file, err := macho.NewFile(f); err == nil {
|
|
return file.DWARF()
|
|
} else if file, err := pe.NewFile(f); err == nil {
|
|
return file.DWARF()
|
|
} else {
|
|
return nil, errors.New("unknown binary format")
|
|
}
|
|
}
|
|
|
|
type outputWriter struct {
|
|
out io.Writer
|
|
executable string
|
|
line []byte
|
|
}
|
|
|
|
// newOutputWriter returns an io.Writer that will intercept panic addresses and
|
|
// will try to insert a source location in the output if the source location can
|
|
// be found in the executable.
|
|
func newOutputWriter(out io.Writer, executable string) *outputWriter {
|
|
return &outputWriter{
|
|
out: out,
|
|
executable: executable,
|
|
}
|
|
}
|
|
|
|
func (w *outputWriter) Write(p []byte) (n int, err error) {
|
|
start := 0
|
|
for i, c := range p {
|
|
if c == '\n' {
|
|
w.out.Write(p[start : i+1])
|
|
start = i + 1
|
|
address := extractPanicAddress(w.line)
|
|
if address != 0 {
|
|
loc, err := addressToLine(w.executable, address)
|
|
if err == nil && loc.Filename != "" {
|
|
fmt.Printf("[tinygo: panic at %s]\n", loc.String())
|
|
}
|
|
}
|
|
w.line = w.line[:0]
|
|
} else {
|
|
w.line = append(w.line, c)
|
|
}
|
|
}
|
|
w.out.Write(p[start:])
|
|
n = len(p)
|
|
return
|
|
}
|