Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set base address on OSX #313

Merged
merged 4 commits into from
Feb 9, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 17 additions & 2 deletions internal/binutils/binutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,25 @@ func (b *binrep) openMachO(name string, start, limit, offset uint64) (plugin.Obj
}
defer of.Close()

// Subtract the load address of the __TEXT section. Usually 0 for shared
// libraries or 0x100000000 for executables. You can check this value by
// running `objdump -private-headers <file>`.

textSegment := of.Segment("__TEXT")
if textSegment == nil {
return nil, fmt.Errorf("Parsing %s: no __TEXT segment", name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error messages should start with lowercase: https://github.com/golang/go/wiki/CodeReviewComments#error-strings

Suggesting fmt.Errorf("not a mach executable: no __TEXT segment: %s", name), to be somewhat consistent with fmt.Errorf("unrecognized binary: %s", name) above in this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied the one above but ok.

}
if textSegment.Addr > start {
return nil, fmt.Errorf("Parsing %s: __TEXT segment address (0x%x) > mapping start address (0x%x)",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error messages should start with lowercase: https://github.com/golang/go/wiki/CodeReviewComments#error-strings

Suggesting fmt.Errorf("not a mach executable: __TEXT segment address (0x%x) > mapping start address (0x%x): %s", testSegment.Addr, start, name).

By the way, could base be negative in case of some exotic executable ASLR? Probably not, I think in reality for PIE executables textSegment address is zero, at least that's what I saw on Linux.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I wasn't sure about that but even if it is you'd probably want to know about it - i.e. get a bug report here, rather than just having it underflow and fail silently (or get lucky with the unsigned arithmetic).

name, textSegment.Addr, start)
}

base := start - textSegment.Addr

if b.fast || (!b.addr2lineFound && !b.llvmSymbolizerFound) {
return &fileNM{file: file{b: b, name: name}}, nil
return &fileNM{file: file{b: b, name: name, base: base}}, nil
}
return &fileAddr2Line{file: file{b: b, name: name}}, nil
return &fileAddr2Line{file: file{b: b, name: name, base: base}}, nil
}

