diff --git a/internal/binutils/binutils.go b/internal/binutils/binutils.go index 8adfa8eb..ec3e738b 100644 --- a/internal/binutils/binutils.go +++ b/internal/binutils/binutils.go @@ -26,6 +26,7 @@ import ( "path/filepath" "regexp" "runtime" + "strconv" "strings" "sync" @@ -120,6 +121,29 @@ func (bu *Binutils) SetTools(config string) { bu.update(func(r *binrep) { initTools(r, config) }) } +func ensureObjdumpVersion(objdump string, objdumpFound bool) (string, bool) { + if !objdumpFound || runtime.GOOS != "darwin" { + return objdump, objdumpFound + } + + // Ensure objdump is at least version 9.0 on Mac OS + cmdOut, err := exec.Command(objdump, "-version").Output() + if err != nil { + return objdump, false + } + + re := regexp.MustCompile(`.*version (([0-9]*)\.([0-9]*)\.([0-9]*)).*$`) + fields := re.FindStringSubmatch(strings.Split(string(cmdOut), "\n")[0]) + if len(fields) != 5 { + return objdump, false + } + + if ver, _ := strconv.Atoi(fields[2]); ver < 9 { + return objdump, false + } + return objdump, objdumpFound +} + func initTools(b *binrep, config string) { // paths collect paths per tool; Key "" contains the default. paths := make(map[string][]string) @@ -140,7 +164,7 @@ func initTools(b *binrep, config string) { b.addr2line, b.addr2lineFound = findExe("gaddr2line", append(paths["addr2line"], defaultPath...)) } b.nm, b.nmFound = findExe("nm", append(paths["nm"], defaultPath...)) - b.objdump, b.objdumpFound = findExe("objdump", append(paths["objdump"], defaultPath...)) + b.objdump, b.objdumpFound = ensureObjdumpVersion(findExe("objdump", append(paths["objdump"], defaultPath...))) } // findExe looks for an executable command on a set of paths. @@ -159,10 +183,19 @@ func findExe(cmd string, paths []string) (string, bool) { // of a binary. func (bu *Binutils) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) { b := bu.get() + if !b.objdumpFound { + return nil, fmt.Errorf("couldn't find objdump") + } args := []string{"-d", "-C", "--no-show-raw-insn", "-l", fmt.Sprintf("--start-address=%#x", start), fmt.Sprintf("--stop-address=%#x", end)} + if runtime.GOOS == "darwin" { + args = []string{"-disassemble-all", "-no-show-raw-insn", + "-line-numbers", fmt.Sprintf("-start-address=%#x", start), + fmt.Sprintf("-stop-address=%#x", end)} + } + if intelSyntax { if runtime.GOOS == "darwin" { args = append(args, "-x86-asm-syntax=intel") diff --git a/internal/binutils/binutils_test.go b/internal/binutils/binutils_test.go index ad34c33e..3a2ff8ee 100644 --- a/internal/binutils/binutils_test.go +++ b/internal/binutils/binutils_test.go @@ -18,10 +18,12 @@ import ( "bytes" "fmt" "math" + "os/exec" "path/filepath" "reflect" "regexp" "runtime" + "strconv" "strings" "testing" @@ -189,14 +191,39 @@ func skipUnlessDarwinAmd64(t *testing.T) { } func testDisasm(t *testing.T, intelSyntax bool) { + if runtime.GOOS == "darwin" { + // get objdump version and skip test if < 9.0 + // binutils does not export get(). Try to get objdump version on our own. + // only works if objdump is in path. + cmdOut, err := exec.Command("objdump", "-version").Output() + if err != nil { + t.Skip("cannot determine objdump version.", err) + } + + re := regexp.MustCompile(`.*version (([0-9]*)\.([0-9]*)\.([0-9]*)).*$`) + fields := re.FindStringSubmatch(strings.Split(string(cmdOut), "\n")[0]) + if len(fields) != 5 { + t.Skip("cannot determine objdump version.", fields) + } + + if ver, _ := strconv.Atoi(fields[2]); ver < 9 { + t.Skip("objdump version too old. ", fields[0]) + } + } + bu := &Binutils{} - insts, err := bu.Disasm(filepath.Join("testdata", "exe_linux_64"), 0, math.MaxUint64, intelSyntax) + testexe := "exe_linux_64" + if runtime.GOOS == "darwin" { + testexe = "exe_mac_64" + } + + insts, err := bu.Disasm(filepath.Join("testdata", testexe), 0, math.MaxUint64, intelSyntax) if err != nil { t.Fatalf("Disasm: unexpected error %v", err) } mainCount := 0 for _, x := range insts { - if x.Function == "main" { + if x.Function == "main" || x.Function == "_main" { mainCount++ } } @@ -206,7 +233,9 @@ func testDisasm(t *testing.T, intelSyntax bool) { } func TestDisasm(t *testing.T) { - skipUnlessLinuxAmd64(t) + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + t.Skip("This test only works on Linux or Mac") + } testDisasm(t, true) testDisasm(t, false) } diff --git a/internal/binutils/disasm.go b/internal/binutils/disasm.go index 28c89aa1..babb4097 100644 --- a/internal/binutils/disasm.go +++ b/internal/binutils/disasm.go @@ -18,6 +18,7 @@ import ( "bytes" "io" "regexp" + "runtime" "strconv" "github.com/google/pprof/internal/plugin" @@ -25,10 +26,11 @@ import ( ) var ( - nmOutputRE = regexp.MustCompile(`^\s*([[:xdigit:]]+)\s+(.)\s+(.*)`) - objdumpAsmOutputRE = regexp.MustCompile(`^\s*([[:xdigit:]]+):\s+(.*)`) - objdumpOutputFileLine = regexp.MustCompile(`^(.*):([0-9]+)`) - objdumpOutputFunction = regexp.MustCompile(`^(\S.*)\(\):`) + nmOutputRE = regexp.MustCompile(`^\s*([[:xdigit:]]+)\s+(.)\s+(.*)`) + objdumpAsmOutputRE = regexp.MustCompile(`^\s*([[:xdigit:]]+):\s+(.*)`) + objdumpOutputFileLine = regexp.MustCompile(`^(.*):([0-9]+)`) + objdumpOutputFunction = regexp.MustCompile(`^(\S.*)\(\):`) + objdumpOutputFunctionDarwin = regexp.MustCompile(`^(\S.*):`) ) func findSymbols(syms []byte, file string, r *regexp.Regexp, address uint64) ([]*plugin.Sym, error) { @@ -144,6 +146,12 @@ func disassemble(asm []byte) ([]plugin.Inst, error) { function = fields[1] continue } + if runtime.GOOS == "darwin" { + if fields := objdumpOutputFunctionDarwin.FindStringSubmatch(input); len(fields) == 2 { + function = fields[1] + continue + } + } // Reset on unrecognized lines. function, file, line = "", "", 0 }