]> Cypherpunks repositories - gostls13.git/commitdiff
runtime/pprof, cmd/pprof: fix profiling for PIE
authorIan Lance Taylor <iant@golang.org>
Fri, 27 May 2016 23:03:44 +0000 (16:03 -0700)
committerIan Lance Taylor <iant@golang.org>
Tue, 31 May 2016 13:02:09 +0000 (13:02 +0000)
In order to support pprof for position independent executables, pprof
needs to adjust the PC addresses stored in the profile by the address at
which the program is loaded. The legacy profiling support which we use
already supports recording the GNU/Linux /proc/self/maps data
immediately after the CPU samples, so do that. Also change the pprof
symbolizer to use the information, if available, when looking up
addresses in the Go pcline data.

Fixes #15714.

Change-Id: I4bf679210ef7c51d85cf873c968ce82db8898e3e
Reviewed-on: https://go-review.googlesource.com/23525
Reviewed-by: Michael Hudson-Doyle <michael.hudson@canonical.com>
12 files changed:
src/cmd/internal/objfile/elf.go
src/cmd/internal/objfile/goobj.go
src/cmd/internal/objfile/macho.go
src/cmd/internal/objfile/objfile.go
src/cmd/internal/objfile/pe.go
src/cmd/internal/objfile/plan9obj.go
src/cmd/pprof/pprof.go
src/go/build/deps_test.go
src/runtime/crash_cgo_test.go
src/runtime/crash_test.go
src/runtime/pprof/pprof.go
src/runtime/pprof/pprof_test.go

index 3bad03409780a4e636227b1718f86e41d2751d92..c8114603d790b3e442138fbf34dfdf4e679010ca 100644 (file)
@@ -106,6 +106,15 @@ func (f *elfFile) goarch() string {
        return ""
 }
 
+func (f *elfFile) loadAddress() (uint64, error) {
+       for _, p := range f.elf.Progs {
+               if p.Type == elf.PT_LOAD {
+                       return p.Vaddr, nil
+               }
+       }
+       return 0, fmt.Errorf("unknown load address")
+}
+
 func (f *elfFile) dwarf() (*dwarf.Data, error) {
        return f.elf.DWARF()
 }
index 5a084a94bec2d1f298556f18a0fb09255e946b4a..43435efc68b481066491cae4976e29aa92c3465f 100644 (file)
@@ -94,6 +94,10 @@ func (f *goobjFile) goarch() string {
        return "GOARCH unimplemented for debug/goobj files"
 }
 
+func (f *goobjFile) loadAddress() (uint64, error) {
+       return 0, fmt.Errorf("unknown load address")
+}
+
 func (f *goobjFile) dwarf() (*dwarf.Data, error) {
        return nil, errors.New("no DWARF data in go object file")
 }
index 754674d7574857026e8f6da40c06a3332dee36a1..1d22a09b13d036e01f8691ec194a6cbf3879dd85 100644 (file)
@@ -125,6 +125,10 @@ func (x uint64s) Len() int           { return len(x) }
 func (x uint64s) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
 func (x uint64s) Less(i, j int) bool { return x[i] < x[j] }
 
+func (f *machoFile) loadAddress() (uint64, error) {
+       return 0, fmt.Errorf("unknown load address")
+}
+
 func (f *machoFile) dwarf() (*dwarf.Data, error) {
        return f.macho.DWARF()
 }
index 48ed9ed4896f522e1fdd67c2d5cf57fe400ea4bb..e5d99f086b5837681de82c0a9f7acf6c189969d5 100644 (file)
@@ -18,6 +18,7 @@ type rawFile interface {
        pcln() (textStart uint64, symtab, pclntab []byte, err error)
        text() (textStart uint64, text []byte, err error)
        goarch() string
+       loadAddress() (uint64, error)
        dwarf() (*dwarf.Data, error)
 }
 
@@ -95,6 +96,13 @@ func (f *File) GOARCH() string {
        return f.raw.goarch()
 }
 
