cmd/viewcore: add --exe option to accept the binary path

This is more convenient than --base when the local copy of the program
is located in a random place.

This change assumes that the first entry appearing in the note file
section corresponds to the main binary, and if --exe is supplied, use
the user-supplied binary instead.

Manual tested:
$ viewcore core --exe=tmp/test mappings
              min              max perm         source       original
           400000           401000  r-x      core@3000     tmp/test@0
           401000           451000  r-x  tmp/test@1000
           451000           4aa000  r-- tmp/test@51000
           4aa000           4cc000  rw-      core@4000 tmp/test@aa000
       c000000000       c000001000  rw-     core@26000
       ...

Change-Id: I3e9e008da324ec57427a60eebdca2f459eceda53
Reviewed-on: https://go-review.googlesource.com/120337
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/cmd/viewcore/main.go b/cmd/viewcore/main.go
index 27d59b0..497b911 100644
--- a/cmd/viewcore/main.go
+++ b/cmd/viewcore/main.go
@@ -132,17 +132,21 @@
 	}
 )
 
-var cfg struct {
+type config struct {
 	// Set based on os.Args[1]
 	corefile string
 
 	// flags
 	base    string
-	cpuprof string
+	exePath string
+	cpuprof string // TODO: move to subcommand config.
 }
 
+var cfg config
+
 func init() {
 	cmdRoot.PersistentFlags().StringVar(&cfg.base, "base", "", "root directory to find core dump file references")
+	cmdRoot.PersistentFlags().StringVar(&cfg.exePath, "exe", "", "main executable file")
 	cmdRoot.PersistentFlags().StringVar(&cfg.cpuprof, "prof", "", "write cpu profile of viewcore to this file for viewcore's developers")
 
 	cmdRoot.AddCommand(
@@ -226,19 +230,19 @@
 }
 
 var coreCache = &struct {
-	corefile string
-	base     string
-	p        *core.Process
-	err      error
+	// copy of params used to generate p.
+	cfg config
+
+	p   *core.Process
+	err error
 }{}
 
 // readCore reads corefile and returns core and gocore process states.
-func readCore(corefile, base string, flags gocore.Flags) (*core.Process, *gocore.Process, error) {
+func readCore(flags gocore.Flags) (*core.Process, *gocore.Process, error) {
 	cc := coreCache
-	if cc.corefile != corefile || cc.base != base {
-		cc.corefile = corefile
-		cc.base = base
-		cc.p, cc.err = core.Core(corefile, base)
+	if cc.cfg != cfg {
+		cc.cfg = cfg
+		cc.p, cc.err = core.Core(cfg.corefile, cfg.base, cfg.exePath)
 	}
 	if cc.err != nil {
 		return nil, nil, cc.err
@@ -263,7 +267,7 @@
 		cmd.Usage()
 		return
 	}
-	_, _, err := readCore(cfg.corefile, cfg.base, 0)
+	p, _, err := readCore(0)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -297,13 +301,32 @@
 	}
 	defer shell.Close()
 
+	// TODO(hyangah): the following code retrieves the main
+	// program path from the mapping information.
+	// The intention is to help users confirm viewcore
+	// picked up correct executable and core file for analysis.
+	// The usefulness of this info is debatable.
+	// Another idea is to present the executable file info
+	// embedded in prpsinfo which will provide useful
+	// new information about the corefile to analyze.
+	exe := "unknown"
+	if m := p.Mappings(); len(m) > 0 {
+		src, _ := m[0].Source()
+		if m[0].CopyOnWrite() {
+			src, _ = m[0].OrigSource()
+		}
+		if src != "" {
+			exe = src
+		}
+	}
+
 	welcomeMsg := `
   Corefile: %s
-  Base: %s
+  Program : %s
 
 Entering interactive mode (type 'help' for commands)
 `
-	fmt.Fprintf(shell.Terminal, welcomeMsg, cfg.corefile, cfg.base)
+	fmt.Fprintf(shell.Terminal, welcomeMsg, cfg.corefile, exe)
 
 	for {
 		l, err := shell.Readline()
@@ -343,7 +366,7 @@
 }
 
 func runOverview(cmd *cobra.Command, args []string) {
-	p, c, err := readCore(cfg.corefile, cfg.base, 0)
+	p, c, err := readCore(0)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -360,7 +383,7 @@
 }
 
 func runMappings(cmd *cobra.Command, args []string) {
-	p, _, err := readCore(cfg.corefile, cfg.base, 0)
+	p, _, err := readCore(0)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -395,7 +418,7 @@
 }
 
 func runGoroutines(cmd *cobra.Command, args []string) {
-	_, c, err := readCore(cfg.corefile, cfg.base, 0)
+	_, c, err := readCore(0)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -419,7 +442,7 @@
 }
 
 func runHistogram(cmd *cobra.Command, args []string) {
-	_, c, err := readCore(cfg.corefile, cfg.base, gocore.FlagTypes)
+	_, c, err := readCore(gocore.FlagTypes)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -455,7 +478,7 @@
 }
 
 func runBreakdown(cmd *cobra.Command, args []string) {
-	_, c, err := readCore(cfg.corefile, cfg.base, 0)
+	_, c, err := readCore(0)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -485,7 +508,7 @@
 }
 
 func runObjgraph(cmd *cobra.Command, args []string) {
-	_, c, err := readCore(cfg.corefile, cfg.base, gocore.FlagTypes)
+	_, c, err := readCore(gocore.FlagTypes)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -549,7 +572,7 @@
 }
 
 func runObjects(cmd *cobra.Command, args []string) {
-	_, c, err := readCore(cfg.corefile, cfg.base, gocore.FlagTypes)
+	_, c, err := readCore(gocore.FlagTypes)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -561,7 +584,7 @@
 }
 
 func runReachable(cmd *cobra.Command, args []string) {
-	_, c, err := readCore(cfg.corefile, cfg.base, gocore.FlagTypes|gocore.FlagReverse)
+	_, c, err := readCore(gocore.FlagTypes | gocore.FlagReverse)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -645,7 +668,7 @@
 }
 
 func runHTML(cmd *cobra.Command, args []string) {
-	_, c, err := readCore(cfg.corefile, cfg.base, gocore.FlagTypes|gocore.FlagReverse)
+	_, c, err := readCore(gocore.FlagTypes | gocore.FlagReverse)
 	if err != nil {
 		exitf("%v\n", err)
 	}
@@ -653,7 +676,7 @@
 }
 
 func runRead(cmd *cobra.Command, args []string) {
-	p, _, err := readCore(cfg.corefile, cfg.base, 0)
+	p, _, err := readCore(0)
 	if err != nil {
 		exitf("%v\n", err)
 	}
diff --git a/internal/core/core_test.go b/internal/core/core_test.go
index 23a56ec..904a75a 100644
--- a/internal/core/core_test.go
+++ b/internal/core/core_test.go
@@ -5,17 +5,25 @@
 package core
 
 import (
+	"fmt"
 	"testing"
 )
 
-// loadTest loads a simple core file which resulted from running the
+// loadExample loads a simple core file which resulted from running the
 // following program on linux/amd64 with go 1.9.0 (the earliest supported runtime):
 // package main
 // func main() {
 //         _ = *(*int)(nil)
 // }
-func loadExample(t *testing.T) *Process {
-	p, err := Core("testdata/core", "testdata")
+func loadExample(t *testing.T, useExePath bool) *Process {
+	t.Helper()
+	var p *Process
+	var err error
+	if useExePath {
+		p, err = Core("testdata/core", "", "testdata/tmp/test")
+	} else {
+		p, err = Core("testdata/core", "testdata", "")
+	}
 	if err != nil {
 		t.Fatalf("can't load test core file: %s", err)
 	}
@@ -24,44 +32,53 @@
 
 // TestMappings makes sure we can find and load some data.
 func TestMappings(t *testing.T) {
-	p := loadExample(t)
-	s, err := p.Symbols()
-	if err != nil {
-		t.Errorf("can't read symbols: %s\n", err)
+	test := func(t *testing.T, useExePath bool) {
+		p := loadExample(t, useExePath)
+		s, err := p.Symbols()
+		if err != nil {
+			t.Errorf("can't read symbols: %s\n", err)
+		}
+
+		a := s["main.main"]
+		m := p.findMapping(a)
+		if m == nil {
+			t.Errorf("text mapping missing")
+		}
+		if m.Perm() != Read|Exec {
+			t.Errorf("bad code section permissions")
+		}
+		if opcode := p.ReadUint8(a); opcode != 0x31 {
+			// 0x31 = xorl instruction.
+			// There's no particular reason why this instruction
+			// is first. This just tests that reading code works
+			// for our specific test binary.
+			t.Errorf("opcode=0x%x, want 0x31", opcode)
+		}
+
+		a = s["runtime.class_to_size"]
+		m = p.findMapping(a)
+		if m == nil {
+			t.Errorf("data mapping missing")
+		}
+		if m.Perm() != Read|Write {
+			t.Errorf("bad data section permissions")
+		}
+		if size := p.ReadUint16(a.Add(2)); size != 8 {
+			t.Errorf("class_to_size[1]=%d, want 8", size)
+		}
 	}
 
-	a := s["main.main"]
-	m := p.findMapping(a)
-	if m == nil {
-		t.Errorf("text mapping missing")
-	}
-	if m.Perm() != Read|Exec {
-		t.Errorf("bad code section permissions")
-	}
-	if opcode := p.ReadUint8(a); opcode != 0x31 {
-		// 0x31 = xorl instruction.
-		// There's no particular reason why this instruction
-		// is first. This just tests that reading code works
-		// for our specific test binary.
-		t.Errorf("opcode=0x%x, want 0x31", opcode)
-	}
-
-	a = s["runtime.class_to_size"]
-	m = p.findMapping(a)
-	if m == nil {
-		t.Errorf("data mapping missing")
-	}
-	if m.Perm() != Read|Write {
-		t.Errorf("bad data section permissions")
-	}
-	if size := p.ReadUint16(a.Add(2)); size != 8 {
-		t.Errorf("class_to_size[1]=%d, want 8", size)
+	for _, useExePath := range []bool{false, true} {
+		name := fmt.Sprintf("useExePath=%t", useExePath)
+		t.Run(name, func(t *testing.T) {
+			test(t, useExePath)
+		})
 	}
 }
 
 // TestConfig checks the configuration accessors.
 func TestConfig(t *testing.T) {
-	p := loadExample(t)
+	p := loadExample(t, false)
 	if arch := p.Arch(); arch != "amd64" {
 		t.Errorf("arch=%s, want amd64", arch)
 	}
@@ -78,7 +95,7 @@
 
 // TestThread makes sure we get information about running threads.
 func TestThread(t *testing.T) {
-	p := loadExample(t)
+	p := loadExample(t, true)
 	syms, err := p.Symbols()
 	if err != nil {
 		t.Errorf("can't read symbols: %s\n", err)
diff --git a/internal/core/process.go b/internal/core/process.go
index 5a25ad4..0e56e0e 100644
--- a/internal/core/process.go
+++ b/internal/core/process.go
@@ -30,7 +30,9 @@
 
 // A Process represents the state of the process that core dumped.
 type Process struct {
-	base         string             // base directory from which files in the core can be found
+	base    string // base directory from which files in the core can be found
+	exePath string // user-supplied main executable path
+
 	exec         []*os.File         // executables (more than one for shlibs)
 	mappings     []*Mapping         // virtual address mappings
 	threads      []*Thread          // os threads (TODO: map from pid?)
@@ -117,14 +119,13 @@
 
 // Core takes the name of a core file and returns a Process that
 // represents the state of the inferior that generated the core file.
-func Core(coreFile, base string) (*Process, error) {
+func Core(coreFile, base, exePath string) (*Process, error) {
 	core, err := os.Open(coreFile)
 	if err != nil {
 		return nil, err
 	}
 
-	p := new(Process)
-	p.base = base
+	p := &Process{base: base, exePath: exePath}
 	if err := p.readCore(core); err != nil {
 		return nil, err
 	}
@@ -361,6 +362,8 @@
 	desc = desc[8:]
 	filenames := string(desc[3*8*count:])
 	desc = desc[:3*8*count]
+
+	origExePath := ""
 	for i := uint64(0); i < count; i++ {
 		min := Address(e.ByteOrder.Uint64(desc))
 		desc = desc[8:]
@@ -379,7 +382,22 @@
 			filenames = ""
 		}
 
-		backing, err := os.Open(filepath.Join(p.base, name))
+		// Assume the first entry is the main binary.
+		if i == 0 {
+			origExePath = name
+		}
+
+		var backing *os.File
+		var err error
+
+		// If the name matches the cached original executable path
+		// and user-provided executable is available, use the
+		// user-provided one.
+		if p.exePath != "" && origExePath == name {
+			backing, err = os.Open(p.exePath)
+		} else {
+			backing, err = os.Open(filepath.Join(p.base, name))
+		}
 		if err != nil {
 			// Can't find mapped file.
 			// We don't want to make this a hard error because there are
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index 05679a0..cc90c65 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -19,7 +19,7 @@
 //         _ = *(*int)(nil)
 // }
 func loadExample(t *testing.T) *Process {
-	c, err := core.Core("testdata/core", "testdata")
+	c, err := core.Core("testdata/core", "testdata", "")
 	if err != nil {
 		t.Fatalf("can't load test core file: %s", err)
 	}
@@ -34,7 +34,7 @@
 	if version == "1.9" {
 		version = ""
 	}
-	c, err := core.Core(fmt.Sprintf("testdata/core%s", version), "testdata")
+	c, err := core.Core(fmt.Sprintf("testdata/core%s", version), "testdata", "")
 	if err != nil {
 		t.Fatalf("can't load test core file: %s", err)
 	}