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 }