lib/memtrace/src/stack/symbolize.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const builtin = @import("builtin");
3 const sys = @import("sys");
4
5 const Allocator = std.mem.Allocator;
6
7 pub const Frame = struct {
8 function: []const u8,
9 location: []const u8,
10 };
11
12 const Range = struct {
13 start: usize,
14 len: usize,
15 };
16
17 pub const Symbols = struct {
18 stdout: []u8,
19 frames: std.ArrayListUnmanaged(Frame),
20 ranges: std.AutoHashMapUnmanaged(u64, Range),
21
22 pub fn deinit(self: *Symbols, allocator: Allocator) void {
23 self.ranges.deinit(allocator);
24 self.frames.deinit(allocator);
25 allocator.free(self.stdout);
26 self.* = undefined;
27 }
28
29 pub fn find(self: *const Symbols, address: u64) []const Frame {
30 const range = self.ranges.get(address) orelse return &.{};
31 return self.frames.items[range.start..][0..range.len];
32 }
33 };
34
35 pub fn resolveAlloc(
36 allocator: Allocator,
37 binary_path: []const u8,
38 addresses: []const u64,
39 ) !Symbols {
40 if (addresses.len == 0) return error.NoStackAddresses;
41 if (comptime builtin.target.ofmt == .elf) {
42 return resolveElfAlloc(
43 allocator,
44 binary_path,
45 addresses,
46 ) catch |err| {
47 if (err == error.InvalidElfMagic) {
48 return resolveExternalAlloc(
49 allocator,
50 binary_path,
51 addresses,
52 );
53 }
54 return err;
55 };
56 }
57 return resolveExternalAlloc(allocator, binary_path, addresses);
58 }
59
60 fn resolveElfAlloc(
61 allocator: Allocator,
62 binary_path: []const u8,
63 addresses: []const u64,
64 ) !Symbols {
65 const io = sys.fs.debugIo();
66 const file = if (std.fs.path.isAbsolute(binary_path))
67 try sys.fs.openAbsoluteFile(binary_path, .{})
68 else
69 try sys.fs.cwd().openFile(io, binary_path, .{});
70 defer file.close(io);
71 var elf_file = try std.debug.ElfFile.load(
72 allocator,
73 io,
74 file,
75 null,
76 &.none,
77 );
78 defer elf_file.deinit(allocator);
79 const dwarf = if (elf_file.dwarf) |*value|
80 value
81 else
82 return error.MissingDebugInfo;
83 try dwarf.open(allocator, elf_file.endian);
84
85 var output = std.Io.Writer.Allocating.init(allocator);
86 defer output.deinit();
87 var text_arena = std.heap.ArenaAllocator.init(allocator);
88 defer text_arena.deinit();
89 for (addresses) |address| {
90 _ = text_arena.reset(.retain_capacity);
91 const source_location = source_location: {
92 const compile_unit = dwarf.findCompileUnit(
93 elf_file.endian,
94 address,
95 ) catch break :source_location null;
96 break :source_location dwarf.getLineNumberInfo(
97 allocator,
98 text_arena.allocator(),
99 elf_file.endian,
100 compile_unit,
101 address,
102 ) catch |err| {
103 if (err == error.OutOfMemory) return error.OutOfMemory;
104 break :source_location null;
105 };
106 };
107 try writeElfSymbol(
108 &output.writer,
109 address,
110 dwarf.getSymbolName(address),
111 source_location,
112 );
113 }
114
115 const stdout = try output.toOwnedSlice();
116 return try symbolsFromOutput(allocator, stdout, addresses);
117 }
118
119 fn writeElfSymbol(
120 writer: *std.Io.Writer,
121 address: u64,
122 name: ?[]const u8,
123 source_location: ?std.debug.SourceLocation,
124 ) !void {
125 try writer.print("0x{x}: {s}", .{ address, name orelse "??" });
126 if (source_location) |location| {
127 try writer.print(
128 " at {s}:{d}:{d}\n",
129 .{
130 location.file_name,
131 location.line,
132 location.column,
133 },
134 );
135 } else {
136 try writer.writeAll(" at ??:0\n");
137 }
138 }
139
140 fn resolveExternalAlloc(
141 allocator: Allocator,
142 binary_path: []const u8,
143 addresses: []const u64,
144 ) !Symbols {
145 const fixed_count = 4;
146 const argv = try allocator.alloc([]const u8, fixed_count + addresses.len);
147 defer allocator.free(argv);
148 const address_text = try allocator.alloc([]const u8, addresses.len);
149 var address_count: usize = 0;
150 defer {
151 for (address_text[0..address_count]) |text| allocator.free(text);
152 allocator.free(address_text);
153 }
154 argv[0] = "addr2line";
155 argv[1] = "-aCifp";
156 argv[2] = "-e";
157 argv[3] = binary_path;
158 for (addresses, 0..) |address, index| {
159 const text = try std.fmt.allocPrint(allocator, "0x{x}", .{address});
160 address_text[index] = text;
161 address_count += 1;
162 argv[fixed_count + index] = text;
163 }
164 var io_state = sys.thread.initThreadedIo(allocator, .{});
165 defer io_state.deinit();
166 const result = try sys.process.run(allocator, io_state.io(), .{
167 .argv = argv,
168 .stdout_limit = .limited(32 * 1024 * 1024),
169 .stderr_limit = .limited(1024 * 1024),
170 });
171 defer allocator.free(result.stderr);
172 if (std.mem.trim(u8, result.stdout, " \t\r\n").len == 0) {
173 allocator.free(result.stdout);
174 return error.SymbolizationFailed;
175 }
176 return try symbolsFromOutput(allocator, result.stdout, addresses);
177 }
178
179 fn symbolsFromOutput(
180 allocator: Allocator,
181 stdout: []u8,
182 addresses: []const u64,
183 ) !Symbols {
184 var symbols = Symbols{
185 .stdout = stdout,
186 .frames = .empty,
187 .ranges = .{},
188 };
189 errdefer symbols.deinit(allocator);
190 try parseOutput(allocator, &symbols);
191 for (addresses) |address| {
192 if (!symbols.ranges.contains(address)) return error.SymbolizationFailed;
193 }
194 return symbols;
195 }
196
197 fn parseOutput(allocator: Allocator, symbols: *Symbols) !void {
198 var current_address: ?u64 = null;
199 var current_start: usize = 0;
200 var lines = std.mem.splitScalar(u8, symbols.stdout, '\n');
201 while (lines.next()) |raw_line| {
202 const line = std.mem.trim(u8, raw_line, " \t\r\n");
203 if (line.len == 0) continue;
204 if (parseAddressLine(line)) |address_line| {
205 if (current_address) |address| {
206 try finishAddress(allocator, symbols, address, current_start);
207 }
208 current_address = address_line.address;
209 current_start = symbols.frames.items.len;
210 try symbols.frames.append(
211 allocator,
212 parseFrame(address_line.text),
213 );
214 continue;
215 }
216 if (current_address == null) return error.InvalidSymbolizerOutput;
217 const inline_prefix = "(inlined by) ";
218 if (!std.mem.startsWith(u8, line, inline_prefix)) {
219 return error.InvalidSymbolizerOutput;
220 }
221 try symbols.frames.append(
222 allocator,
223 parseFrame(line[inline_prefix.len..]),
224 );
225 }
226 if (current_address) |address| {
227 try finishAddress(allocator, symbols, address, current_start);
228 }
229 }
230
231 const AddressLine = struct {
232 address: u64,
233 text: []const u8,
234 };
235
236 fn parseAddressLine(line: []const u8) ?AddressLine {
237 if (!std.mem.startsWith(u8, line, "0x")) return null;
238 const separator = std.mem.indexOf(u8, line, ": ") orelse return null;
239 const address = std.fmt.parseUnsigned(
240 u64,
241 line[2..separator],
242 16,
243 ) catch return null;
244 return .{
245 .address = address,
246 .text = line[separator + 2 ..],
247 };
248 }
249
250 fn parseFrame(text: []const u8) Frame {
251 if (std.mem.lastIndexOf(u8, text, " at ")) |separator| {
252 return .{
253 .function = text[0..separator],
254 .location = text[separator + 4 ..],
255 };
256 }
257 return .{ .function = text, .location = "" };
258 }
259
260 fn finishAddress(
261 allocator: Allocator,
262 symbols: *Symbols,
263 address: u64,
264 start: usize,
265 ) !void {
266 if (symbols.frames.items.len == start) return error.InvalidSymbolizerOutput;
267 try symbols.ranges.putNoClobber(allocator, address, .{
268 .start = start,
269 .len = symbols.frames.items.len - start,
270 });
271 }
272
273 test "ELF resolver tears down caller-owned allocations after failure" {
274 if (comptime builtin.os.tag != .linux or
275 builtin.target.ofmt != .elf)
276 {
277 return error.SkipZigTest;
278 }
279 var failing = std.testing.FailingAllocator.init(
280 std.testing.allocator,
281 .{ .fail_index = 1 },
282 );
283 try std.testing.expectError(
284 error.OutOfMemory,
285 resolveElfAlloc(
286 failing.allocator(),
287 "/proc/self/exe",
288 &.{1},
289 ),
290 );
291 try std.testing.expect(failing.has_induced_failure);
292 try std.testing.expectEqual(
293 failing.allocated_bytes,
294 failing.freed_bytes,
295 );
296 }
297
298 test "addr2line parser keeps inline frames under one address" {
299 const text =
300 "0x0000000000000010: leaf at leaf.zig:1\n" ++
301 " (inlined by) caller at caller.zig:2\n" ++
302 "0x0000000000000020: root at root.zig:3\n";
303 const owned = try std.testing.allocator.dupe(u8, text);
304 var symbols = Symbols{
305 .stdout = owned,
306 .frames = .empty,
307 .ranges = .{},
308 };
309 defer symbols.deinit(std.testing.allocator);
310 try parseOutput(std.testing.allocator, &symbols);
311 try std.testing.expectEqual(@as(usize, 2), symbols.find(0x10).len);
312 try std.testing.expectEqualStrings(
313 "caller",
314 symbols.find(0x10)[1].function,
315 );
316 try std.testing.expectEqual(@as(usize, 1), symbols.find(0x20).len);
317 }