internal/gocore: support PIE ELF binaries

Go binaries built with `-buildmode=pie` are loaded at random addresses
(ASLR) in memory by Linux.

Before this change, the mappings read from the `NT_NOTE` section of the
ELF binary matches the exe mappings:

    # From NT_NOTE:
    0x400000-0x491000             ----   593920 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x0
    0x491000-0x54b000             ----   761856 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x91000
    0x54b000-0x556000             ----    45056 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x14b000

    # From executable:
    0x400000-0x491000             r-xp   593920 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x0
    0x491000-0x54b000             r--p   761856 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x91000
    0x54b000-0x555000             rw-p    40960 /tmp/TestVersionsgoroot#003207751013/001/test.exe+0x14b000
    0x555000-0x579000             rw-p   147456 anon+0x-440

    # From core
    0xc000000000-0xc000400000     rw-p  4194304 /tmp/TestVersionsgoroot#003207751013/001/core+0x32000
    0x7f8b17c00000-0x7f8b19c00000 rw-p 33554432 /tmp/TestVersionsgoroot#003207751013/001/core+0x432000
    0x7f8b29d80000-0x7f8b29d81000 rw-p     4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2432000
    0x7f8b49d80000-0x7f8b49d81000 rw-p     4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2433000
    0x7f8b5bc30000-0x7f8b5bc31000 rw-p     4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2434000
    0x7f8b5e006000-0x7f8b5e007000 rw-p     4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x2435000
    0x7f8b5e466000-0x7f8b5e4c6000 rw-p   393216 /tmp/TestVersionsgoroot#003207751013/001/core+0x2436000
    0x7f8b5e4c6000-0x7f8b5e5c6000 rw-p  1048576 /tmp/TestVersionsgoroot#003207751013/001/core+0x2496000
    0x7f8b5e5c6000-0x7f8b5e5d7000 rw-p    69632 /tmp/TestVersionsgoroot#003207751013/001/core+0x2596000
    0x7f8b5e657000-0x7f8b5e658000 rw-p     4096 /tmp/TestVersionsgoroot#003207751013/001/core+0x25a7000
    0x7f8b5e6d7000-0x7f8b5e737000 rw-p   393216 /tmp/TestVersionsgoroot#003207751013/001/core+0x25a8000
    0x7f8b5e737000-0x7f8b5e73b000 r--p    16384 /tmp/TestVersionsgoroot#003207751013/001/core+0x2608000
    0x7f8b5e73b000-0x7f8b5e73d000 r-xp     8192 /tmp/TestVersionsgoroot#003207751013/001/core+0x260c000
    0x7ffd88d3c000-0x7ffd88d5e000 rw-p   139264 /tmp/TestVersionsgoroot#003207751013/001/core+0x260e000

And things worked fine. But, with PIE binaries, we see the following raw
addresses when loading (e.g.):

    # From NT_NOTE:
    0x563f53d35000-0x563f53dc7000 ----   598016 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x0
    0x563f53dc7000-0x563f53e10000 ----   299008 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x92000
    0x563f53e10000-0x563f53ea3000 ----   602112 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0xdb000
    0x563f53ea3000-0x563f53eaf000 ----    49152 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x16e000
    0x7ff5d0deb000-0x7ff5d0dec000 ----     4096 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x0
    0x7ff5d0dec000-0x7ff5d0e11000 ----   151552 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x1000
    0x7ff5d0e11000-0x7ff5d0e1b000 ----    40960 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x26000
    0x7ff5d0e1b000-0x7ff5d0e1f000 ----    16384 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2+0x30000

    # From executable:
    0x400000-0x492000             r-xp   598016 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x0
    0x492000-0x4db000             r--p   299008 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x92000
    0x4db000-0x56f000             rw-p   606208 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0xdb000
    0x56f000-0x579000             rw-p    40960 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/test.exe+0x16f000
    0x579000-0x59d000             rw-p   147456 anon+0x-460

    # From core
    0xc000000000-0xc000400000     rw-p  4194304 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x3000
    0x563f53d35000-0x563f53d36000 r-xp     4096 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x403000
    0x563f53d36000-0x563f53dc7000 r-xp   593920 anon+0x0
    0x563f53dc7000-0x563f53e10000 r--p   299008 anon+0x0
    0x563f53e10000-0x563f53ea3000 r--p   602112 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x404000
    0x563f53ea3000-0x563f53eaf000 rw-p    49152 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x497000
    0x563f53eaf000-0x563f53ed2000 rw-p   143360 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x4a3000
    ...
    0x7ff5d0deb000-0x7ff5d0dec000 r--p     4096 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x26a4000
    0x7ff5d0dec000-0x7ff5d0e11000 r-xp   151552 anon+0x0
    0x7ff5d0e11000-0x7ff5d0e1b000 r--p    40960 anon+0x0
    0x7ff5d0e1b000-0x7ff5d0e1f000 rw-p    16384 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x26a5000
    0x7fff34a26000-0x7fff34a48000 rw-p   139264 /tmp/TestVersionsgoroot-buildmode=pie3327190927/001/core+0x26a9000

