From 476f55fd8aec65cb1bd3417dd1c8c583c9e385d8 Mon Sep 17 00:00:00 2001 From: Lorenzo Masini Date: Wed, 8 Mar 2017 16:45:23 +0100 Subject: [PATCH] cmd/objdump: print Go code alongside assembly Added -S flag to print go source file line above corresponding disassembly: $ go tool objdump -S -s main.main fmthello TEXT main.main(SB) /home/rugginoso/Documents/src/go/src/cmd/objdump/testdata/fmthello.go func main() { 0x47d450 64488b0c25f8ffffff FS MOVQ FS:0xfffffff8, CX 0x47d459 483b6110 CMPQ 0x10(CX), SP 0x47d45d 7631 JBE 0x47d490 0x47d45f 4883ec18 SUBQ $0x18, SP 0x47d463 48896c2410 MOVQ BP, 0x10(SP) 0x47d468 488d6c2410 LEAQ 0x10(SP), BP Println("hello, world") 0x47d46d 488d0563b00200 LEAQ 0x2b063(IP), AX 0x47d474 48890424 MOVQ AX, 0(SP) 0x47d478 48c74424080c000000 MOVQ $0xc, 0x8(SP) 0x47d481 e81a000000 CALL main.Println(SB) } 0x47d486 488b6c2410 MOVQ 0x10(SP), BP 0x47d48b 4883c418 ADDQ $0x18, SP 0x47d48f c3 RET func main() { 0x47d490 e8ebf1fcff CALL runtime.morestack_noctxt(SB) 0x47d495 ebb9 JMP main.main(SB) Execution time: $ time go tool objdump testdata/fmthello > /dev/null real 0m0.430s user 0m0.440s sys 0m0.000s $ time go tool objdump -S testdata/fmthello > /dev/null real 0m0.471s user 0m0.476s sys 0m0.012s Fixes #18245 Change-Id: I9b2f8338f9ee443c1352efd270d3ba85e3dd9b78 Reviewed-on: https://go-review.googlesource.com/37953 Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot Reviewed-by: Keith Randall --- src/cmd/internal/objfile/disasm.go | 108 ++++++++++++++++++++++++++++- src/cmd/objdump/main.go | 7 +- src/cmd/objdump/objdump_test.go | 43 +++++++++--- 3 files changed, 141 insertions(+), 17 deletions(-) diff --git a/src/cmd/internal/objfile/disasm.go b/src/cmd/internal/objfile/disasm.go index 8af0c8f859..d61cb27182 100644 --- a/src/cmd/internal/objfile/disasm.go +++ b/src/cmd/internal/objfile/disasm.go @@ -6,10 +6,16 @@ package objfile import ( "bufio" + "bytes" + "cmd/internal/src" + "container/list" "debug/gosym" "encoding/binary" "fmt" "io" + "io/ioutil" + "os" + "path/filepath" "regexp" "sort" "strings" @@ -102,10 +108,82 @@ func base(path string) string { return path } +// CachedFile contains the content of a file split into lines. +type CachedFile struct { + FileName string + Lines [][]byte +} + +// FileCache is a simple LRU cache of file contents. +type FileCache struct { + files *list.List + maxLen int +} + +// NewFileCache returns a FileCache which can contain up to maxLen cached file contents. +func NewFileCache(maxLen int) *FileCache { + return &FileCache{ + files: list.New(), + maxLen: maxLen, + } +} + +// Line returns the source code line for the given file and line number. +// If the file is not already cached, reads it , inserts it into the cache, +// and removes the least recently used file if necessary. +// If the file is in cache, moves it up to the front of the list. +func (fc *FileCache) Line(filename string, line int) ([]byte, error) { + if filepath.Ext(filename) != ".go" { + return nil, nil + } + + // Clean filenames returned by src.Pos.SymFilename() + // or src.PosBase.SymFilename() removing + // the leading src.FileSymPrefix. + if strings.HasPrefix(filename, src.FileSymPrefix) { + filename = filename[len(src.FileSymPrefix):] + } + + // Expand literal "$GOROOT" rewrited by obj.AbsFile() + filename = filepath.Clean(os.ExpandEnv(filename)) + + var cf *CachedFile + var e *list.Element + + for e = fc.files.Front(); e != nil; e = e.Next() { + cf = e.Value.(*CachedFile) + if cf.FileName == filename { + break + } + } + + if e == nil { + content, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + cf = &CachedFile{ + FileName: filename, + Lines: bytes.Split(content, []byte{'\n'}), + } + fc.files.PushFront(cf) + + if fc.files.Len() >= fc.maxLen { + fc.files.Remove(fc.files.Back()) + } + } else { + fc.files.MoveToFront(e) + } + + return cf.Lines[line-1], nil +} + // Print prints a disassembly of the file to w. // If filter is non-nil, the disassembly only includes functions with names matching filter. +// If printCode is true, the disassembly includs corresponding source lines. // The disassembly only includes functions that overlap the range [start, end). -func (d *Disasm) Print(w io.Writer, filter *regexp.Regexp, start, end uint64) { +func (d *Disasm) Print(w io.Writer, filter *regexp.Regexp, start, end uint64, printCode bool) { if start < d.textStart { start = d.textStart } @@ -114,6 +192,12 @@ func (d *Disasm) Print(w io.Writer, filter *regexp.Regexp, start, end uint64) { } printed := false bw := bufio.NewWriter(w) + + var fc *FileCache + if printCode { + fc = NewFileCache(8) + } + for _, sym := range d.syms { symStart := sym.Addr symEnd := sym.Addr + uint64(sym.Size) @@ -132,14 +216,32 @@ func (d *Disasm) Print(w io.Writer, filter *regexp.Regexp, start, end uint64) { file, _, _ := d.pcln.PCToLine(sym.Addr) fmt.Fprintf(bw, "TEXT %s(SB) %s\n", sym.Name, file) - tw := tabwriter.NewWriter(bw, 1, 8, 1, '\t', 0) + tw := tabwriter.NewWriter(bw, 18, 8, 1, '\t', tabwriter.StripEscape) if symEnd > end { symEnd = end } code := d.text[:end-d.textStart] + + var lastFile string + var lastLine int + d.Decode(symStart, symEnd, relocs, func(pc, size uint64, file string, line int, text string) { i := pc - d.textStart - fmt.Fprintf(tw, "\t%s:%d\t%#x\t", base(file), line, pc) + + if printCode { + if file != lastFile || line != lastLine { + if srcLine, err := fc.Line(file, line); err == nil { + fmt.Fprintf(tw, "%s%s%s\n", []byte{tabwriter.Escape}, srcLine, []byte{tabwriter.Escape}) + } + + lastFile, lastLine = file, line + } + + fmt.Fprintf(tw, " %#x\t", pc) + } else { + fmt.Fprintf(tw, " %s:%d\t%#x\t", base(file), line, pc) + } + if size%4 != 0 || d.goarch == "386" || d.goarch == "amd64" { // Print instruction as bytes. fmt.Fprintf(tw, "%x", code[i:i+size]) diff --git a/src/cmd/objdump/main.go b/src/cmd/objdump/main.go index 8bf9e4e306..7a3ba55517 100644 --- a/src/cmd/objdump/main.go +++ b/src/cmd/objdump/main.go @@ -43,11 +43,12 @@ import ( "cmd/internal/objfile" ) +var printCode = flag.Bool("S", false, "print go code alongside assembly") var symregexp = flag.String("s", "", "only dump symbols matching this regexp") var symRE *regexp.Regexp func usage() { - fmt.Fprintf(os.Stderr, "usage: go tool objdump [-s symregexp] binary [start end]\n\n") + fmt.Fprintf(os.Stderr, "usage: go tool objdump [-S] [-s symregexp] binary [start end]\n\n") flag.PrintDefaults() os.Exit(2) } @@ -88,7 +89,7 @@ func main() { usage() case 1: // disassembly of entire object - dis.Print(os.Stdout, symRE, 0, ^uint64(0)) + dis.Print(os.Stdout, symRE, 0, ^uint64(0), *printCode) os.Exit(0) case 3: @@ -101,7 +102,7 @@ func main() { if err != nil { log.Fatalf("invalid end PC: %v", err) } - dis.Print(os.Stdout, symRE, start, end) + dis.Print(os.Stdout, symRE, start, end, *printCode) os.Exit(0) } } diff --git a/src/cmd/objdump/objdump_test.go b/src/cmd/objdump/objdump_test.go index 419be6717a..91adde3eb3 100644 --- a/src/cmd/objdump/objdump_test.go +++ b/src/cmd/objdump/objdump_test.go @@ -57,24 +57,18 @@ func buildObjdump() error { } var x86Need = []string{ - "fmthello.go:6", - "TEXT main.main(SB)", "JMP main.main(SB)", "CALL main.Println(SB)", "RET", } var armNeed = []string{ - "fmthello.go:6", - "TEXT main.main(SB)", //"B.LS main.main(SB)", // TODO(rsc): restore; golang.org/issue/9021 "BL main.Println(SB)", "RET", } var ppcNeed = []string{ - "fmthello.go:6", - "TEXT main.main(SB)", "BR main.main(SB)", "CALL main.Println(SB)", "RET", @@ -91,7 +85,7 @@ var target = flag.String("target", "", "test disassembly of `goos/goarch` binary // binary for the current system (only) and test that objdump // can handle that one. -func testDisasm(t *testing.T, flags ...string) { +func testDisasm(t *testing.T, printCode bool, flags ...string) { goarch := runtime.GOARCH if *target != "" { f := strings.Split(*target, "/") @@ -114,9 +108,15 @@ func testDisasm(t *testing.T, flags ...string) { t.Fatalf("go build fmthello.go: %v\n%s", err, out) } need := []string{ - "fmthello.go:6", "TEXT main.main(SB)", } + + if printCode { + need = append(need, ` Println("hello, world")`) + } else { + need = append(need, "fmthello.go:6") + } + switch goarch { case "amd64", "386": need = append(need, x86Need...) @@ -126,7 +126,16 @@ func testDisasm(t *testing.T, flags ...string) { need = append(need, ppcNeed...) } - out, err = exec.Command(exe, "-s", "main.main", hello).CombinedOutput() + args = []string{ + "-s", "main.main", + hello, + } + + if printCode { + args = append([]string{"-S"}, args...) + } + + out, err = exec.Command(exe, args...).CombinedOutput() if err != nil { t.Fatalf("objdump fmthello.exe: %v\n%s", err, out) } @@ -153,7 +162,19 @@ func TestDisasm(t *testing.T) { case "s390x": t.Skipf("skipping on %s, issue 15255", runtime.GOARCH) } - testDisasm(t) + testDisasm(t, false) +} + +func TestDisasmCode(t *testing.T) { + switch runtime.GOARCH { + case "arm64": + t.Skipf("skipping on %s, issue 10106", runtime.GOARCH) + case "mips", "mipsle", "mips64", "mips64le": + t.Skipf("skipping on %s, issue 12559", runtime.GOARCH) + case "s390x": + t.Skipf("skipping on %s, issue 15255", runtime.GOARCH) + } + testDisasm(t, true) } func TestDisasmExtld(t *testing.T) { @@ -178,5 +199,5 @@ func TestDisasmExtld(t *testing.T) { if !build.Default.CgoEnabled { t.Skip("skipping because cgo is not enabled") } - testDisasm(t, "-ldflags=-linkmode=external") + testDisasm(t, false, "-ldflags=-linkmode=external") } -- 2.48.1