Replace Base() with ObjAddr() in the plugin.ObjFile interface. (#622)

Modify the plugin.ObjFile interface by replacing the "Base() uint64" method
with an "ObjAddr(addr uint64) (uint64, error)" method. The latter computes
the objdump address corresponding to the given runtime address.

The change removes direct access to the base value, but enables us to delay
computing the base until it is actually needed.
diff --git a/driver/driver.go b/driver/driver.go
index e65bc2f..fc05f91 100644
--- a/driver/driver.go
+++ b/driver/driver.go
@@ -159,8 +159,8 @@
 	// Name returns the underlying file name, if available.
 	Name() string
 
-	// Base returns the base address to use when looking up symbols in the file.
-	Base() uint64
+	// ObjAddr returns the objdump address corresponding to a runtime address.
+	ObjAddr(addr uint64) (uint64, error)
 
 	// BuildID returns the GNU build ID of the file, or an empty string.
 	BuildID() string
diff --git a/internal/binutils/binutils.go b/internal/binutils/binutils.go
index bfe792b..58fd195 100644
--- a/internal/binutils/binutils.go
+++ b/internal/binutils/binutils.go
@@ -508,8 +508,8 @@
 	return f.name
 }
 
-func (f *file) Base() uint64 {
-	return f.base
+func (f *file) ObjAddr(addr uint64) (uint64, error) {
+	return addr - f.base, nil
 }
 
 func (f *file) BuildID() string {
diff --git a/internal/binutils/binutils_test.go b/internal/binutils/binutils_test.go
index e22a10c..a7c84f5 100644
--- a/internal/binutils/binutils_test.go
+++ b/internal/binutils/binutils_test.go
@@ -358,17 +358,22 @@
 			if m == nil {
 				t.Fatalf("Symbols: did not find main")
 			}
-			for _, addr := range []uint64{m.Start + f.Base(), tc.addr} {
-				gotFrames, err := f.SourceLine(addr)
-				if err != nil {
-					t.Fatalf("SourceLine: unexpected error %v", err)
-				}
-				wantFrames := []plugin.Frame{
-					{Func: "main", File: "/tmp/hello.c", Line: 3},
-				}
-				if !reflect.DeepEqual(gotFrames, wantFrames) {
-					t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, wantFrames)
-				}
+			addr, err := f.ObjAddr(tc.addr)
+			if err != nil {
+				t.Fatalf("ObjAddr(%x) failed: %v", tc.addr, err)
+			}
+			if addr != m.Start {
+				t.Errorf("ObjAddr(%x) got %x, want %x", tc.addr, addr, m.Start)
+			}
+			gotFrames, err := f.SourceLine(tc.addr)
+			if err != nil {
+				t.Fatalf("SourceLine: unexpected error %v", err)
+			}
+			wantFrames := []plugin.Frame{
+				{Func: "main", File: "/tmp/hello.c", Line: 3},
+			}
+			if !reflect.DeepEqual(gotFrames, wantFrames) {
+				t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, wantFrames)
 			}
 		})
 	}
@@ -512,18 +517,22 @@
 			if m == nil {
 				t.Fatalf("Symbols: did not find main")
 			}
-
-			for _, addr := range []uint64{m.Start + f.Base(), tc.addr} {
-				gotFrames, err := f.SourceLine(addr)
-				if err != nil {
-					t.Fatalf("SourceLine: unexpected error %v", err)
-				}
-				wantFrames := []plugin.Frame{
-					{Func: "main", File: "hello.c", Line: 3},
-				}
-				if !reflect.DeepEqual(gotFrames, wantFrames) {
-					t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, wantFrames)
-				}
+			addr, err := f.ObjAddr(tc.addr)
+			if err != nil {
+				t.Fatalf("ObjAddr(%x) failed: %v", tc.addr, err)
+			}
+			if addr != m.Start {
+				t.Errorf("ObjAddr(%x) got %x, want %x", tc.addr, addr, m.Start)
+			}
+			gotFrames, err := f.SourceLine(tc.addr)
+			if err != nil {
+				t.Fatalf("SourceLine: unexpected error %v", err)
+			}
+			wantFrames := []plugin.Frame{
+				{Func: "main", File: "hello.c", Line: 3},
+			}
+			if !reflect.DeepEqual(gotFrames, wantFrames) {
+				t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, wantFrames)
 			}
 		})
 	}
