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

add support for stack traces on macosx #780

Merged
merged 2 commits into from Feb 21, 2018
Merged

add support for stack traces on macosx #780

merged 2 commits into from Feb 21, 2018

Conversation

bnoordhuis
Copy link
Contributor

Add basic address->symbol resolution support. Uses symtab data from the
MachO image, not external dSYM data; that's left as a future exercise.

The net effect is that we can now map addresses to function names but
not much more. File names and line number data will have to wait until
a future pull request.

Partially fixes #434.

Style feedback very welcome, by the way. I tried to follow the house style but I probably missed things.

Add basic address->symbol resolution support.  Uses symtab data from the
MachO image, not external dSYM data; that's left as a future exercise.

The net effect is that we can now map addresses to function names but
not much more.  File names and line number data will have to wait until
a future pull request.

Partially fixes #434.
}

fn readNoEof(in: &io.FileInStream, sink: var) !void {
if (@typeOf(sink) == []Nlist64) {
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 couldn't figure out a way to do this generically.

Copy link
Member

@andrewrk andrewrk Feb 19, 2018

Choose a reason for hiding this comment

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

I have a lot of responses to this, starting with minimal changes, ending with let's change the standard library.

  1. I'm actually fine with this, given that it's at least correct for this file, and it's a private function so it's not really exposed to much of anything.

  2. Here's how to do what you wanted to do:

fn readNoEof(in: &io.FileInStream, sink: var) !void {
    switch (@typeId(@typeOf(sink)) {
        builtin.TypeId.Slice => {
            const T = @typeOf(sink).Child;
            ...
        },
        builtin.TypeId.Pointer => {
            const T = @typeOf(sink).Child;
            ...
        },
        else => @compileError("expected slice or pointer"),
    }
}
  1. However, let's use this as an example to write idiomatic zig code. I think it would be better to separate this out into 2 functions:
fn readNoEof(in: &io.FileInStream, comptime T: type, result: []T) !void {
    return in.stream.readNoEof(([]u8)(result));
}
fn readOneNoEof(in: &io.FileInStream, comptime T: type, result: &T) !void {
    return readNoEof(in, T, result[0..1]);
}

related: #287

  1. One more thing, this looks like it's a pretty common thing to want to do. Let's modify std.io.InStream to support these APIs. I think that it's ok to break backwards compatibility of readNoEof and give it a comptime parameter of the type you want to read. Existing callsites can be updated to get the old behavior by passing in u8.

    And then we can add a safety check, if you pass a struct type to readNoEof, it will give you a compile error if your struct is not packed (because you would get an undefined value when deserializing a non-packed struct).

    I think this macho code should be passing around a &std.io.InStream instead of the &std.io.FileInStream directly. This is where that readNoEof API would go. I understand that it's a little weird because you'd have to use var for the parameter due to error sets. That's In/OutStream design flaw introduced with new error set changes. #764 and we don't have a good solution for that yet (other than "just use var").

  2. Finally, I think we should go back to (3) for now, because in order for (4) to make sense, we need the ability to specify endianness in packed structs. That's pending Add endianness as one of the pointer properties #649. Once packed structs support the concept of endianness, it makes sense to use them as types to serialization/deserialization functions.

Hope that helps!

My proposal is:

  • do (3) since it's easy
  • merge this PR since it's better than status quo
  • I'll bump the priority of Add endianness as one of the pointer properties #649
  • Once we get packed struct endianness working, we can add support for serializing/deserializing them using std.io.OutStream / std.io.InStream and then update this code to use std.io.InStream instead of std.io.FileInStream

@andrewrk
Copy link
Member

Exciting! I'm happy to merge this given that the code looks good and it passes all the tests, but I won't have access to my mac computer to try it out until Wednesday.

I'll give some style feedback as requested.

}
}

pub fn openSelfDebugInfo(allocator: &mem.Allocator) !&ElfStackTrace {
switch (builtin.object_format) {
builtin.ObjectFormat.elf => {
const st = try allocator.create(ElfStackTrace);
errdefer allocator.destroy(st);
Copy link
Member

Choose a reason for hiding this comment

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

good catch

}

fn readNoEof(in: &io.FileInStream, sink: var) !void {
if (@typeOf(sink) == []Nlist64) {
Copy link
Member

@andrewrk andrewrk Feb 19, 2018

Choose a reason for hiding this comment

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

I have a lot of responses to this, starting with minimal changes, ending with let's change the standard library.

  1. I'm actually fine with this, given that it's at least correct for this file, and it's a private function so it's not really exposed to much of anything.

  2. Here's how to do what you wanted to do:

fn readNoEof(in: &io.FileInStream, sink: var) !void {
    switch (@typeId(@typeOf(sink)) {
        builtin.TypeId.Slice => {
            const T = @typeOf(sink).Child;
            ...
        },
        builtin.TypeId.Pointer => {
            const T = @typeOf(sink).Child;
            ...
        },
        else => @compileError("expected slice or pointer"),
    }
}
  1. However, let's use this as an example to write idiomatic zig code. I think it would be better to separate this out into 2 functions:
fn readNoEof(in: &io.FileInStream, comptime T: type, result: []T) !void {
    return in.stream.readNoEof(([]u8)(result));
}
fn readOneNoEof(in: &io.FileInStream, comptime T: type, result: &T) !void {
    return readNoEof(in, T, result[0..1]);
}

related: #287

  1. One more thing, this looks like it's a pretty common thing to want to do. Let's modify std.io.InStream to support these APIs. I think that it's ok to break backwards compatibility of readNoEof and give it a comptime parameter of the type you want to read. Existing callsites can be updated to get the old behavior by passing in u8.

    And then we can add a safety check, if you pass a struct type to readNoEof, it will give you a compile error if your struct is not packed (because you would get an undefined value when deserializing a non-packed struct).

    I think this macho code should be passing around a &std.io.InStream instead of the &std.io.FileInStream directly. This is where that readNoEof API would go. I understand that it's a little weird because you'd have to use var for the parameter due to error sets. That's In/OutStream design flaw introduced with new error set changes. #764 and we don't have a good solution for that yet (other than "just use var").

  2. Finally, I think we should go back to (3) for now, because in order for (4) to make sense, we need the ability to specify endianness in packed structs. That's pending Add endianness as one of the pointer properties #649. Once packed structs support the concept of endianness, it makes sense to use them as types to serialization/deserialization functions.

Hope that helps!

My proposal is:

  • do (3) since it's easy
  • merge this PR since it's better than status quo
  • I'll bump the priority of Add endianness as one of the pointer properties #649
  • Once we get packed struct endianness working, we can add support for serializing/deserializing them using std.io.OutStream / std.io.InStream and then update this code to use std.io.InStream instead of std.io.FileInStream

@andrewrk
Copy link
Member

cc @jfo - you were looking into this too

@andrewrk andrewrk merged commit 2b35615 into ziglang:master Feb 21, 2018
@andrewrk
Copy link
Member

I played with this a bit on my mac. Cool - much better than no stack trace:

andy@andrews-air:~/Downloads/zig/build$ ./zig test test.zig
Test 1/1 aoeu...reached unreachable code
_panic.6: 0x0000000105b2ef34 in ??? (???)
_panic: 0x0000000105b2ed6b in ??? (???)
_aoeu: 0x0000000105b2ed08 in ??? (???)
_main.0: 0x0000000105b375ea in ??? (???)
_callMain: 0x0000000105b37379 in ??? (???)
_callMainWithArgs: 0x0000000105b372f9 in ??? (???)
_main: 0x0000000105b37184 in ??? (???)
???: 0x00007fff58f56115 in ??? (???)
???: 0x0000000000000001 in ??? (???)

@jfo
Copy link
Sponsor Contributor

jfo commented Feb 21, 2018

Awesome! I'd like to try and dig in more to help with improving this if I can find the time!

@andrewrk, is this eventually meant to be an error return trace? or does that trigger under slightly different circumstances?

@andrewrk
Copy link
Member

Both error return traces and stack traces are arrays of return addresses. We obtain an error return trace as an actual array of return addresses, and we find the stack trace in a "streaming" fashion by walking up the stack, looking at the return addresses along the way. In both cases we call printSourceAtAddress.

So improving printSourceAtAddress affects both error return traces and stack traces.

The above output was generated with:

test "aoeu" {
    unreachable;
}

To output an error return trace you can do:

test "aoeu" {
    return error.Foo;
}

Here's what the former looks like on linux:

[nix-shell:~/Downloads/zig/build]$ ./zig test test.zig 
Test 1/1 aoeu...reached unreachable code
/home/andy/Downloads/zig/build/lib/zig/std/debug/index.zig:120:26: 0x00000000002038a4 in ??? (test)
    dumpCurrentStackTrace();
                         ^
/home/andy/Downloads/zig/build/lib/zig/std/special/panic.zig:20:39: 0x00000000002036db in ??? (test)
            @import("std").debug.panic("{}", msg);
                                      ^
/home/andy/Downloads/zig/build/test.zig:2:5: 0x0000000000203678 in ??? (test)
    unreachable;
    ^
/home/andy/Downloads/zig/build/lib/zig/std/special/test_runner.zig:11:25: 0x000000000021d388 in ??? (test)
        try test_fn.func();
                        ^
/home/andy/Downloads/zig/build/lib/zig/std/special/bootstrap.zig:85:22: 0x000000000021d18b in ??? (test)
            root.main() catch |err| {
                     ^
/home/andy/Downloads/zig/build/lib/zig/std/special/bootstrap.zig:62:20: 0x000000000021d0eb in ??? (test)
    return callMain();
                   ^
/home/andy/Downloads/zig/build/lib/zig/std/special/bootstrap.zig:52:39: 0x000000000021cf7c in ??? (test)
    std.os.posix.exit(callMainWithArgs(argc, argv, envp));
                                      ^
/home/andy/Downloads/zig/build/lib/zig/std/special/bootstrap.zig:39:5: 0x000000000021cf00 in ??? (test)
    @noInlineCall(posixCallMainAndExit);
    ^

Here's what the latter looks like on linux:

[nix-shell:~/Downloads/zig/build]$ ./zig test test.zig 
Test 1/1 aoeu...error: Foo
/home/andy/Downloads/zig/build/test.zig:2:5: 0x0000000000203659 in ??? (test)
    return error.Foo;
    ^
/home/andy/Downloads/zig/build/lib/zig/std/special/test_runner.zig:11:9: 0x000000000021d2f2 in ??? (test)
        try test_fn.func();
        ^

Does that answer your question?

@jfo
Copy link
Sponsor Contributor

jfo commented Feb 21, 2018

Yes, it does.

As a related issue, I noticed this isn't context aware wrt terminal control codes, if I'm printing somewhere that doesn't respond to them or piping the output to a file.

image

image

This is a fantastic step though, thanks @bnoordhuis !

@andrewrk
Copy link
Member

Oops, good catch.

We even compute whether we should have tty colors and then ignore the parameter:

https://github.com/zig-lang/zig/blob/884b5fb4cfa81fba863f24cf5c6d9d7c2a21d11f/std/debug/index.zig#L145

On a related note - the way that terminal colors works on windows is different. I think we should have a tty colors abstraction in std.io that works for both windows and posix.

Here's the abstraction in zig compiler. It's a little clunky:
https://github.com/zig-lang/zig/blob/884b5fb4cfa81fba863f24cf5c6d9d7c2a21d11f/src/os.cpp#L973

@bnoordhuis
Copy link
Contributor Author

I think we should have a tty colors abstraction in std.io that works for both windows and posix.

FWIW, libuv has an ANSI emulator, which looks like it's the same principle albeit rather more elaborate:

https://github.com/libuv/libuv/blob/cef8a4624b907a126835ef122fd7fc1a5cdece65/src/win/tty.c#L1358-L1520

Might be a good project for someone looking to kill a few hours to port that over to zig.

@andrewrk
Copy link
Member

That's an interesting idea. So there are 2 approaches here:

  • libuv's idea: on windows parse the terminal codes and emit the correct library calls
  • zig compiler's idea: force the API of writing to the terminal to go through an abstraction that will either lower to API calls for windows, or escape codes for posix

One thing that I want to attempt to solve in the standard library, at some point, is the problem of not escaping terminal codes by default. It seems to me that if you do std.debug.warn and stderr is a tty, it should escape terminal codes by default on posix, and there should be an API to do the terminal colors and other stuff explicitly. std.io.OutStream could have a function ttyPrint which escapes everything but defines extra format specifiers such as {blue}aoeuaoeu{reset}.

I actually think that the windows API is better here, because you have to explicitly call functions to do terminal actions, so you don't accidentally let user input control your terminal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

stack trace implementation for MACH-O
3 participants