This caused two issues:

 1. The mappings from the executable and the core were not unified
    properly done in `addProgMappings -> splicedMemory.Add` due to the
    address ranges being different. Both existeded in the
    `splicedMemory` set. Normally, the core mappings would override the
    executable mappings when they overlap. To correct for this, add the
    load offset (`staticBase`) to the executable addresses.
 2. DWARF data (contained in the executable) references the base
    addresses (for example, `allgs` is at 0x4003c4). Due to #1, a lookup
    *would not crash*, but it would read the location from the
    executable. In a PIE binary, such a lookup into the `.data` section
    would be a relocation, and be present in the binary as all zeroes
    (0x0). If #1 were fixed to offset by `staticBase` without fixing #2
    would result in a read from unmapped memory.

Inspiration was taken from the Delve commit that added PIE support for
multiple platforms/architectures in
https://github.com/go-delve/delve/commit/025d47c6e96e8ab5a2d2c142ae554e760cddabf8.

Also add tests that this works. I verified that if the changes from
non-test files are omitted, the test fails.

Change-Id: Ifa08f71e7ed22320b78bc6015554e997b8ae521d
Reviewed-on: https://go-review.googlesource.com/c/debug/+/618977
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Nicolas Hillegeer <aktau@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
diff --git a/internal/core/process.go b/internal/core/process.go
index 7890be5..74400e4 100644
--- a/internal/core/process.go
+++ b/internal/core/process.go
@@ -40,6 +40,7 @@
 	meta metadata // basic metadata about the core
 
 	entryPoint Address
+	staticBase uint64    // Offset at which the executable was loaded in memory. 0 when binary is not-PIE.
 	args       string    // first part of args retrieved from NT_PRPSINFO
 	threads    []*Thread // os threads (TODO: map from pid?)
 
@@ -183,6 +184,12 @@
 	return p.syms, p.symErr
 }
 
+// StaticBase returns the offset at which the main executable was loaded in
+// memory. For example, it should be used when dereferencing DWARF locations.
+func (p *Process) StaticBase() uint64 {
+	return p.staticBase
+}
+
 var mapFile = func(fd int, offset int64, length int) (data []byte, err error) {
 	return nil, fmt.Errorf("file mapping is not implemented yet")
 }
@@ -241,11 +248,16 @@
 		return nil, fmt.Errorf("failed to parse executable: %v", err)
 	}
 
+	staticBase := uint64(entryPoint) - exeElf.Entry // If not PIE, this is 0.
+	if exeElf.Entry > uint64(entryPoint) {
+		return nil, fmt.Errorf("malformed binary or core, core entry point (%d) - executable entry point (%d) is < 0", entryPoint, exeElf.Entry)
+	}
+
 	// The base memory layout is defined by the binary itself. Additional
 	// mappings from the core layer on top. This ordering is important to
 	// ensure that dirty data/bss pages from the core take priority over
 	// the initial state from the binary.
-	mem := readExecMappings(exeFile, exeElf)
+	mem := readExecMappings(exeFile, exeElf, staticBase)
 	addCoreMappings(&mem, coreFile, coreElf)
 	// Add os.File references to mappings of files.
 	warnings := updateMappingFiles(&mem, fileMappings, base, exeFile, origExePath)
@@ -342,6 +354,7 @@
 	p := &Process{
 		meta:       meta,
 		entryPoint: entryPoint,
+		staticBase: staticBase,
 		args:       args,
 		threads:    threads,
 		memory:     mem,
@@ -357,13 +370,14 @@
 }
 
 // readExecMappings returns the memory mappings defined by the executable