+// LoadAddress returns the expected load address of the file.
+// This differs from the actual load address for a position-independent
+// executable.
+func (f *File) LoadAddress() (uint64, error) {
+       return f.raw.loadAddress()
+}
+
 // DWARF returns DWARF debug data for the file, if any.
 // This is for cmd/pprof to locate cgo functions.
 func (f *File) DWARF() (*dwarf.Data, error) {
index c02476237189c36e1e537627ec30eee3e2387211..46b23172422b749fc5c9aca8bc2113a61e7d8985 100644 (file)
@@ -199,6 +199,10 @@ func (f *peFile) goarch() string {
        return ""
 }
 
+func (f *peFile) loadAddress() (uint64, error) {
+       return 0, fmt.Errorf("unknown load address")
+}
+
 func (f *peFile) dwarf() (*dwarf.Data, error) {
        return f.pe.DWARF()
 }
index 6ee389dc2e2223e2b52816c34e4b4f041fec3808..3e34f65ae7e8e795c38c324316af6ed4ed3823d5 100644 (file)
@@ -147,6 +147,10 @@ func (f *plan9File) goarch() string {
        return ""
 }
 
+func (f *plan9File) loadAddress() (uint64, error) {
+       return 0, fmt.Errorf("unknown load address")
+}
+
 func (f *plan9File) dwarf() (*dwarf.Data, error) {
        return nil, errors.New("no DWARF data in Plan 9 file")
 }
index bce37dcb97199f0692cc0343d7023b431245ca0e..0187045b4afb3da337ce9833a39299cb52c1e15a 100644 (file)
@@ -117,6 +117,9 @@ func (*objTool) Open(name string, start uint64) (plugin.ObjFile, error) {
                name: name,
                file: of,
        }
+       if load, err := of.LoadAddress(); err == nil {
+               f.offset = start - load
+       }
        return f, nil
 }
 
