1const DebugSymbols = @This();
2
3const std = @import("std");
4const Io = std.Io;
5const assert = std.debug.assert;
6const fs = std.fs;
7const log = std.log.scoped(.link_dsym);
8const macho = std.macho;
9const makeStaticString = MachO.makeStaticString;
10const math = std.math;
11const mem = std.mem;
12const Writer = std.Io.Writer;
13const Allocator = std.mem.Allocator;
14
15const link = @import("../../link.zig");
16const MachO = @import("../MachO.zig");
17const StringTable = @import("../StringTable.zig");
18const Type = @import("../../Type.zig");
19const trace = @import("../../tracy.zig").trace;
20const load_commands = @import("load_commands.zig");
21const padToIdeal = MachO.padToIdeal;
22
23io: Io,
24allocator: Allocator,
25file: ?Io.File,
26
27symtab_cmd: macho.symtab_command = .{},
28uuid_cmd: macho.uuid_command = .{ .uuid = @splat(0) },
29
30segments: std.ArrayList(macho.segment_command_64) = .empty,
31sections: std.ArrayList(macho.section_64) = .empty,
32
33dwarf_segment_cmd_index: ?u8 = null,
34linkedit_segment_cmd_index: ?u8 = null,
35
36debug_info_section_index: ?u8 = null,
37debug_abbrev_section_index: ?u8 = null,
38debug_str_section_index: ?u8 = null,
39debug_aranges_section_index: ?u8 = null,
40debug_line_section_index: ?u8 = null,
41debug_line_str_section_index: ?u8 = null,
42debug_loclists_section_index: ?u8 = null,
43debug_rnglists_section_index: ?u8 = null,
44
45relocs: std.ArrayList(Reloc) = .empty,
46
47/// Output synthetic sections
48symtab: std.ArrayList(macho.nlist_64) = .empty,
49strtab: std.ArrayList(u8) = .empty,
50
51pub const Reloc = struct {
52 type: enum {
53 direct_load,
54 got_load,
55 },
56 target: u32,
57 offset: u64,
58 addend: u32,
59};
60
61/// You must call this function *after* `ZigObject.initMetadata()`
62/// has been called to get a viable debug symbols output.
63pub fn initMetadata(self: *DebugSymbols, macho_file: *MachO) !void {
64 try self.strtab.append(self.allocator, 0);
65
66 {
67 self.dwarf_segment_cmd_index = @as(u8, @intCast(self.segments.items.len));
68
69 const page_size = macho_file.getPageSize();
70 const off = @as(u64, @intCast(page_size));
71 const ideal_size: u16 = 200 + 128 + 160 + 250;
72 const needed_size = mem.alignForward(u64, padToIdeal(ideal_size), page_size);
73
74 log.debug("found __DWARF segment free space 0x{x} to 0x{x}", .{ off, off + needed_size });
75
76 try self.segments.append(self.allocator, .{
77 .segname = makeStaticString("__DWARF"),
78 .vmsize = needed_size,
79 .fileoff = off,
80 .filesize = needed_size,
81 .cmdsize = @sizeOf(macho.segment_command_64),
82 });
83 }
84
85 self.debug_str_section_index = try self.createSection("__debug_str", 0);
86 self.debug_info_section_index = try self.createSection("__debug_info", 0);
87 self.debug_abbrev_section_index = try self.createSection("__debug_abbrev", 0);
88 self.debug_aranges_section_index = try self.createSection("__debug_aranges", 4);
89 self.debug_line_section_index = try self.createSection("__debug_line", 0);
90 self.debug_line_str_section_index = try self.createSection("__debug_line_str", 0);
91 self.debug_loclists_section_index = try self.createSection("__debug_loclists", 0);
92 self.debug_rnglists_section_index = try self.createSection("__debug_rnglists", 0);
93
94 self.linkedit_segment_cmd_index = @intCast(self.segments.items.len);
95 try self.segments.append(self.allocator, .{
96 .segname = makeStaticString("__LINKEDIT"),
97 .maxprot = .{ .READ = true },
98 .initprot = .{ .READ = true },
99 .cmdsize = @sizeOf(macho.segment_command_64),
100 });
101}
102
103fn createSection(self: *DebugSymbols, sectname: []const u8, alignment: u16) !u8 {
104 const segment = self.getDwarfSegmentPtr();
105 var sect = macho.section_64{
106 .sectname = makeStaticString(sectname),
107 .segname = segment.segname,
108 .@"align" = alignment,
109 };
110
111 log.debug("create {s},{s} section", .{ sect.segName(), sect.sectName() });
112
113 const index: u8 = @intCast(self.sections.items.len);
114 try self.sections.append(self.allocator, sect);
115 segment.cmdsize += @sizeOf(macho.section_64);
116 segment.nsects += 1;
117
118 return index;
119}
120
121pub fn growSection(
122 self: *DebugSymbols,
123 sect_index: u8,
124 needed_size: u64,
125 requires_file_copy: bool,
126 macho_file: *MachO,
127) !void {
128 const io = self.io;
129 const sect = self.getSectionPtr(sect_index);
130
131 const allocated_size = self.allocatedSize(sect.offset);
132 if (needed_size > allocated_size) {
133 const existing_size = sect.size;
134 sect.size = 0; // free the space
135 const new_offset = try self.findFreeSpace(needed_size, 1);
136
137 log.debug("moving {s} section: {} bytes from 0x{x} to 0x{x}", .{
138 sect.sectName(), existing_size, sect.offset, new_offset,
139 });
140
141 if (requires_file_copy) {
142 const file = self.file.?;
143 try link.File.copyRangeAll2(io, file, file, sect.offset, new_offset, existing_size);
144 }
145
146 sect.offset = @intCast(new_offset);
147 } else if (sect.offset + allocated_size == std.math.maxInt(u64)) {
148 try self.file.?.setLength(io, sect.offset + needed_size);
149 }
150
151 sect.size = needed_size;
152 self.markDirty(sect_index, macho_file);
153}
154
155pub fn markDirty(self: *DebugSymbols, sect_index: u8, macho_file: *MachO) void {
156 if (macho_file.getZigObject()) |zo| {
157 if (self.debug_info_section_index.? == sect_index) {
158 zo.debug_info_header_dirty = true;
159 } else if (self.debug_line_section_index.? == sect_index) {
160 zo.debug_line_header_dirty = true;
161 } else if (self.debug_abbrev_section_index.? == sect_index) {
162 zo.debug_abbrev_dirty = true;
163 } else if (self.debug_str_section_index.? == sect_index) {
164 zo.debug_strtab_dirty = true;
165 } else if (self.debug_aranges_section_index.? == sect_index) {
166 zo.debug_aranges_dirty = true;
167 }
168 }
169}
170
171fn detectAllocCollision(self: *DebugSymbols, start: u64, size: u64) !?u64 {
172 const io = self.io;
173 var at_end = true;
174 const end = start + padToIdeal(size);
175
176 for (self.sections.items) |section| {
177 const increased_size = padToIdeal(section.size);
178 const test_end = section.offset + increased_size;
179 if (start < test_end) {
180 if (end > section.offset) return test_end;
181 if (test_end < std.math.maxInt(u64)) at_end = false;
182 }
183 }
184
185 if (at_end) try self.file.?.setLength(io, end);
186 return null;
187}
188
189fn findFreeSpace(self: *DebugSymbols, object_size: u64, min_alignment: u64) !u64 {
190 const segment = self.getDwarfSegmentPtr();
191 var offset: u64 = segment.fileoff;
192 while (try self.detectAllocCollision(offset, object_size)) |item_end| {
193 offset = mem.alignForward(u64, item_end, min_alignment);
194 }
195 return offset;
196}
197
198pub fn flush(self: *DebugSymbols, macho_file: *MachO) !void {
199 const io = self.io;
200 const zo = macho_file.getZigObject().?;
201 for (self.relocs.items) |*reloc| {
202 const sym = zo.symbols.items[reloc.target];
203 const sym_name = sym.getName(macho_file);
204 const addr = switch (reloc.type) {
205 .direct_load => sym.getAddress(.{}, macho_file),
206 .got_load => sym.getGotAddress(macho_file),
207 };
208 const sect = &self.sections.items[self.debug_info_section_index.?];
209 const file_offset = sect.offset + reloc.offset;
210 log.debug("resolving relocation: {d}@{x} ('{s}') at offset {x}", .{
211 reloc.target, addr, sym_name, file_offset,
212 });
213 try self.file.?.writePositionalAll(io, mem.asBytes(&addr), file_offset);
214 }
215
216 self.finalizeDwarfSegment(macho_file);
217 try self.writeLinkeditSegmentData(macho_file);
218
219 // Write load commands
220 const ncmds, const sizeofcmds = try self.writeLoadCommands(macho_file);
221 try self.writeHeader(macho_file, ncmds, sizeofcmds);
222}
223
224pub fn deinit(self: *DebugSymbols) void {
225 const gpa = self.allocator;
226 const io = self.io;
227 if (self.file) |file| file.close(io);
228 self.segments.deinit(gpa);
229 self.sections.deinit(gpa);
230 self.relocs.deinit(gpa);
231 self.symtab.deinit(gpa);
232 self.strtab.deinit(gpa);
233}
234
235pub fn swapRemoveRelocs(self: *DebugSymbols, target: u32) void {
236 // TODO re-implement using a hashmap with free lists
237 var last_index: usize = 0;
238 while (last_index < self.relocs.items.len) {
239 const reloc = self.relocs.items[last_index];
240 if (reloc.target == target) {
241 _ = self.relocs.swapRemove(last_index);
242 } else {
243 last_index += 1;
244 }
245 }
246}
247
248fn finalizeDwarfSegment(self: *DebugSymbols, macho_file: *MachO) void {
249 const base_vmaddr = blk: {
250 // Note that we purposely take the last VM address of the MachO binary including
251 // the binary's LINKEDIT segment. This is in contrast to how dsymutil does it
252 // which overwrites the the address space taken by the original MachO binary,
253 // however at the cost of having LINKEDIT preceed DWARF in dSYM binary which we
254 // do not want as we want to be able to incrementally move DWARF sections in the
255 // file as we please.
256 const last_seg = macho_file.getLinkeditSegment();
257 break :blk last_seg.vmaddr + last_seg.vmsize;
258 };
259 const dwarf_segment = self.getDwarfSegmentPtr();
260
261 var file_size: u64 = 0;
262 for (self.sections.items) |header| {
263 file_size = @max(file_size, header.offset + header.size);
264 }
265
266 const page_size = macho_file.getPageSize();
267 const aligned_size = mem.alignForward(u64, file_size, page_size);
268 dwarf_segment.vmaddr = base_vmaddr;
269 dwarf_segment.filesize = aligned_size;
270 dwarf_segment.vmsize = aligned_size;
271
272 const linkedit = self.getLinkeditSegmentPtr();
273 linkedit.vmaddr = mem.alignForward(
274 u64,
275 dwarf_segment.vmaddr + aligned_size,
276 page_size,
277 );
278 linkedit.fileoff = mem.alignForward(
279 u64,
280 dwarf_segment.fileoff + aligned_size,
281 page_size,
282 );
283 log.debug("found __LINKEDIT segment free space at 0x{x}", .{linkedit.fileoff});
284}
285
286fn writeLoadCommands(self: *DebugSymbols, macho_file: *MachO) !struct { usize, usize } {
287 const io = self.io;
288 const gpa = self.allocator;
289 const needed_size = load_commands.calcLoadCommandsSizeDsym(macho_file, self);
290 const buffer = try gpa.alloc(u8, needed_size);
291 defer gpa.free(buffer);
292
293 var writer: Writer = .fixed(buffer);
294
295 var ncmds: usize = 0;
296
297 // UUID comes first presumably to speed up lookup by the consumer like lldb.
298 @memcpy(&self.uuid_cmd.uuid, &macho_file.uuid_cmd.uuid);
299 try writer.writeStruct(self.uuid_cmd, .little);
300 ncmds += 1;
301
302 // Segment and section load commands
303 {
304 // Write segment/section headers from the binary file first.
305 const slice = macho_file.sections.slice();
306 var sect_id: usize = 0;
307 for (macho_file.segments.items, 0..) |seg, seg_id| {
308 if (seg_id == macho_file.linkedit_seg_index.?) break;
309 var out_seg = seg;
310 out_seg.fileoff = 0;
311 out_seg.filesize = 0;
312 try writer.writeStruct(out_seg, .little);
313 for (slice.items(.header)[sect_id..][0..seg.nsects]) |header| {
314 var out_header = header;
315 out_header.offset = 0;
316 try writer.writeStruct(out_header, .little);
317 }
318 sect_id += seg.nsects;
319 }
320 ncmds += macho_file.segments.items.len - 1;
321
322 // Next, commit DSYM's __LINKEDIT and __DWARF segments headers.
323 sect_id = 0;
324 for (self.segments.items) |seg| {
325 try writer.writeStruct(seg, .little);
326 for (self.sections.items[sect_id..][0..seg.nsects]) |header| {
327 try writer.writeStruct(header, .little);
328 }
329 sect_id += seg.nsects;
330 }
331 ncmds += self.segments.items.len;
332 }
333
334 try writer.writeStruct(self.symtab_cmd, .little);
335 ncmds += 1;
336
337 assert(writer.end == needed_size);
338
339 try self.file.?.writePositionalAll(io, buffer, @sizeOf(macho.mach_header_64));
340
341 return .{ ncmds, buffer.len };
342}
343
344fn writeHeader(self: *DebugSymbols, macho_file: *MachO, ncmds: usize, sizeofcmds: usize) !void {
345 const io = self.io;
346 var header: macho.mach_header_64 = .{};
347 header.filetype = macho.MH_DSYM;
348
349 switch (macho_file.getTarget().cpu.arch) {
350 .aarch64 => {
351 header.cputype = macho.CPU_TYPE_ARM64;
352 header.cpusubtype = macho.CPU_SUBTYPE_ARM_ALL;
353 },
354 .x86_64 => {
355 header.cputype = macho.CPU_TYPE_X86_64;
356 header.cpusubtype = macho.CPU_SUBTYPE_X86_64_ALL;
357 },
358 else => return error.UnsupportedCpuArchitecture,
359 }
360
361 header.ncmds = @intCast(ncmds);
362 header.sizeofcmds = @intCast(sizeofcmds);
363
364 log.debug("writing Mach-O header {}", .{header});
365
366 try self.file.?.writePositionalAll(io, mem.asBytes(&header), 0);
367}
368
369fn allocatedSize(self: *DebugSymbols, start: u64) u64 {
370 if (start == 0) return 0;
371 const seg = self.getDwarfSegmentPtr();
372 assert(start >= seg.fileoff);
373 var min_pos: u64 = std.math.maxInt(u64);
374 for (self.sections.items) |section| {
375 if (section.offset <= start) continue;
376 if (section.offset < min_pos) min_pos = section.offset;
377 }
378 return min_pos - start;
379}
380
381fn writeLinkeditSegmentData(self: *DebugSymbols, macho_file: *MachO) !void {
382 const tracy = trace(@src());
383 defer tracy.end();
384
385 const page_size = macho_file.getPageSize();
386 const seg = &self.segments.items[self.linkedit_segment_cmd_index.?];
387
388 var off = math.cast(u32, seg.fileoff) orelse return error.Overflow;
389 off = try self.writeSymtab(off, macho_file);
390 off = mem.alignForward(u32, off, @alignOf(u64));
391 off = try self.writeStrtab(off);
392 seg.filesize = off - seg.fileoff;
393
394 const aligned_size = mem.alignForward(u64, seg.filesize, page_size);
395 seg.vmsize = aligned_size;
396}
397
398pub fn writeSymtab(self: *DebugSymbols, off: u32, macho_file: *MachO) !u32 {
399 const tracy = trace(@src());
400 defer tracy.end();
401
402 const io = self.io;
403 const gpa = self.allocator;
404 const cmd = &self.symtab_cmd;
405 cmd.nsyms = macho_file.symtab_cmd.nsyms;
406 cmd.strsize = macho_file.symtab_cmd.strsize;
407 cmd.symoff = off;
408
409 try self.symtab.resize(gpa, cmd.nsyms);
410 try self.strtab.resize(gpa, cmd.strsize);
411 self.strtab.items[0] = 0;
412
413 if (macho_file.getZigObject()) |zo| {
414 zo.writeSymtab(macho_file, self);
415 }
416 for (macho_file.objects.items) |index| {
417 macho_file.getFile(index).?.writeSymtab(macho_file, self);
418 }
419 for (macho_file.dylibs.items) |index| {
420 macho_file.getFile(index).?.writeSymtab(macho_file, self);
421 }
422 if (macho_file.getInternalObject()) |internal| {
423 internal.writeSymtab(macho_file, self);
424 }
425
426 try self.file.?.writePositionalAll(io, @ptrCast(self.symtab.items), cmd.symoff);
427
428 return off + cmd.nsyms * @sizeOf(macho.nlist_64);
429}
430
431pub fn writeStrtab(self: *DebugSymbols, off: u32) !u32 {
432 const io = self.io;
433 const cmd = &self.symtab_cmd;
434 cmd.stroff = off;
435 try self.file.?.writePositionalAll(io, self.strtab.items, cmd.stroff);
436 return off + cmd.strsize;
437}
438
439pub fn getSectionIndexes(self: *DebugSymbols, segment_index: u8) struct { start: u8, end: u8 } {
440 var start: u8 = 0;
441 const nsects: u8 = for (self.segments.items, 0..) |seg, i| {
442 if (i == segment_index) break @intCast(seg.nsects);
443 start += @intCast(seg.nsects);
444 } else 0;
445 return .{ .start = start, .end = start + nsects };
446}
447
448fn getDwarfSegmentPtr(self: *DebugSymbols) *macho.segment_command_64 {
449 const index = self.dwarf_segment_cmd_index.?;
450 return &self.segments.items[index];
451}
452
453fn getLinkeditSegmentPtr(self: *DebugSymbols) *macho.segment_command_64 {
454 const index = self.linkedit_segment_cmd_index.?;
455 return &self.segments.items[index];
456}
457
458pub fn getSectionPtr(self: *DebugSymbols, sect: u8) *macho.section_64 {
459 assert(sect < self.sections.items.len);
460 return &self.sections.items[sect];
461}
462
463pub fn getSection(self: DebugSymbols, sect: u8) macho.section_64 {
464 assert(sect < self.sections.items.len);
465 return self.sections.items[sect];
466}