-// itself.
-func readExecMappings(exeFile *os.File, exeElf *elf.File) splicedMemory {
+// itself. staticBase should be the offset at which the executable was loaded in
+// memory.
+func readExecMappings(exeFile *os.File, exeElf *elf.File, staticBase uint64) splicedMemory {
 	// Load virtual memory mappings.
 	var mem splicedMemory
 	for _, prog := range exeElf.Progs {
 		if prog.Type == elf.PT_LOAD {
-			addProgMappings(&mem, prog, exeFile)
+			addProgMappings(&mem, prog, exeFile, staticBase)
 		}
 	}
 	return mem
@@ -373,14 +387,18 @@
 func addCoreMappings(mem *splicedMemory, coreFile *os.File, coreElf *elf.File) {
 	for _, prog := range coreElf.Progs {
 		if prog.Type == elf.PT_LOAD {
-			addProgMappings(mem, prog, coreFile)
+			addProgMappings(mem, prog, coreFile, 0)
 		}
 	}
 }
 
 // addProgMappings adds memory mappings for prog (from file f) to mem.
-func addProgMappings(mem *splicedMemory, prog *elf.Prog, f *os.File) {
+// staticBase is added to the p_vaddr [1].
+//
+// [1]: https://man7.org/linux/man-pages/man5/elf.5.html
+func addProgMappings(mem *splicedMemory, prog *elf.Prog, f *os.File, staticBase uint64) {
 	min := Address(prog.Vaddr)
+	min = min.Add(int64(staticBase))
 	max := min.Add(int64(prog.Memsz))
 	var perm Perm
 	if prog.Flags&elf.PF_R != 0 {
diff --git a/internal/gocore/dwarf.go b/internal/gocore/dwarf.go
index 967531a..e8f9497 100644
--- a/internal/gocore/dwarf.go
+++ b/internal/gocore/dwarf.go
@@ -455,6 +455,7 @@
 		} else {
 			a = core.Address(p.proc.ByteOrder().Uint32(loc[1:]))
 		}
+		a = a.Add(int64(p.proc.StaticBase()))
 		if !p.proc.Writeable(a) {
 			// Read-only globals can't have heap pointers.
 			// TODO: keep roots around anyway?
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index 91f69c1..8c8c58e 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -85,7 +85,7 @@
 
 // loadExampleGenerated generates a core from a binary built with
 // runtime.GOROOT().
-func loadExampleGenerated(t *testing.T) *Process {
+func loadExampleGenerated(t *testing.T, buildFlags ...string) *Process {
 	t.Helper()
 	testenv.MustHaveGoBuild(t)
 	switch runtime.GOOS {
@@ -100,7 +100,7 @@
 	defer cleanup()
 
 	dir := t.TempDir()
-	file, output, err := generateCore(dir)
+	file, output, err := generateCore(dir, buildFlags...)
 	t.Logf("crasher output: %s", output)
 	if err != nil {
 		t.Fatalf("generateCore() got err %v want nil", err)
@@ -157,7 +157,7 @@
 	}
 }
 
-func generateCore(dir string) (string, []byte, error) {
+func generateCore(dir string, buildFlags ...string) (string, []byte, error) {
 	goTool, err := testenv.GoTool()
 	if err != nil {
 		return "", nil, fmt.Errorf("cannot find go tool: %w", err)
@@ -170,7 +170,10 @@
 	}
 
 	srcPath := filepath.Join(cwd, source)
-	cmd := exec.Command(goTool, "build", "-o", "test.exe", srcPath)
+	argv := []string{"build"}
+	argv = append(argv, buildFlags...)
+	argv = append(argv, "-o", "test.exe", srcPath)
+	cmd := exec.Command(goTool, argv...)
 	cmd.Dir = dir
 
 	b, err := cmd.CombinedOutput()
@@ -443,15 +446,22 @@
 	}
 
 	t.Run("goroot", func(t *testing.T) {
-		p := loadExampleGenerated(t)
-		checkProcess(t, p)
+		for _, buildFlags := range [][]string{
+			nil,
+			{"-buildmode=pie"},
+		} {
+			t.Run(strings.Join(buildFlags, ","), func(t *testing.T) {
+				p := loadExampleGenerated(t, buildFlags...)
+				checkProcess(t, p)
 
-		// TODO(aktau): Move sanityCheckDominator into sanityCheckProcess once this
-		//              passes for loadExampleGenerated.
-		t.Skip(`skipping dominator check due to "panic: can't find type runtime.itab"`)
-		lt := runLT(p)
-		if !checkDominator(t, lt) {
-			t.Errorf("sanityCheckDominator(...) = false, want true")
+				// TODO(aktau): Move checkDominator into checkProcess once this passes
+				// for loadExampleGenerated.
+				t.Skip(`skipping dominator check due to "panic: can't find type runtime.itab"`)
+				lt := runLT(p)
+				if !checkDominator(t, lt) {
+					t.Errorf("checkDominator(...) = false, want true")
+				}
+			})
 		}
 	})
 }