@@ -169,10 +172,11 @@ func (*objTool) SetConfig(config string) {
 // (instead of invoking GNU binutils).
 // A file represents a single executable being analyzed.
 type file struct {
-       name string
-       sym  []objfile.Sym
-       file *objfile.File
-       pcln *gosym.Table
+       name   string
+       offset uint64
+       sym    []objfile.Sym
+       file   *objfile.File
+       pcln   *gosym.Table
 
        triedDwarf bool
        dwarf      *dwarf.Data
@@ -200,6 +204,7 @@ func (f *file) SourceLine(addr uint64) ([]plugin.Frame, error) {
                }
                f.pcln = pcln
        }
+       addr -= f.offset
        file, line, fn := f.pcln.PCToLine(addr)
        if fn != nil {
                frame := []plugin.Frame{
index f9a428edd42b4552694d77ecd024f89b83fc321e..335e774a7cf6b0820be6a1309a17df0ffd0406f7 100644 (file)
@@ -173,7 +173,7 @@ var pkgDeps = map[string][]string{
        "regexp":         {"L2", "regexp/syntax"},
        "regexp/syntax":  {"L2"},
        "runtime/debug":  {"L2", "fmt", "io/ioutil", "os", "time"},
-       "runtime/pprof":  {"L2", "fmt", "text/tabwriter"},
+       "runtime/pprof":  {"L2", "fmt", "os", "text/tabwriter"},
        "runtime/trace":  {"L0"},
        "text/tabwriter": {"L2"},
 
index 5d1cc77c98aca80884aaec0b363da4a972bab225..4f7c10b923e3769c7849772dac8e7dcd3f38d773 100644 (file)
@@ -263,3 +263,33 @@ func TestCgoPprof(t *testing.T) {
                t.Error("missing cpuHog in pprof output")
        }
 }
+
+func TestCgoPprofPIE(t *testing.T) {
+       if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
+               t.Skipf("not yet supported on %s/%s", runtime.GOOS, runtime.GOARCH)
+       }
+       testenv.MustHaveGoRun(t)
+
+       exe, err := buildTestProg(t, "testprogcgo", "-ldflags=-extldflags=-pie")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       got, err := testEnv(exec.Command(exe, "CgoPprof")).CombinedOutput()
+       if err != nil {
+               t.Fatal(err)
+       }
+       fn := strings.TrimSpace(string(got))
+       defer os.Remove(fn)
+
+       top, err := exec.Command("go", "tool", "pprof", "-top", "-nodecount=1", exe, fn).CombinedOutput()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       t.Logf("%s", top)
+
+       if !bytes.Contains(top, []byte("cpuHog")) {
+               t.Error("missing cpuHog in pprof output")
+       }
+}
index 2941b8e8f8a6d33e97a261f25358ebc47bbe69a1..ec740990dc4eace57900a917acc7918de3ba9775 100644 (file)
@@ -69,7 +69,7 @@ func runTestProg(t *testing.T, binary, name string) string {
        return string(got)
 }
 
-func buildTestProg(t *testing.T, binary string) (string, error) {
+func buildTestProg(t *testing.T, binary string, flags ...string) (string, error) {
        checkStaleRuntime(t)
 
        testprog.Lock()
@@ -86,23 +86,27 @@ func buildTestProg(t *testing.T, binary string) (string, error) {
        if testprog.target == nil {
                testprog.target = make(map[string]buildexe)
        }
-       target, ok := testprog.target[binary]
+       name := binary
+       if len(flags) > 0 {
+               name += "_" + strings.Join(flags, "_")
+       }
+       target, ok := testprog.target[name]
        if ok {
                return target.exe, target.err
        }
 
-       exe := filepath.Join(testprog.dir, binary+".exe")
-       cmd := exec.Command("go", "build", "-o", exe)
+       exe := filepath.Join(testprog.dir, name+".exe")
+       cmd := exec.Command("go", append([]string{"build", "-o", exe}, flags...)...)
        cmd.Dir = "testdata/" + binary
        out, err := testEnv(cmd).CombinedOutput()
        if err != nil {
                exe = ""
-               target.err = fmt.Errorf("building %s: %v\n%s", binary, err, out)
-               testprog.target[binary] = target
+               target.err = fmt.Errorf("building %s %v: %v\n%s", binary, flags, err, out)
+               testprog.target[name] = target
                return "", target.err
        }
        target.exe = exe
-       testprog.target[binary] = target
+       testprog.target[name] = target
        return exe, nil
 }
 
index 728c3dc24a84d5ae82920a8e51d010306960be8c..b05c925ad16500732fad1551e6bc437cdad50329 100644 (file)
@@ -13,6 +13,7 @@ import (
        "bytes"
        "fmt"
        "io"
+       "os"
        "runtime"
        "sort"
        "strings"
@@ -620,6 +621,42 @@ func profileWriter(w io.Writer) {
                }
                w.Write(data)
        }
+
+       // We are emitting the legacy profiling format, which permits
+       // a memory map following the CPU samples. The memory map is
+       // simply a copy of the GNU/Linux /proc/self/maps file. The
+       // profiler uses the memory map to map PC values in shared
+       // libraries to a shared library in the filesystem, in order
+       // to report the correct function and, if the shared library
+       // has debug info, file/line. This is particularly useful for
+       // PIE (position independent executables) as on ELF systems a
+       // PIE is simply an executable shared library.
+       //
+       // Because the profiling format expects the memory map in
+       // GNU/Linux format, we only do this on GNU/Linux for now. To
+       // add support for profiling PIE on other ELF-based systems,
+       // it may be necessary to map the system-specific mapping
+       // information to the GNU/Linux format. For a reasonably
+       // portable C++ version, see the FillProcSelfMaps function in
+       // https://github.com/gperftools/gperftools/blob/master/src/base/sysinfo.cc
+       //
+       // The code that parses this mapping for the pprof tool is
+       // ParseMemoryMap in cmd/internal/pprof/legacy_profile.go, but
+       // don't change that code, as similar code exists in other
+       // (non-Go) pprof readers. Change this code so that that code works.
+       //
+       // We ignore errors reading or copying the memory map; the
+       // profile is likely usable without it, and we have no good way
+       // to report errors.
+       if runtime.GOOS == "linux" {
+               f, err := os.Open("/proc/self/maps")
+               if err == nil {
+                       io.WriteString(w, "\nMAPPED_LIBRARIES:\n")
+                       io.Copy(w, f)
+                       f.Close()
+               }
+       }
+
        cpu.done <- true
 }
 
index 3852d93e7264c6d98d493772a82416857b3fc0c4..a6f5eda458789a84266a5d0fe0808ac1531c61ec 100644 (file)
@@ -86,10 +86,14 @@ func TestCPUProfileMultithreaded(t *testing.T) {
        })
 }
 
-func parseProfile(t *testing.T, bytes []byte, f func(uintptr, []uintptr)) {
+func parseProfile(t *testing.T, valBytes []byte, f func(uintptr, []uintptr)) {
        // Convert []byte to []uintptr.
-       l := len(bytes) / int(unsafe.Sizeof(uintptr(0)))
-       val := *(*[]uintptr)(unsafe.Pointer(&bytes))
+       l := len(valBytes)
+       if i := bytes.Index(valBytes, []byte("\nMAPPED_LIBRARIES:\n")); i >= 0 {
+               l = i
+       }
+       l /= int(unsafe.Sizeof(uintptr(0)))
+       val := *(*[]uintptr)(unsafe.Pointer(&valBytes))
        val = val[:l]
 
        // 5 for the header, 3 for the trailer.