authorgravatar for info@bnoordhuis.nlBen Noordhuis <info@bnoordhuis.nl> 2018-02-19 23:06:54+01:00
committergravatar for info@bnoordhuis.nlBen Noordhuis <info@bnoordhuis.nl> 2018-02-19 23:11:11+01:00
logab48934e9cefb510d39ba3fe8c0dcf7619bec4cf
treeb02155986fb9d36b4b5e3ac94263d43c9421c13e
parentbde15cf0806f2b8d6fb0c90602b42a74863ec515

add support for stack traces on macosx

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.

4 files changed, 271 insertions(+), 57 deletions(-)

CMakeLists.txt+1
......@@ -386,6 +386,7 @@ set(ZIG_STD_FILES
386386 "index.zig"
387387 "io.zig"
388388 "linked_list.zig"
389 "macho.zig"
389390 "math/acos.zig"
390391 "math/acosh.zig"
391392 "math/asin.zig"
std/debug/index.zig+91-57
......@@ -5,6 +5,7 @@ const io = std.io;
55const os = std.os;
66const elf = std.elf;
77const DW = std.dwarf;
8const macho = std.macho;
89const ArrayList = std.ArrayList;
910const builtin = @import("builtin");
1011
......@@ -180,43 +181,57 @@ pub fn writeCurrentStackTrace(out_stream: var, allocator: &mem.Allocator,
180181}
181182
182183fn printSourceAtAddress(debug_info: &ElfStackTrace, out_stream: var, address: usize) !void {
183 if (builtin.os == builtin.Os.windows) {
184 return error.UnsupportedDebugInfo;
185 }
186184 // TODO we really should be able to convert @sizeOf(usize) * 2 to a string literal
187185 // at compile time. I'll call it issue #313
188186 const ptr_hex = if (@sizeOf(usize) == 4) "0x{x8}" else "0x{x16}";
189187
190 const compile_unit = findCompileUnit(debug_info, address) catch {
191 try out_stream.print("???:?:?: " ++ DIM ++ ptr_hex ++ " in ??? (???)" ++ RESET ++ "\n ???\n\n",
192 address);
193 return;
194 };
195 const compile_unit_name = try compile_unit.die.getAttrString(debug_info, DW.AT_name);
196 if (getLineNumberInfo(debug_info, compile_unit, address - 1)) |line_info| {
197 defer line_info.deinit();
198 try out_stream.print(WHITE ++ "{}:{}:{}" ++ RESET ++ ": " ++
199 DIM ++ ptr_hex ++ " in ??? ({})" ++ RESET ++ "\n",
200 line_info.file_name, line_info.line, line_info.column,
201 address, compile_unit_name);
202 if (printLineFromFile(debug_info.allocator(), out_stream, line_info)) {
203 if (line_info.column == 0) {
204 try out_stream.write("\n");
205 } else {
206 {var col_i: usize = 1; while (col_i < line_info.column) : (col_i += 1) {
207 try out_stream.writeByte(' ');
208 }}
209 try out_stream.write(GREEN ++ "^" ++ RESET ++ "\n");
188 switch (builtin.os) {
189 builtin.Os.windows => return error.UnsupportedDebugInfo,
190 builtin.Os.macosx => {
191 // TODO(bnoordhuis) It's theoretically possible to obtain the
192 // compilation unit from the symbtab but it's not that useful
193 // in practice because the compiler dumps everything in a single
194 // object file. Future improvement: use external dSYM data when
195 // available.
196 const unknown = macho.Symbol { .name = "???", .address = address };
197 const symbol = debug_info.symbol_table.search(address) ?? &unknown;
198 try out_stream.print(WHITE ++ "{}" ++ RESET ++ ": " ++
199 DIM ++ ptr_hex ++ " in ??? (???)" ++ RESET ++ "\n",
200 symbol.name, address);
201 },
202 else => {
203 const compile_unit = findCompileUnit(debug_info, address) catch {
204 try out_stream.print("???:?:?: " ++ DIM ++ ptr_hex ++ " in ??? (???)" ++ RESET ++ "\n ???\n\n",
205 address);
206 return;
207 };
208 const compile_unit_name = try compile_unit.die.getAttrString(debug_info, DW.AT_name);
209 if (getLineNumberInfo(debug_info, compile_unit, address - 1)) |line_info| {
210 defer line_info.deinit();
211 try out_stream.print(WHITE ++ "{}:{}:{}" ++ RESET ++ ": " ++
212 DIM ++ ptr_hex ++ " in ??? ({})" ++ RESET ++ "\n",
213 line_info.file_name, line_info.line, line_info.column,
214 address, compile_unit_name);
215 if (printLineFromFile(debug_info.allocator(), out_stream, line_info)) {
216 if (line_info.column == 0) {
217 try out_stream.write("\n");
218 } else {
219 {var col_i: usize = 1; while (col_i < line_info.column) : (col_i += 1) {
220 try out_stream.writeByte(' ');
221 }}
222 try out_stream.write(GREEN ++ "^" ++ RESET ++ "\n");
223 }
224 } else |err| switch (err) {
225 error.EndOfFile => {},
226 else => return err,
227 }
228 } else |err| switch (err) {
229 error.MissingDebugInfo, error.InvalidDebugInfo => {
230 try out_stream.print(ptr_hex ++ " in ??? ({})\n", address, compile_unit_name);
231 },
232 else => return err,
210233 }
211 } else |err| switch (err) {
212 error.EndOfFile => {},
213 else => return err,
214 }
215 } else |err| switch (err) {
216 error.MissingDebugInfo, error.InvalidDebugInfo => {
217 try out_stream.print(ptr_hex ++ " in ??? ({})\n", address, compile_unit_name);
218234 },
219 else => return err,
220235 }
221236}
222237
......@@ -249,12 +264,22 @@ pub fn openSelfDebugInfo(allocator: &mem.Allocator) !&ElfStackTrace {
249264 try scanAllCompileUnits(st);
250265 return st;
251266 },
267 builtin.ObjectFormat.macho => {
268 var exe_file = try os.openSelfExe();
269 defer exe_file.close();
270
271 const st = try allocator.create(ElfStackTrace);
272 errdefer allocator.destroy(st);
273
274 *st = ElfStackTrace {
275 .symbol_table = try macho.loadSymbols(allocator, &io.FileInStream.init(&exe_file)),
276 };
277
278 return st;
279 },
252280 builtin.ObjectFormat.coff => {
253281 return error.TodoSupportCoffDebugInfo;
254282 },
255 builtin.ObjectFormat.macho => {
256 return error.TodoSupportMachoDebugInfo;
257 },
258283 builtin.ObjectFormat.wasm => {
259284 return error.TodoSupportCOFFDebugInfo;
260285 },
......@@ -297,31 +322,40 @@ fn printLineFromFile(allocator: &mem.Allocator, out_stream: var, line_info: &con
297322 }
298323}
299324
300pub const ElfStackTrace = struct {
301 self_exe_file: os.File,
302 elf: elf.Elf,
303 debug_info: &elf.SectionHeader,
304 debug_abbrev: &elf.SectionHeader,
305 debug_str: &elf.SectionHeader,
306 debug_line: &elf.SectionHeader,
307 debug_ranges: ?&elf.SectionHeader,
308 abbrev_table_list: ArrayList(AbbrevTableHeader),
309 compile_unit_list: ArrayList(CompileUnit),
310
311 pub fn allocator(self: &const ElfStackTrace) &mem.Allocator {
312 return self.abbrev_table_list.allocator;
313 }
325pub const ElfStackTrace = switch (builtin.os) {
326 builtin.Os.macosx => struct {
327 symbol_table: macho.SymbolTable,
314328
315 pub fn readString(self: &ElfStackTrace) ![]u8 {
316 var in_file_stream = io.FileInStream.init(&self.self_exe_file);
317 const in_stream = &in_file_stream.stream;
318 return readStringRaw(self.allocator(), in_stream);
319 }
329 pub fn close(self: &ElfStackTrace) void {
330 self.symbol_table.deinit();
331 }
332 },
333 else => struct {
334 self_exe_file: os.File,
335 elf: elf.Elf,
336 debug_info: &elf.SectionHeader,
337 debug_abbrev: &elf.SectionHeader,
338 debug_str: &elf.SectionHeader,
339 debug_line: &elf.SectionHeader,
340 debug_ranges: ?&elf.SectionHeader,
341 abbrev_table_list: ArrayList(AbbrevTableHeader),
342 compile_unit_list: ArrayList(CompileUnit),
343
344 pub fn allocator(self: &const ElfStackTrace) &mem.Allocator {
345 return self.abbrev_table_list.allocator;
346 }
320347
321 pub fn close(self: &ElfStackTrace) void {
322 self.self_exe_file.close();
323 self.elf.close();
324 }
348 pub fn readString(self: &ElfStackTrace) ![]u8 {
349 var in_file_stream = io.FileInStream.init(&self.self_exe_file);
350 const in_stream = &in_file_stream.stream;
351 return readStringRaw(self.allocator(), in_stream);
352 }
353
354 pub fn close(self: &ElfStackTrace) void {
355 self.self_exe_file.close();
356 self.elf.close();
357 }
358 },
325359};
326360
327361const PcRange = struct {
std/index.zig+2
......@@ -21,6 +21,7 @@ pub const endian = @import("endian.zig");
2121pub const fmt = @import("fmt/index.zig");
2222pub const heap = @import("heap.zig");
2323pub const io = @import("io.zig");
24pub const macho = @import("macho.zig");
2425pub const math = @import("math/index.zig");
2526pub const mem = @import("mem.zig");
2627pub const net = @import("net.zig");
......@@ -51,6 +52,7 @@ test "std" {
5152 _ = @import("endian.zig");
5253 _ = @import("fmt/index.zig");
5354 _ = @import("io.zig");
55 _ = @import("macho.zig");
5456 _ = @import("math/index.zig");
5557 _ = @import("mem.zig");
5658 _ = @import("heap.zig");
std/macho.zig created+177
......@@ -0,0 +1,177 @@
1const builtin = @import("builtin");
2const std = @import("index.zig");
3const io = std.io;
4const mem = std.mem;
5
6const MH_MAGIC_64 = 0xFEEDFACF;
7const MH_PIE = 0x200000;
8const LC_SYMTAB = 2;
9
10const MachHeader64 = packed struct {
11 magic: u32,
12 cputype: u32,
13 cpusubtype: u32,
14 filetype: u32,
15 ncmds: u32,
16 sizeofcmds: u32,
17 flags: u32,
18 reserved: u32,
19};
20
21const LoadCommand = packed struct {
22 cmd: u32,
23 cmdsize: u32,
24};
25
26const SymtabCommand = packed struct {
27 symoff: u32,
28 nsyms: u32,
29 stroff: u32,
30 strsize: u32,
31};
32
33const Nlist64 = packed struct {
34 n_strx: u32,
35 n_type: u8,
36 n_sect: u8,
37 n_desc: u16,
38 n_value: u64,
39};
40
41pub const Symbol = struct {
42 name: []const u8,
43 address: u64,
44
45 fn addressLessThan(lhs: &const Symbol, rhs: &const Symbol) bool {
46 return lhs.address < rhs.address;
47 }
48};
49
50pub const SymbolTable = struct {
51 allocator: &mem.Allocator,
52 symbols: []const Symbol,
53 strings: []const u8,
54
55 // Doubles as an eyecatcher to calculate the PIE slide, see loadSymbols().
56 // Ideally we'd use _mh_execute_header because it's always at 0x100000000
57 // in the image but as it's located in a different section than executable
58 // code, its displacement is different.
59 pub fn deinit(self: &SymbolTable) void {
60 self.allocator.free(self.symbols);
61 self.symbols = []const Symbol {};
62
63 self.allocator.free(self.strings);
64 self.strings = []const u8 {};
65 }
66
67 pub fn search(self: &const SymbolTable, address: usize) ?&const Symbol {
68 var min: usize = 0;
69 var max: usize = self.symbols.len - 1; // Exclude sentinel.
70 while (min < max) {
71 const mid = min + (max - min) / 2;
72 const curr = &self.symbols[mid];
73 const next = &self.symbols[mid + 1];
74 if (address >= next.address) {
75 min = mid + 1;
76 } else if (address < curr.address) {
77 max = mid;
78 } else {
79 return curr;
80 }
81 }
82 return null;
83 }
84};
85
86pub fn loadSymbols(allocator: &mem.Allocator, in: &io.FileInStream) !SymbolTable {
87 var file = in.file;
88 try file.seekTo(0);
89
90 var hdr: MachHeader64 = undefined;
91 try readNoEof(in, &hdr);
92 if (hdr.magic != MH_MAGIC_64) return error.MissingDebugInfo;
93 const is_pie = MH_PIE == (hdr.flags & MH_PIE);
94
95 var pos: usize = @sizeOf(@typeOf(hdr));
96 var ncmd: u32 = hdr.ncmds;
97 while (ncmd != 0) : (ncmd -= 1) {
98 try file.seekTo(pos);
99 var lc: LoadCommand = undefined;
100 try readNoEof(in, &lc);
101 if (lc.cmd == LC_SYMTAB) break;
102 pos += lc.cmdsize;
103 } else {
104 return error.MissingDebugInfo;
105 }
106
107 var cmd: SymtabCommand = undefined;
108 try readNoEof(in, &cmd);
109
110 try file.seekTo(cmd.symoff);
111 var syms = try allocator.alloc(Nlist64, cmd.nsyms);
112 defer allocator.free(syms);
113 try readNoEof(in, syms);
114
115 try file.seekTo(cmd.stroff);
116 var strings = try allocator.alloc(u8, cmd.strsize);
117 errdefer allocator.free(strings);
118 try in.stream.readNoEof(strings);
119
120 var nsyms: usize = 0;
121 for (syms) |sym| if (isSymbol(sym)) nsyms += 1;
122 if (nsyms == 0) return error.MissingDebugInfo;
123
124 var symbols = try allocator.alloc(Symbol, nsyms + 1); // Room for sentinel.
125 errdefer allocator.free(symbols);
126
127 var pie_slide: usize = 0;
128 var nsym: usize = 0;
129 for (syms) |sym| {
130 if (!isSymbol(sym)) continue;
131 const start = sym.n_strx;
132 const end = ??mem.indexOfScalarPos(u8, strings, start, 0);
133 const name = strings[start..end];
134 const address = sym.n_value;
135 symbols[nsym] = Symbol { .name = name, .address = address };
136 nsym += 1;
137 if (is_pie and mem.eql(u8, name, "_SymbolTable_deinit")) {
138 pie_slide = @ptrToInt(SymbolTable.deinit) - address;
139 }
140 }
141
142 // Effectively a no-op, lld emits symbols in ascending order.
143 std.sort.insertionSort(Symbol, symbols[0..nsyms], Symbol.addressLessThan);
144
145 // Insert the sentinel. Since we don't know where the last function ends,
146 // we arbitrarily limit it to the start address + 4 KB.
147 const top = symbols[nsyms - 1].address + 4096;
148 symbols[nsyms] = Symbol { .name = "", .address = top };
149
150 if (pie_slide != 0) {
151 for (symbols) |*symbol| symbol.address += pie_slide;
152 }
153
154 return SymbolTable {
155 .allocator = allocator,
156 .symbols = symbols,
157 .strings = strings,
158 };
159}
160
161fn readNoEof(in: &io.FileInStream, sink: var) !void {
162 if (@typeOf(sink) == []Nlist64) {
163 const T = @typeOf(sink[0]);
164 const len = @sizeOf(T) * sink.len;
165 const bytes = @ptrCast(&u8, &sink[0]);
166 return in.stream.readNoEof(bytes[0..len]);
167 } else {
168 const T = @typeOf(*sink);
169 const len = @sizeOf(T);
170 const bytes = @ptrCast(&u8, sink);
171 return in.stream.readNoEof(bytes[0..len]);
172 }
173}
174
175fn isSymbol(sym: &const Nlist64) bool {
176 return sym.n_value != 0 and sym.n_desc == 0;
177}