diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 0f3b707afd..bfec07fd6e 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -24,6 +24,7 @@ Command | Description [goroutine](#goroutine) | Shows or changes current goroutine [goroutines](#goroutines) | List program goroutines. [help](#help) | Prints the help message. +[libraries](#libraries) | List loaded dynamic libraries [list](#list) | Show source code. [locals](#locals) | Print local variables. [next](#next) | Step over to next source line. @@ -266,6 +267,10 @@ Type "help" followed by the name of a command for more information about it. Aliases: h +## libraries +List loaded dynamic libraries + + ## list Show source code. diff --git a/_fixtures/internal/pluginsupport/pluginsupport.go b/_fixtures/internal/pluginsupport/pluginsupport.go new file mode 100644 index 0000000000..95ffeb07e8 --- /dev/null +++ b/_fixtures/internal/pluginsupport/pluginsupport.go @@ -0,0 +1,9 @@ +package pluginsupport + +type Something interface { + Callback(int) int +} + +type SomethingElse interface { + Callback2(int, int) float64 +} diff --git a/_fixtures/plugin1/plugin1.go b/_fixtures/plugin1/plugin1.go new file mode 100644 index 0000000000..c6f58e5de8 --- /dev/null +++ b/_fixtures/plugin1/plugin1.go @@ -0,0 +1,13 @@ +package main + +import "fmt" + +func Fn1() string { + return "hello" +} + +func HelloFn(n int) string { + n++ + s := fmt.Sprintf("hello%d", n) + return s +} diff --git a/_fixtures/plugin2/plugin2.go b/_fixtures/plugin2/plugin2.go new file mode 100644 index 0000000000..9177b05f19 --- /dev/null +++ b/_fixtures/plugin2/plugin2.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "github.com/derekparker/delve/_fixtures/internal/pluginsupport" +) + +func Fn2() string { + return "world" +} + +type asomethingelse struct { + x, y float64 +} + +func (a *asomethingelse) Callback2(n, m int) float64 { + r := a.x + 2*a.y + r += float64(n) / float64(m) + return r +} + +func TypesTest(s pluginsupport.Something) pluginsupport.SomethingElse { + if A != nil { + aIsNotNil(fmt.Sprintf("%s", A)) + } + return &asomethingelse{1.0, float64(s.Callback(2))} +} + +var A interface{} + +func aIsNotNil(str string) { + // nothing here +} diff --git a/_fixtures/plugintest.go b/_fixtures/plugintest.go new file mode 100644 index 0000000000..b3d4527a83 --- /dev/null +++ b/_fixtures/plugintest.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "os" + "plugin" + "runtime" +) + +func must(err error) { + if err != nil { + panic(err) + } +} + +func main() { + plug1, err := plugin.Open(os.Args[1]) + must(err) + + runtime.Breakpoint() + + plug2, err := plugin.Open(os.Args[2]) + must(err) + + runtime.Breakpoint() + + fn1, err := plug1.Lookup("Fn1") + must(err) + fn2, err := plug2.Lookup("Fn2") + must(err) + + a := fn1.(func() string)() + b := fn2.(func() string)() + + fmt.Println(plug1, plug2, fn1, fn2, a, b) +} diff --git a/_fixtures/plugintest2.go b/_fixtures/plugintest2.go new file mode 100644 index 0000000000..55f180427f --- /dev/null +++ b/_fixtures/plugintest2.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "github.com/derekparker/delve/_fixtures/internal/pluginsupport" + "os" + "plugin" +) + +type asomething struct { + n int +} + +func (a *asomething) Callback(n int) int { + return a.n + n +} + +func (a *asomething) String() string { + return "success" +} + +var ExeGlobal = &asomething{2} + +func must(err error) { + if err != nil { + panic(err) + } +} + +func main() { + plug1, err := plugin.Open(os.Args[1]) + must(err) + plug2, err := plugin.Open(os.Args[2]) + must(err) + fn1iface, err := plug1.Lookup("HelloFn") + must(err) + fn2iface, err := plug2.Lookup("TypesTest") + must(err) + fn1 := fn1iface.(func(int) string) + fn2 := fn2iface.(func(pluginsupport.Something) pluginsupport.SomethingElse) + a := fn1(3) + b := fn2(&asomething{2}) + fmt.Println(a, b, ExeGlobal) +} diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index d6c9e316f8..ff65bc1c0a 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -26,7 +26,8 @@ import ( "github.com/derekparker/delve/pkg/goversion" ) -// BinaryInfo holds information on the binary being executed. +// BinaryInfo holds information on the binaries being executed (this +// includes both the executable and also any loaded libraries). type BinaryInfo struct { // Path on disk of the binary being executed. Path string @@ -50,6 +51,8 @@ type BinaryInfo struct { staticBase uint64 + ElfDynamicSection ElfDynamicSection + // Maps package names to package paths, needed to lookup types inside DWARF info packageMap map[string]string @@ -79,6 +82,10 @@ type BinaryInfo struct { loadErrMu sync.Mutex loadErr error + + // Images is a list of loaded shared libraries (also known as + // shared objects on linux or DLLs on windws). + Images []*Image } // ErrUnsupportedLinuxArch is returned when attempting to debug a binary compiled for an unsupported architecture. @@ -288,6 +295,12 @@ type buildIDHeader struct { Type uint32 } +// ElfDynamicSection describes the .dynamic section of an ELF executable. +type ElfDynamicSection struct { + Addr uint64 // relocated address of where the .dynamic section is mapped in memory + Size uint64 // size of the .dynamic section of the executable +} + // NewBinaryInfo returns an initialized but unloaded BinaryInfo struct. func NewBinaryInfo(goos, goarch string) *BinaryInfo { r := &BinaryInfo{GOOS: goos, nameOfRuntimeType: make(map[uintptr]nameOfRuntimeTypeEntry), typeCache: make(map[dwarf.Offset]godwarf.Type)} @@ -411,6 +424,26 @@ func (bi *BinaryInfo) PCToFunc(pc uint64) *Function { return nil } +// Image represents a loaded library file (shared object on linux, DLL on windows). +type Image struct { + Path string + addr uint64 +} + +// AddSharedObject adds the specified shared object to bi. +func (bi *BinaryInfo) AddImage(path string, addr uint64) { + if !strings.HasPrefix(path, "/") { + return + } + for _, image := range bi.Images { + if image.Path == path && image.addr == addr { + return + } + } + //TODO(aarzilli): actually load informations about the image here + bi.Images = append(bi.Images, &Image{Path: path, addr: addr}) +} + // Close closes all internal readers. func (bi *BinaryInfo) Close() error { if bi.sepDebugCloser != nil { @@ -667,6 +700,11 @@ func (bi *BinaryInfo) LoadBinaryInfoElf(path string, entryPoint uint64, debugInf } } + if dynsec := elfFile.Section(".dynamic"); dynsec != nil { + bi.ElfDynamicSection.Addr = dynsec.Addr + bi.staticBase + bi.ElfDynamicSection.Size = dynsec.Size + } + dwarfFile := elfFile bi.dwarf, err = elfFile.DWARF() diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index 7b8f94c6f9..509529904f 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -702,6 +702,12 @@ continueLoop: return nil, err } + if p.BinInfo().GOOS == "linux" { + if err := linutil.ElfUpdateSharedObjects(p); err != nil { + return nil, err + } + } + if err := p.setCurrentBreakpoints(); err != nil { return nil, err } diff --git a/pkg/proc/linutil/dynamic.go b/pkg/proc/linutil/dynamic.go new file mode 100644 index 0000000000..31f3aaa39c --- /dev/null +++ b/pkg/proc/linutil/dynamic.go @@ -0,0 +1,172 @@ +package linutil + +import ( + "bytes" + "encoding/binary" + "errors" + + "github.com/derekparker/delve/pkg/proc" +) + +const ( + maxNumLibraries = 1000000 // maximum number of loaded libraries, to avoid loading forever on corrupted memory + maxLibraryPathLength = 1000000 // maximum length for the path of a library, to avoid loading forever on corrupted memory +) + +var ErrTooManyLibraries = errors.New("too many libraries") +var ErrStringTooLong = errors.New("string too long") + +const ( + _DT_NULL = 0 // DT_NULL as defined by SysV ABI specification + _DT_DEBUG = 21 // DT_DEBUG as defined by SysV ABI specification +) + +// dynamicSearchDebug searches for the DT_DEBUG entry in the .dynamic section +func dynamicSearchDebug(p proc.Process) (uint64, error) { + bi := p.BinInfo() + mem := p.CurrentThread() + + dynbuf := make([]byte, bi.ElfDynamicSection.Size) + _, err := mem.ReadMemory(dynbuf, uintptr(bi.ElfDynamicSection.Addr)) + if err != nil { + return 0, err + } + + rd := bytes.NewReader(dynbuf) + + for { + var tag, val uint64 + if err := binary.Read(rd, binary.LittleEndian, &tag); err != nil { + return 0, err + } + if err := binary.Read(rd, binary.LittleEndian, &val); err != nil { + return 0, err + } + switch tag { + case _DT_NULL: + return 0, nil + case _DT_DEBUG: + return val, nil + } + } +} + +// hard-coded offsets of the fields of the r_debug and link_map structs, see +// /usr/include/elf/link.h for a full description of those structs. +const ( + _R_DEBUG_MAP_OFFSET = 8 + _LINK_MAP_ADDR_OFFSET = 0 // offset of link_map.l_addr field (base address shared object is loaded at) + _LINK_MAP_NAME_OFFSET = 8 // offset of link_map.l_name field (absolute file name object was found in) + _LINK_MAP_LD = 16 // offset of link_map.l_ld field (dynamic section of the shared object) + _LINK_MAP_NEXT = 24 // offset of link_map.l_next field + _LINK_MAP_PREV = 32 // offset of link_map.l_prev field +) + +func onePtr(p proc.Process, addr uint64) (uint64, error) { + ptrbuf := make([]byte, p.BinInfo().Arch.PtrSize()) + _, err := p.CurrentThread().ReadMemory(ptrbuf, uintptr(addr)) + if err != nil { + return 0, err + } + return binary.LittleEndian.Uint64(ptrbuf), nil +} + +type linkMap struct { + addr uint64 + name string + ld uint64 + next, prev uint64 +} + +func readLinkMapNode(p proc.Process, r_map uint64) (*linkMap, error) { + bi := p.BinInfo() + + var lm linkMap + var ptrs [5]uint64 + for i := range ptrs { + var err error + ptrs[i], err = onePtr(p, r_map+uint64(bi.Arch.PtrSize()*i)) + if err != nil { + return nil, err + } + } + lm.addr = ptrs[0] + var err error + lm.name, err = readCString(p, ptrs[1]) + if err != nil { + return nil, err + } + lm.ld = ptrs[2] + lm.next = ptrs[3] + lm.prev = ptrs[4] + return &lm, nil +} + +func readCString(p proc.Process, addr uint64) (string, error) { + if addr == 0 { + return "", nil + } + mem := p.CurrentThread() + buf := make([]byte, 1) + r := []byte{} + for { + if len(r) > maxLibraryPathLength { + return "", ErrStringTooLong + } + _, err := mem.ReadMemory(buf, uintptr(addr)) + if err != nil { + return "", err + } + if buf[0] == 0 { + break + } + r = append(r, buf[0]) + addr++ + } + return string(r), nil +} + +// ElfUpdateSharedObjects reads the list of dynamic libraries loaded by the +// dynamic linker from the .dynamic section and uses it to updated p.BinInfo(). +// See the SysV ABI for a description of how the .dynamic section works: +// http://www.sco.com/developers/gabi/latest/contents.html +func ElfUpdateSharedObjects(p proc.Process) error { + bi := p.BinInfo() + if bi.ElfDynamicSection.Addr == 0 { + // no dynamic section, therefore nothing to do here + return nil + } + debugAddr, err := dynamicSearchDebug(p) + if err != nil { + return err + } + if debugAddr == 0 { + // no DT_DEBUG entry + return nil + } + + r_map, err := onePtr(p, debugAddr+_R_DEBUG_MAP_OFFSET) + if err != nil { + return err + } + + libs := []string{} + + for { + if r_map == 0 { + break + } + if len(libs) > maxNumLibraries { + return ErrTooManyLibraries + } + lm, err := readLinkMapNode(p, r_map) + if err != nil { + return err + } + bi.AddImage(lm.name, lm.addr) + libs = append(libs, lm.name) + r_map = lm.next + } + + return nil +} diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go index 7928edbe89..aadf7d625f 100644 --- a/pkg/proc/native/proc_linux.go +++ b/pkg/proc/native/proc_linux.go @@ -233,7 +233,7 @@ func (dbp *Process) updateThreadList() error { return err } } - return nil + return linutil.ElfUpdateSharedObjects(dbp) } func findExecutable(path string, pid int) string { @@ -453,6 +453,10 @@ func (dbp *Process) stop(trapthread *Thread) (err error) { } } + if err := linutil.ElfUpdateSharedObjects(dbp); err != nil { + return err + } + // set breakpoints on all threads for _, th := range dbp.threads { if th.CurrentBreakpoint.Breakpoint == nil { diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 5b79c99650..dcfbe59209 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -4117,3 +4117,40 @@ func TestIssue1374(t *testing.T) { } }) } + +func TestListImages(t *testing.T) { + pluginFixtures := protest.WithPlugins(t, "plugin1/", "plugin2/") + + withTestProcessArgs("plugintest", t, ".", []string{pluginFixtures[0].Path, pluginFixtures[1].Path}, 0, func(p proc.Process, fixture protest.Fixture) { + assertNoError(proc.Continue(p), t, "first continue") + plugin1Found := false + t.Logf("Libraries before:") + for _, image := range p.BinInfo().Images { + t.Logf("\t%#v", image) + if image.Path == pluginFixtures[0].Path { + plugin1Found = true + } + } + if !plugin1Found { + t.Fatalf("Could not find plugin1") + } + assertNoError(proc.Continue(p), t, "second continue") + plugin1Found, plugin2Found := false, false + t.Logf("Libraries after:") + for _, image := range p.BinInfo().Images { + t.Logf("\t%#v", image) + switch image.Path { + case pluginFixtures[0].Path: + plugin1Found = true + case pluginFixtures[1].Path: + plugin2Found = true + } + } + if !plugin1Found { + t.Fatalf("Could not find plugin1") + } + if !plugin2Found { + t.Fatalf("Could not find plugin2") + } + }) +} diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go index 733bad858e..218132d488 100644 --- a/pkg/proc/test/support.go +++ b/pkg/proc/test/support.go @@ -29,6 +29,8 @@ type Fixture struct { Path string // Source is the absolute path of the test binary source. Source string + // BuildDir is the directory where the build command was run. + BuildDir string } // FixtureKey holds the name and builds flags used for a test fixture. @@ -72,6 +74,7 @@ const ( // EnableDWZCompression will enable DWZ compression of DWARF sections. EnableDWZCompression BuildModePIE + BuildModePlugin ) // BuildFixture will compile the fixture 'name' using the provided build flags. @@ -125,6 +128,9 @@ func BuildFixture(name string, flags BuildFlags) Fixture { if flags&BuildModePIE != 0 { buildFlags = append(buildFlags, "-buildmode=pie") } + if flags&BuildModePlugin != 0 { + buildFlags = append(buildFlags, "-buildmode=plugin") + } if path != "" { buildFlags = append(buildFlags, name+".go") } @@ -151,7 +157,9 @@ func BuildFixture(name string, flags BuildFlags) Fixture { source, _ := filepath.Abs(path) source = filepath.ToSlash(source) - fixture := Fixture{Name: name, Path: tmpfile, Source: source} + absdir, _ := filepath.Abs(dir) + + fixture := Fixture{Name: name, Path: tmpfile, Source: source, BuildDir: absdir} Fixtures[fk] = fixture return Fixtures[fk] @@ -302,3 +310,22 @@ func DefaultTestBackend(testBackend *string) { *testBackend = "native" } } + +// WithPlugins builds the fixtures in plugins as plugins and returns them. +// The test calling WithPlugins will be skipped if the current combination +// of OS, architecture and version of GO doesn't support plugins or +// debugging plugins. +func WithPlugins(t *testing.T, plugins ...string) []Fixture { + if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 12) { + t.Skip("versions of Go before 1.12 do not include debug information in packages that import plugin (or they do but it's wrong)") + } + if runtime.GOOS != "linux" { + t.Skip("only supported on linux") + } + + r := make([]Fixture, len(plugins)) + for i := range plugins { + r[i] = BuildFixture(plugins[i], BuildModePlugin) + } + return r +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 3c702c4c5a..2bd03cbd03 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -343,6 +343,7 @@ Defines as an alias to or removes an alias.`}, edit [locspec] If locspec is omitted edit will open the current source file in the editor, otherwise it will open the specified location.`}, + {aliases: []string{"libraries"}, cmdFn: libraries, helpMsg: `List loaded dynamic libraries`}, } if client == nil || client.Recorded() { @@ -1559,6 +1560,18 @@ func disassCommand(t *Term, ctx callContext, args string) error { return nil } +func libraries(t *Term, ctx callContext, args string) error { + libs, err := t.client.ListDynamicLibraries() + if err != nil { + return err + } + d := digits(len(libs)) + for i := range libs { + fmt.Printf("%"+strconv.Itoa(d)+"d. %s\n", i, libs[i].Path) + } + return nil +} + func digits(n int) int { if n <= 0 { return 1 diff --git a/service/api/conversions.go b/service/api/conversions.go index 3daf42cb23..986226bc5c 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -314,3 +314,7 @@ func ConvertRegisters(in []proc.Register) (out []Register) { func ConvertCheckpoint(in proc.Checkpoint) (out Checkpoint) { return Checkpoint(in) } + +func ConvertImage(image *proc.Image) Image { + return Image{Path: image.Path} +} diff --git a/service/api/types.go b/service/api/types.go index 7873382c5b..aa5f8f3398 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -442,3 +442,8 @@ type Checkpoint struct { When string Where string } + +// Image represents a loaded shared object (go plugin or shared library) +type Image struct { + Path string +} diff --git a/service/client.go b/service/client.go index f4211c3aec..62c03cbe21 100644 --- a/service/client.go +++ b/service/client.go @@ -138,6 +138,9 @@ type Client interface { // IsMulticlien returns true if the headless instance is multiclient. IsMulticlient() bool + // ListDynamicLibraries returns a list of loaded dynamic libraries. + ListDynamicLibraries() ([]api.Image, error) + // Disconnect closes the connection to the server without sending a Detach request first. // If cont is true a continue command will be sent instead. Disconnect(cont bool) error diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 0fffa13e25..5cd97702b6 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -1118,6 +1118,18 @@ func (d *Debugger) ClearCheckpoint(id int) error { return d.target.ClearCheckpoint(id) } +// ListLibraries returns a list of loaded dynamic libraries. +func (d *Debugger) ListDynamicLibraries() []api.Image { + d.processMutex.Lock() + defer d.processMutex.Unlock() + bi := d.target.BinInfo() + r := make([]api.Image, len(bi.Images)) + for i := range bi.Images { + r[i] = api.ConvertImage(bi.Images[i]) + } + return r +} + func go11DecodeErrorCheck(err error) error { if _, isdecodeerr := err.(dwarf.DecodeError); !isdecodeerr { return err diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 5ada18bcef..5d517fb34c 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -395,6 +395,12 @@ func (c *RPCClient) Disconnect(cont bool) error { return c.client.Close() } +func (c *RPCClient) ListDynamicLibraries() ([]api.Image, error) { + var out ListDynamicLibrariesOut + c.call("ListDynamicLibraries", ListDynamicLibrariesIn{}, &out) + return out.List, nil +} + func (c *RPCClient) call(method string, args, reply interface{}) error { return c.client.Call("RPCServer."+method, args, reply) } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index e9193cf538..7d409b2715 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -685,3 +685,17 @@ func (s *RPCServer) FunctionReturnLocations(in FunctionReturnLocationsIn, out *F } return nil } + +// ListDynamicLibrariesIn holds the arguments of ListDynamicLibraries +type ListDynamicLibrariesIn struct { +} + +// ListDynamicLibrariesOut holds the return values of ListDynamicLibraries +type ListDynamicLibrariesOut struct { + List []api.Image +} + +func (s *RPCServer) ListDynamicLibraries(in ListDynamicLibrariesIn, out *ListDynamicLibrariesOut) error { + out.List = s.debugger.ListDynamicLibraries() + return nil +}