func (b *binrep) openELF(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
Expand Down
92 changes: 78 additions & 14 deletions internal/binutils/binutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,20 @@ func TestSetFastSymbolization(t *testing.T) {

func skipUnlessLinuxAmd64(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
t.Skip("Disasm only tested on x86-64 linux")
t.Skip("This test only works on x86-64 Linux")
}
}

func skipUnlessDarwinAmd64(t *testing.T) {
if runtime.GOOS != "darwin" || runtime.GOARCH != "amd64" {
t.Skip("This test only works on x86-64 Mac")
}
}

func TestDisasm(t *testing.T) {
skipUnlessLinuxAmd64(t)
bu := &Binutils{}
insts, err := bu.Disasm(filepath.Join("testdata", "hello"), 0, math.MaxUint64)
insts, err := bu.Disasm(filepath.Join("testdata", "exe_linux_64"), 0, math.MaxUint64)
if err != nil {
t.Fatalf("Disasm: unexpected error %v", err)
}
Expand All @@ -199,6 +205,17 @@ func TestDisasm(t *testing.T) {
}
}

func findSymbol(syms []*plugin.Sym, name string) *plugin.Sym {
for _, s := range syms {
for _, n := range s.Name {
if n == name {
return s
}
}
}
return nil
}

func TestObjFile(t *testing.T) {
skipUnlessLinuxAmd64(t)
for _, tc := range []struct {
Expand All @@ -215,7 +232,7 @@ func TestObjFile(t *testing.T) {
} {
t.Run(tc.desc, func(t *testing.T) {
bu := &Binutils{}
f, err := bu.Open(filepath.Join("testdata", "hello"), tc.start, tc.limit, tc.offset)
f, err := bu.Open(filepath.Join("testdata", "exe_linux_64"), tc.start, tc.limit, tc.offset)
if err != nil {
t.Fatalf("Open: unexpected error %v", err)
}
Expand All @@ -225,17 +242,7 @@ func TestObjFile(t *testing.T) {
t.Fatalf("Symbols: unexpected error %v", err)
}

find := func(name string) *plugin.Sym {
for _, s := range syms {
for _, n := range s.Name {
if n == name {
return s
}
}
}
return nil
}
m := find("main")
m := findSymbol(syms, "main")
if m == nil {
t.Fatalf("Symbols: did not find main")
}
Expand All @@ -255,6 +262,63 @@ func TestObjFile(t *testing.T) {
}
}

func TestMachoFiles(t *testing.T) {
skipUnlessDarwinAmd64(t)

// Load `file`, pretending it was mapped at `start`. Then get the symbol
// table. Check that it contains the symbol `sym` and that the address
// `addr` gives the `expected` stack trace.
for _, tc := range []struct {
desc string
file string
start, limit, offset uint64
addr uint64
sym string
expected []plugin.Frame
}{
{"normal mapping", "exe_mac_64", 0x100000000, math.MaxUint64, 0,
0x100000f50, "_main",
[]plugin.Frame{
{Func: "main", File: "/tmp/hello.c", Line: 3},
}},
{"other mapping", "exe_mac_64", 0x200000000, math.MaxUint64, 0,
0x200000f50, "_main",
[]plugin.Frame{
{Func: "main", File: "/tmp/hello.c", Line: 3},
}},
{"lib normal mapping", "lib_mac_64", 0, math.MaxUint64, 0,
0xfa0, "_bar",
[]plugin.Frame{
{Func: "bar", File: "/tmp/lib.c", Line: 6},
}},
} {
t.Run(tc.desc, func(t *testing.T) {
bu := &Binutils{}
f, err := bu.Open(filepath.Join("testdata", tc.file), tc.start, tc.limit, tc.offset)
if err != nil {
t.Fatalf("Open: unexpected error %v", err)
}
defer f.Close()
syms, err := f.Symbols(nil, 0)
if err != nil {
t.Fatalf("Symbols: unexpected error %v", err)
}

m := findSymbol(syms, tc.sym)
if m == nil {
t.Fatalf("Symbols: could not find symbol %v", tc.sym)
}
gotFrames, err := f.SourceLine(tc.addr)
if err != nil {
t.Fatalf("SourceLine: unexpected error %v", err)
}
if !reflect.DeepEqual(gotFrames, tc.expected) {
t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, tc.expected)
}
})
}
}

func TestLLVMSymbolizer(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("testtdata/llvm-symbolizer has only been tested on linux")
Expand Down
34 changes: 29 additions & 5 deletions internal/binutils/disasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,48 @@ var (
func findSymbols(syms []byte, file string, r *regexp.Regexp, address uint64) ([]*plugin.Sym, error) {
// Collect all symbols from the nm output, grouping names mapped to
// the same address into a single symbol.

// The symbols to return.
var symbols []*plugin.Sym

// The current group of symbol names, and the address they are all at.
names, start := []string{}, uint64(0)

buf := bytes.NewBuffer(syms)
for symAddr, name, err := nextSymbol(buf); err == nil; symAddr, name, err = nextSymbol(buf) {

for {
symAddr, name, err := nextSymbol(buf)
if err == io.EOF {
// Done! If there was an unfinished group, append it.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit / opiniated: remove the exclamation mark, those are overly emotional for comments and log messages to my taste...

if len(names) != 0 {
if match := matchSymbol(names, start, symAddr-1, r, address); match != nil {
symbols = append(symbols, &plugin.Sym{Name: match, File: file, Start: start, End: symAddr - 1})
}
}

// And return the symbols.
return symbols, nil
}

if err != nil {
// There was some kind of serious error reading nm's output.
return nil, err
}
if start == symAddr {

// If this symbol is at the same address as the current group, add it to the group.
if symAddr == start {
names = append(names, name)
continue
}

// Otherwise append the current group to the list of symbols.
if match := matchSymbol(names, start, symAddr-1, r, address); match != nil {
symbols = append(symbols, &plugin.Sym{Name: match, File: file, Start: start, End: symAddr - 1})
}

// And start a new group.
names, start = []string{name}, symAddr
}

return symbols, nil
}

// matchSymbol checks if a symbol is to be selected by checking its
Expand All @@ -62,7 +86,7 @@ func matchSymbol(names []string, start, end uint64, r *regexp.Regexp, address ui
return names
}
for _, name := range names {
if r.MatchString(name) {
if r == nil || r.MatchString(name) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we pass r as nil from anywhere? Ah, you do know in f.Symbols(nil, 0) in the test. I think you can just do f.Symbols(regexp.MustCompile(tc.sym), 0)?.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No but the comments for the Symbols() interface say it can be nil.

return []string{name}
}

Expand Down
Binary file added internal/binutils/testdata/exe_mac_64
Binary file not shown.
20 changes: 20 additions & 0 deletions internal/binutils/testdata/exe_mac_64.dSYM/Contents/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.exe_mac_64</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>dSYM</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
Binary file not shown.
Binary file added internal/binutils/testdata/lib_mac_64
Binary file not shown.
20 changes: 20 additions & 0 deletions internal/binutils/testdata/lib_mac_64.dSYM/Contents/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading https://stackoverflow.com/questions/8058497/can-clang-build-executables-with-dsym-debug-information, it appears to me that building the test binaries from the command line it should be possible to have the debug information as part of those, and these additional dSYM files and directory would not be needed. I can look into doing that myself, can you include the source code of the executable and library test binaries that you used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep you can build it from source too. I just copied how the Linux test worked.

The only weird thing is if you have linking as a separate step (i.e. clang++ -c foo.cpp -o foo.o; clang++ foo.o -o foo rather than just doing it all in one step you need to run dysmutil on the binary. Shouldn't matter in this case because it is just one file.

<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.lib_mac_64</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>dSYM</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
Binary file not shown.