@@ -657,22 +666,23 @@
 	for _, tc := range []struct {
 		desc                 string
 		start, limit, offset uint64
-		wantError            bool
-		wantBase             uint64
+		addr                 uint64
+		wantObjAddr          uint64
 	}{
-		{"exec mapping", 0x5400000, 0x5401000, 0, false, 0x5000000},
+		{"exec mapping", 0x5400000, 0x5401000, 0, 0x5400400, 0x400400},
 	} {
 		t.Run(tc.desc, func(t *testing.T) {
 			b := binrep{}
 			o, err := b.openELF(name, tc.start, tc.limit, tc.offset)
-			if (err != nil) != tc.wantError {
-				t.Errorf("got error %v, want any error=%v", err, tc.wantError)
-			}
 			if err != nil {
-				return
+				t.Fatalf("openELF got unexpected error: %v", err)
 			}
-			if got := o.Base(); got != tc.wantBase {
-				t.Errorf("got base %x; want %x\n", got, tc.wantBase)
+			got, err := o.ObjAddr(tc.addr)
+			if err != nil {
+				t.Fatalf("ObjAddr got unexpected error: %v", err)
+			}
+			if got != tc.wantObjAddr {
+				t.Errorf("got ObjAddr %x; want %x\n", got, tc.wantObjAddr)
 			}
 		})
 	}
diff --git a/internal/driver/driver_test.go b/internal/driver/driver_test.go
index e00bed5..c6c9c93 100644
--- a/internal/driver/driver_test.go
+++ b/internal/driver/driver_test.go
@@ -1619,9 +1619,9 @@
 	return m.name
 }
 
-// Base returns the base address to use when looking up symbols in the file.
-func (m *mockFile) Base() uint64 {
-	return m.base
+// ObjAddr returns the objdump address corresponding to a runtime address.
+func (m *mockFile) ObjAddr(addr uint64) (uint64, error) {
+	return addr - m.base, nil
 }
 
 // BuildID returns the GNU build ID of the file, or an empty string.
diff --git a/internal/driver/fetch_test.go b/internal/driver/fetch_test.go
index fb1cc06..b561151 100644
--- a/internal/driver/fetch_test.go
+++ b/internal/driver/fetch_test.go
@@ -168,7 +168,7 @@
 type testFile struct{ name, buildID string }
 
 func (f testFile) Name() string                                               { return f.name }
-func (testFile) Base() uint64                                                 { return 0 }
+func (testFile) ObjAddr(addr uint64) (uint64, error)                          { return addr, nil }
 func (f testFile) BuildID() string                                            { return f.buildID }
 func (testFile) SourceLine(addr uint64) ([]plugin.Frame, error)               { return nil, nil }
 func (testFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) { return nil, nil }
diff --git a/internal/driver/webui_test.go b/internal/driver/webui_test.go
index 3986cd3..33b4b20 100644
--- a/internal/driver/webui_test.go
+++ b/internal/driver/webui_test.go
@@ -146,10 +146,10 @@
 
 type fakeObj struct{}
 
-func (f fakeObj) Close() error    { return nil }
-func (f fakeObj) Name() string    { return "testbin" }
-func (f fakeObj) Base() uint64    { return 0 }
-func (f fakeObj) BuildID() string { return "" }
+func (f fakeObj) Close() error                        { return nil }
+func (f fakeObj) Name() string                        { return "testbin" }
+func (f fakeObj) ObjAddr(addr uint64) (uint64, error) { return addr, nil }
+func (f fakeObj) BuildID() string                     { return "" }
 func (f fakeObj) SourceLine(addr uint64) ([]plugin.Frame, error) {
 	return nil, fmt.Errorf("SourceLine unimplemented")
 }
diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go
index 3a8d0af..a57a0b2 100644
--- a/internal/plugin/plugin.go
+++ b/internal/plugin/plugin.go
@@ -131,8 +131,9 @@
 	// Name returns the underlyinf file name, if available
 	Name() string
 
-	// Base returns the base address to use when looking up symbols in the file.
-	Base() uint64
+	// ObjAddr returns the objdump (linker) address corresponding to a runtime
+	// address, and an error.
+	ObjAddr(addr uint64) (uint64, error)
 
 	// BuildID returns the GNU build ID of the file, or an empty string.
 	BuildID() string
diff --git a/internal/report/report.go b/internal/report/report.go
index bc5685d..4a86554 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -445,7 +445,7 @@
 			return err
 		}
 
