blob: 69ee7027fa19310f53f9733d002ef3b0d10a19e6 [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
// This file is the source code for a test helper binary 'dwdumploc',
// which is built and then run by the tests in cmd/link/internal/dwtest.
// It is set up to import packages "op" and "proc" from Delve (so
// as to use Delve's DWARF location expression parser), and the
// "dwtest" package from cmd/link/internal/dwtest (for general
// DWARF examination/inspection).
//
// Given an input Go binary and a function name, this program
// visits the DWARF for the function and dumps out the location expressions
// for the function's input parameters on entry to the func. Example:
//
// ./dumpdwloc.exe -m ./dumpdwloc.exe -f main.locateFuncDetails
// 1: in-param "executable" loc="{ [0: S=8 RAX] [1: S=8 RBX] }"
// 2: in-param "fcn" loc="{ [0: S=8 RCX] [1: S=8 RDI] }"
// 3: out-param "~r0" loc="<not available>"
// 4: out-param "~r1" loc="<not available>"
//
// Since location expressions can refer to machine registers, dump
// output for location expressions are architecture-dependent. The
// dumper tool currently supports two archs: amd64 and arm64.
//
import (
"debug/dwarf"
"debug/elf"
"debug/macho"
"debug/pe"
"dwdumploc/dwtest"
"flag"
"fmt"
"log"
"os"
"runtime"
"strings"
"github.com/go-delve/delve/pkg/dwarf/op"
"github.com/go-delve/delve/pkg/proc"
)
var verbflag = flag.Int("v", 0, "Verbose trace output level")
var fcnflag = flag.String("f", "", "name of function to display")
var moduleflag = flag.String("m", "", "load module to read")
func verb(vlevel int, s string, a ...interface{}) {
if *verbflag >= vlevel {
fmt.Fprintf(os.Stderr, s, a...)
fmt.Fprintf(os.Stderr, "\n")
}
}
func warn(s string, a ...interface{}) {
fmt.Fprintf(os.Stderr, s, a...)
fmt.Fprintf(os.Stderr, "\n")
}
func usage(msg string) {
if len(msg) > 0 {
fmt.Fprintf(os.Stderr, "error: %s\n", msg)
}
fmt.Fprintf(os.Stderr, "usage: dwdumploc [flags] -m <binary> -f <fcn>\n")
flag.PrintDefaults()
os.Exit(2)
}
type finfo struct {
name string
dwOffset dwarf.Offset
dwLoPC uint64
dwHiPC uint64
valid bool
dwx *dwtest.Examiner
}
// opener defines an interface for opening DWARF info in a Go binary.
type opener interface {
// Open examines binary pointed to by 'path', returning a pointer
// to debug/dwarf.Data for it. If something goes wrong opening the
// binary (file missing, or maybe not a supported binary) a
// suitable error will be returned in the first error return. If
// something goes wrong reading the DWARF, a suitable error will
// be returned in the second error return.
Open(path string) (*dwarf.Data, error, error)
Which() string
}
// Remark: the boilerplate for three scenarios below (Elf, Macho, and
// PE) seems like it should be an ideal scenario where you could use
// generics, but my attempts at this were not especially successful.
// The sticking point is that each of elf.Open/macho.Open etc return a
// different thing, and I couldn't quite figure out how to get this to
// work with generics. TODO: make another attempt.
// elfOpener implements the opener interface for ELF
// (https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) binaries
//
type elfOpener struct {
}
func (eo *elfOpener) Open(path string) (*dwarf.Data, error, error) {
f, err := elf.Open(path)
if err != nil {
return nil, err, nil
}
d, err := f.DWARF()
if err != nil {
return nil, nil, err
}
return d, nil, nil
}
func (eo *elfOpener) Which() string {
return "elf"
}
// machoOpener implements the opener for macos/darwin Macho-O
// (https://en.wikipedia.org/wiki/Mach-O) binaries.
type machoOpener struct {
}
func (mo *machoOpener) Open(path string) (*dwarf.Data, error, error) {
f, err := macho.Open(path)
if err != nil {
return nil, err, nil
}
d, err := f.DWARF()
if err != nil {
return nil, nil, err
}
return d, nil, nil
}
func (mo *machoOpener) Which() string {
return "macho"
}
// peOpener implements the opener interface for Windows PE
// (https://en.wikipedia.org/wiki/Portable_Executable) binaries.
type peOpener struct {
}
func (po *peOpener) Open(path string) (*dwarf.Data, error, error) {
f, err := pe.Open(path)
if err != nil {
return nil, err, nil
}
d, err := f.DWARF()
if err != nil {
return nil, nil, err
}
return d, nil, nil
}
func (po *peOpener) Which() string {
return "pe"
}
// openDwarf tries to open the Go binary 'path' as an ELF file,
// a Macho file, or a PE file in turn. If an open succeeds, it
// returns a dwarf.Data for the binary; if they all fail, it returns
// an error.
func openDwarf(path string) (*dwarf.Data, error) {
eo := elfOpener{}
mo := machoOpener{}
po := peOpener{}
var eoo opener = &eo
var moo opener = &mo
var poo opener = &po
openers := []opener{eoo, moo, poo}
postmortem := ""
for k, o := range openers {
d, errf, errd := o.Open(path)
if d != nil {
return d, nil
}
if errd != nil {
return nil, errd
}
postmortem += fmt.Sprintf("\tattempt %d (%s) failed: %v\n", k, o.Which(), errf)
}
return nil, fmt.Errorf("init failed:\n%s", postmortem)
}
// locateFuncDetails walks the DWARF for Go binary 'executable'
// looking for a subprogram DIE for function 'fcn'. If it finds a
// function of the right name, it fills in info from the DWARF into
// a "finfo" struct and returns it, or returns an empty struct
// and an error if something went wrong.
func locateFuncDetails(executable string, fcn string) (finfo, error) {
rrv := finfo{}
if inf, err := os.Open(executable); err != nil {
return rrv, fmt.Errorf("unable to open input executable %s: %v", executable, err)
} else {
inf.Close()
}
verb(1, "loading DWARF for %s", executable)
d, err := openDwarf(executable)
if err != nil {
return finfo{}, err
}
verb(1, "DWARF loaded for %s", executable)
// Construct dwtest.Examiner helper object, to make
// inspection of the DWARF easier.
rdr := d.Reader()
dwx := dwtest.Examiner{}
if err := dwx.Populate(rdr); err != nil {
return finfo{}, fmt.Errorf("error reading DWARF: %v", err)
}
// Walk DIEs looking for subprogram DIEs.
dies := dwx.DIEs()
for idx := 0; idx < len(dies); idx++ {
die := dies[idx]
off := die.Offset
if die.Tag == dwarf.TagCompileUnit {
if name, ok := die.Val(dwarf.AttrName).(string); ok {
verb(2, "compilation unit: %s", name)
}
continue
}
if die.Tag != dwarf.TagSubprogram {
// TODO: skip children
continue
}
// Name has to match the function we're looking for.
name, ok := die.Val(dwarf.AttrName).(string)
if !ok {
continue
}
if name != fcn {
// TODO: skip children
continue
}
verb(1, "found function %s at offset %x", fcn, off)
rrv.dwOffset = off
if lopc, ok := die.Val(dwarf.AttrLowpc).(uint64); ok {
rrv.dwLoPC = lopc
} else {
return finfo{}, fmt.Errorf("target function seems to be missing LowPC attribute")
}
if hipc, ok := die.Val(dwarf.AttrHighpc).(uint64); ok {
rrv.dwHiPC = hipc
} else {
return finfo{}, fmt.Errorf("target function seems to be missing HighPC attribute")
}
rrv.dwx = &dwx
rrv.name = fcn
rrv.valid = true
return rrv, nil
}
return finfo{}, fmt.Errorf("could not locate target function in DWARF")
}
var AMD64DWARFRegisters = map[int]string{
0: "RAX",
1: "RDX",
2: "RCX",
3: "RBX",
4: "RSI",
5: "RDI",
6: "RBP",
7: "RSP",
8: "R8",
9: "R9",
10: "R10",
11: "R11",
12: "R12",
13: "R13",
14: "R14",
15: "R15",
17: "X0",
18: "X1",
19: "X2",
20: "X3",
21: "X4",
22: "X5",
23: "X6",
24: "X7",
25: "X8",
26: "X9",
27: "X10",
28: "X11",
29: "X12",
30: "X13",
31: "X14",
32: "X15",
}
var ARM64DWARFRegisters = map[int]string{
// int
0: "R0",
1: "R1",
2: "R2",
3: "R3",
4: "R4",
5: "R5",
6: "R6",
7: "R7",
8: "R8",
9: "R9",
10: "R10",
11: "R11",
12: "R12",
13: "R13",
14: "R14",
15: "R15",
16: "R16",
17: "R17",
18: "R18",
19: "R19",
20: "R20",
21: "R21",
22: "R22",
23: "R23",
24: "R24",
25: "R25",
26: "R26",
27: "R27",
28: "R28",
29: "R29",
30: "R30",
// float
64: "F0",
65: "F1",
66: "F2",
67: "F3",
68: "F4",
69: "F5",
70: "F6",
71: "F7",
72: "F8",
73: "F9",
74: "F10",
75: "F11",
76: "F12",
77: "F13",
78: "F14",
79: "F15",
80: "F16",
81: "F17",
82: "F18",
83: "F19",
84: "F20",
85: "F21",
86: "F22",
87: "F23",
88: "F24",
89: "F25",
90: "F26",
91: "F27",
92: "F28",
93: "F29",
94: "F30",
95: "F31",
}
func regString(dwreg int) string {
var v string
switch runtime.GOARCH {
case "amd64":
v = AMD64DWARFRegisters[dwreg]
case "arm64":
v = ARM64DWARFRegisters[dwreg]
default:
panic(fmt.Sprintf("no support for arch %s", runtime.GOARCH))
}
if v != "" {
return v
}
return fmt.Sprintf("reg=%d", dwreg)
}
func pstring(addr int64, pcs []op.Piece, err error) (string, error) {
if err != nil {
serr := fmt.Sprintf("%s", err)
if strings.HasPrefix(serr, "could not find loclist entry") {
return "<not available>", nil
}
return "", err
}
if pcs == nil {
return fmt.Sprintf("addr=%x", addr), nil
}
r := "{"
for k, p := range pcs {
r += fmt.Sprintf(" [%d: S=%d", k, p.Size)
if p.Kind == op.RegPiece {
r += fmt.Sprintf(" %s]", regString(int(p.Val)))
} else {
r += fmt.Sprintf(" addr=0x%x]", p.Val)
}
}
r += " }"
return r, nil
}
// processParams initializes a 'proc.BinaryInfo' object for the binary
// in question and then walks the formal parameters of the selected
// function, invoking the Location method on each param to read its
// location expression. Results are dumped to stdout, and an error is
// returned if something goes wrong.
func processParams(executable string, fi *finfo) error {
const _cfa = 0x1000
bi := proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH)
if err := bi.LoadBinaryInfo(executable, 0, []string{}); err != nil {
return err
}
// Walk subprogram DIE's children.
pidx := fi.dwx.IdxFromOffset(fi.dwOffset)
childDies := fi.dwx.Children(pidx)
idx := 0
for _, e := range childDies {
if e.Tag != dwarf.TagFormalParameter {
continue
}
if e.Val(dwarf.AttrName) == nil {
continue
}
idx++
name := e.Val(dwarf.AttrName).(string)
var isrvar bool
if e.Tag == dwarf.TagFormalParameter {
isrvar = e.Val(dwarf.AttrVarParam).(bool)
}
addr, pieces, _, err := bi.Location(e, dwarf.AttrLocation, fi.dwLoPC, op.DwarfRegisters{CFA: _cfa, FrameBase: _cfa}, nil)
pdump, err := pstring(addr, pieces, err)
if err != nil {
if fmt.Sprintf("%s", err) == "empty OP stack" {
pdump = "<not available>"
} else {
return fmt.Errorf("bad return from bi.Location at pc 0x%x: %q\n", fi.dwLoPC, err)
}
}
wh := "in"
if isrvar {
wh = "out"
}
fmt.Printf("%d: %s-param %q loc=%q\n", idx, wh, name, pdump)
}
return nil
}
// examineFile kicks off the search for DWARF info for 'fcn' within
// Go binary 'executable', printing results to stdout if possible.
func examineFile(executable string, fcn string) {
verb(1, "examineFile(%s,%s)", executable, fcn)
fi, err := locateFuncDetails(executable, fcn)
if err != nil {
log.Fatalf("error: %v\n", err)
}
if !fi.valid {
log.Fatalf("could not locate target function %s in executable %s", fcn, executable)
}
if err := processParams(executable, &fi); err != nil {
log.Fatalf("error: %v\n", err)
}
}
func main() {
log.SetFlags(0)
log.SetPrefix("dwdumploc: ")
flag.Parse()
verb(1, "in main")
if *fcnflag == "" || *moduleflag == "" {
usage("please supply -f and -m options")
}
if flag.NArg() != 0 {
usage("unexpected additional arguments")
}
examineFile(*moduleflag, *fcnflag)
verb(1, "leaving main")
}