-		ns := annotateAssembly(insts, sns, s.base)
+		ns := annotateAssembly(insts, sns, s.file)
 
 		fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0])
 		for _, name := range s.sym.Name[1:] {
@@ -534,7 +534,6 @@
 			addr = *address
 		}
 		msyms, err := f.Symbols(rx, addr)
-		base := f.Base()
 		f.Close()
 		if err != nil {
 			continue
@@ -543,7 +542,6 @@
 			objSyms = append(objSyms,
 				&objSymbol{
 					sym:  ms,
-					base: base,
 					file: f,
 				},
 			)
@@ -558,7 +556,6 @@
 // added to correspond to sample addresses
 type objSymbol struct {
 	sym  *plugin.Sym
-	base uint64
 	file plugin.ObjFile
 }
 
@@ -578,8 +575,7 @@
 	for _, s := range symbols {
 		// Gather samples for this symbol.
 		for _, n := range ns {
-			address := n.Info.Address - s.base
-			if address >= s.sym.Start && address < s.sym.End {
+			if address, err := s.file.ObjAddr(n.Info.Address); err == nil && address >= s.sym.Start && address < s.sym.End {
 				symNodes[s] = append(symNodes[s], n)
 			}
 		}
@@ -621,7 +617,7 @@
 // annotateAssembly annotates a set of assembly instructions with a
 // set of samples. It returns a set of nodes to display. base is an
 // offset to adjust the sample addresses.
-func annotateAssembly(insts []plugin.Inst, samples graph.Nodes, base uint64) []assemblyInstruction {
+func annotateAssembly(insts []plugin.Inst, samples graph.Nodes, file plugin.ObjFile) []assemblyInstruction {
 	// Add end marker to simplify printing loop.
 	insts = append(insts, plugin.Inst{
 		Addr: ^uint64(0),
@@ -645,7 +641,10 @@
 
 		// Sum all the samples until the next instruction (to account
 		// for samples attributed to the middle of an instruction).
-		for next := insts[ix+1].Addr; s < len(samples) && samples[s].Info.Address-base < next; s++ {
+		for next := insts[ix+1].Addr; s < len(samples); s++ {
+			if addr, err := file.ObjAddr(samples[s].Info.Address); err != nil || addr >= next {
+				break
+			}
 			sample := samples[s]
 			n.flatDiv += sample.FlatDiv
 			n.flat += sample.Flat
diff --git a/internal/report/source.go b/internal/report/source.go
index 4f841ef..6e2becb 100644
--- a/internal/report/source.go
+++ b/internal/report/source.go
@@ -321,9 +321,18 @@
 	}
 
 	for _, r := range ranges {
-		base := r.obj.Base()
-		insts, err := sp.objectTool.Disasm(r.mapping.File, r.begin-base, r.end-base,
-			rpt.options.IntelSyntax)
+		objBegin, err := r.obj.ObjAddr(r.begin)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range start %x: %v\n", r.begin, err)
+			continue
+		}
+		objEnd, err := r.obj.ObjAddr(r.end)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range end %x: %v\n", r.end, err)
+			continue
+		}
+		base := r.begin - objBegin
+		insts, err := sp.objectTool.Disasm(r.mapping.File, objBegin, objEnd, rpt.options.IntelSyntax)
 		if err != nil {
 			// TODO(sanjay): Report that the covered addresses are missing.
 			continue
diff --git a/internal/symbolizer/symbolizer_test.go b/internal/symbolizer/symbolizer_test.go
index e93e25e..50e33a6 100644
--- a/internal/symbolizer/symbolizer_test.go
+++ b/internal/symbolizer/symbolizer_test.go
@@ -279,8 +279,8 @@
 	return ""
 }
 
-func (mockObjFile) Base() uint64 {
-	return 0
+func (mockObjFile) ObjAddr(addr uint64) (uint64, error) {
+	return addr, nil
 }
 
 func (mockObjFile) BuildID() string {