lib/choir/src/backends/elf.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const alloc_phase = @import("alloc_phase");
   3 const sys = @import("sys");
   4 const artifact = @import("root.zig").artifact;
   5 const machine = @import("machine.zig");
   6 
   7 const Allocator = std.mem.Allocator;
   8 
   9 const ehdr_size = 64;
  10 const shdr_size = 64;
  11 const sym_size = 24;
  12 const rela_size = 24;
  13 
  14 pub const ObjectError = Allocator.Error || ValidationError;
  15 
  16 const ValidationError = error{
  17     DuplicateSymbol,
  18     InvalidAlignment,
  19     InvalidDataSymbol,
  20     InvalidTextSymbol,
  21     InvalidRelocationOffset,
  22     MissingRelocationSymbol,
  23     UnsupportedArchitecture,
  24     UnsupportedRelocation,
  25     ObjectSizeOverflow,
  26 };
  27 
  28 /// One slot to patch. `section` names the section holding the slot, and only a section the
  29 /// file carries bytes for may hold one: `.text`, `.rodata`, or `.data`. A `.bss` slot is
  30 /// refused because there are no bytes in the file to write an address into.
  31 pub const Relocation = struct {
  32     section: []const u8 = ".text",
  33     offset: u64,
  34     symbol: []const u8,
  35     kind: artifact.RelocationKind,
  36     addend: i64 = 0,
  37     width_bits: u16 = 64,
  38 };
  39 
  40 pub const TextSymbol = struct {
  41     name: []const u8,
  42     offset: u64,
  43     size: u64,
  44 };
  45 
  46 pub const RelocatableObject = struct {
  47     architecture: artifact.Architecture = .x86_64,
  48     entry_symbol: []const u8,
  49     text: []const u8,
  50     text_alignment: usize = 16,
  51     text_symbols: []const TextSymbol = &.{},
  52     data_symbols: []const machine.DataSymbol = &.{},
  53     relocations: []const Relocation = &.{},
  54     executable_stack: bool = false,
  55 };
  56 
  57 pub const X86_64MachineCodeObject = struct {
  58     entry_symbol: []const u8,
  59     code: []const u8,
  60     text_symbols: []const TextSymbol = &.{},
  61     relocations: []const machine.CallRelocation = &.{},
  62     data_relocations: []const machine.DataRelocation = &.{},
  63     data_symbols: []const machine.DataSymbol = &.{},
  64     executable_stack: bool = false,
  65 };
  66 
  67 pub fn buildX86_64MachineCodeObject(
  68     allocator: Allocator,
  69     input: X86_64MachineCodeObject,
  70 ) ObjectError![]u8 {
  71     const object_relocations = try allocator.alloc(Relocation, input.relocations.len + input.data_relocations.len);
  72     defer allocator.free(object_relocations);
  73 
  74     for (input.relocations, 0..) |relocation, index| {
  75         object_relocations[index] = .{
  76             .offset = relocation.offset,
  77             .symbol = relocation.target,
  78             .kind = .call,
  79             .width_bits = 64,
  80         };
  81     }
  82     for (input.data_relocations, 0..) |relocation, index| {
  83         object_relocations[input.relocations.len + index] = .{
  84             .offset = relocation.offset,
  85             .symbol = relocation.target,
  86             .kind = .absolute,
  87             .addend = relocation.addend,
  88             .width_bits = relocation.width_bits,
  89         };
  90     }
  91 
  92     return buildRelocatableObject(allocator, .{
  93         .architecture = .x86_64,
  94         .entry_symbol = input.entry_symbol,
  95         .text = input.code,
  96         .text_symbols = input.text_symbols,
  97         .data_symbols = input.data_symbols,
  98         .relocations = object_relocations,
  99         .executable_stack = input.executable_stack,
 100     });
 101 }
 102 
 103 pub fn buildRelocatableObject(
 104     allocator: Allocator,
 105     input: RelocatableObject,
 106 ) ObjectError![]u8 {
 107     if (input.architecture != .x86_64) return error.UnsupportedArchitecture;
 108     try validateAlignment(input.text_alignment);
 109     const default_symbols = [_]TextSymbol{.{
 110         .name = input.entry_symbol,
 111         .offset = 0,
 112         .size = input.text.len,
 113     }};
 114     const text_symbols = if (input.text_symbols.len == 0) &default_symbols else input.text_symbols;
 115     for (text_symbols) |symbol| try validateTextSymbol(input.text.len, symbol);
 116 
 117     var rodata = try DataSectionLayout.build(allocator, input.data_symbols, .rodata);
 118     defer rodata.deinit(allocator);
 119     var data = try DataSectionLayout.build(allocator, input.data_symbols, .data);
 120     defer data.deinit(allocator);
 121     var bss = try DataSectionLayout.build(allocator, input.data_symbols, .bss);
 122     defer bss.deinit(allocator);
 123 
 124     var sections = Sections.init(input, rodata, data, bss);
 125     var symbols = try Symbols.init(allocator);
 126     defer symbols.deinit(allocator);
 127     try symbols.collect(allocator, sections, .{
 128         .rodata = rodata.symbols,
 129         .data = data.symbols,
 130         .bss = bss.symbols,
 131     }, text_symbols, input.relocations);
 132 
 133     const sizes: TargetSizes = .{ input.text.len, rodata.size, data.size };
 134     var groups = try collectRelocations(allocator, input, sizes, &symbols.indices);
 135     defer groups.deinit(allocator);
 136 
 137     const layout = try sections.place(&symbols, groups);
 138     const buffer = try allocator.alloc(u8, layout.size);
 139     errdefer allocator.free(buffer);
 140     @memset(buffer, 0);
 141 
 142     const placed = [_]PlacedBytes{
 143         .{ .section = sections.text, .target = .text, .bytes = input.text },
 144         .{ .section = sections.rodata, .target = .rodata, .bytes = rodata.bytes },
 145         .{ .section = sections.data, .target = .data, .bytes = data.bytes },
 146     };
 147     for (placed) |entry| {
 148         if (entry.section == 0) continue;
 149         const offset = sections.headers[entry.section].offset;
 150         copyInto(buffer, offset, entry.bytes);
 151         scrubRelocationSlots(buffer[offset..][0..entry.bytes.len], input.relocations, entry.target);
 152     }
 153     for (relocation_targets) |target| {
 154         const section = sections.rela[@backingInt(target)];
 155         if (section == 0) continue;
 156         writeRelaRecords(buffer, sections.headers[section].offset, groups.slice(target));
 157     }
 158     writeSymbolRecords(buffer, sections.headers[sections.symbols].offset, symbols.records.items);
 159     copyInto(buffer, sections.headers[sections.strings].offset, symbols.strings.bytes());
 160     copyInto(buffer, sections.headers[sections.names].offset, Sections.names_text);
 161     writeElfHeader(buffer, .{
 162         .section_header_offset = layout.headers,
 163         .section_count = sections.count,
 164         .section_string_table_index = sections.names,
 165     });
 166     sections.write(buffer, layout.headers);
 167     std.debug.assert(buffer.len >= input.text.len);
 168     return buffer;
 169 }
 170 
 171 const Sections = struct {
 172     headers: [12]SectionHeader = @splat(.null_header),
 173     count: u16 = 1,
 174     text: u16 = 0,
 175     rodata: u16 = 0,
 176     data: u16 = 0,
 177     bss: u16 = 0,
 178     /// One relocation section per target that has relocations, indexed by `RelocationTarget`.
 179     rela: [relocation_target_count]u16 = @splat(0),
 180     symbols: u16 = 0,
 181     strings: u16 = 0,
 182     names: u16 = 0,
 183 
 184     /// A name is found by its first occurrence, so each plain name is spelled before the
 185     /// `.rela.` section that quotes it.
 186     const names_text = "\x00.text\x00.rodata\x00.data\x00.bss\x00.rela.text\x00" ++
 187         ".rela.rodata\x00.rela.data\x00.symtab\x00.strtab\x00.shstrtab\x00" ++
 188         ".note.GNU-stack\x00";
 189     const Layout = struct { headers: usize, size: usize };
 190 
 191     fn init(
 192         input: RelocatableObject,
 193         rodata: DataSectionLayout,
 194         data: DataSectionLayout,
 195         bss: DataSectionLayout,
 196     ) Sections {
 197         var self: Sections = .{};
 198         self.text = self.add(".text", .{
 199             .section_type = std.elf.SHT_PROGBITS,
 200             .flags = std.elf.SHF_ALLOC | std.elf.SHF_EXECINSTR,
 201             .size = input.text.len,
 202             .alignment = input.text_alignment,
 203         });
 204         if (rodata.size != 0) self.rodata = self.add(".rodata", .{
 205             .section_type = std.elf.SHT_PROGBITS,
 206             .flags = std.elf.SHF_ALLOC,
 207             .size = rodata.size,
 208             .alignment = rodata.alignment,
 209         });
 210         if (data.size != 0) self.data = self.add(".data", .{
 211             .section_type = std.elf.SHT_PROGBITS,
 212             .flags = std.elf.SHF_ALLOC | std.elf.SHF_WRITE,
 213             .size = data.size,
 214             .alignment = data.alignment,
 215         });
 216         if (bss.size != 0) self.bss = self.add(".bss", .{
 217             .section_type = std.elf.SHT_NOBITS,
 218             .flags = std.elf.SHF_ALLOC | std.elf.SHF_WRITE,
 219             .size = bss.size,
 220             .alignment = bss.alignment,
 221         });
 222         self.addRelocationSections(input.relocations);
 223         self.symbols = self.add(".symtab", .{
 224             .section_type = std.elf.SHT_SYMTAB,
 225             .alignment = 8,
 226             .entry_size = sym_size,
 227         });
 228         self.strings = self.add(".strtab", .{ .section_type = std.elf.SHT_STRTAB, .alignment = 1 });
 229         self.names = self.add(".shstrtab", .{
 230             .section_type = std.elf.SHT_STRTAB,
 231             .size = names_text.len,
 232             .alignment = 1,
 233         });
 234         _ = self.add(".note.GNU-stack", .{
 235             .section_type = std.elf.SHT_PROGBITS,
 236             .flags = if (input.executable_stack) std.elf.SHF_EXECINSTR else 0,
 237             .alignment = 1,
 238         });
 239         self.headers[self.symbols].link = self.strings;
 240         for (self.rela) |section| {
 241             if (section != 0) self.headers[section].link = self.symbols;
 242         }
 243         std.debug.assert(self.count <= self.headers.len);
 244         return self;
 245     }
 246 
 247     /// Adds one `SHT_RELA` section per target that has relocations. A target with none gets no
 248     /// section, which is what keeps an object holding only `.text` slots identical to what this
 249     /// writer produced before data sections existed.
 250     fn addRelocationSections(self: *Sections, relocations: []const Relocation) void {
 251         if (relocationCount(relocations, .text) != 0) {
 252             self.rela[@backingInt(RelocationTarget.text)] =
 253                 self.add(".rela.text", relaHeader(self.text));
 254         }
 255         if (self.rodata != 0 and relocationCount(relocations, .rodata) != 0) {
 256             self.rela[@backingInt(RelocationTarget.rodata)] =
 257                 self.add(".rela.rodata", relaHeader(self.rodata));
 258         }
 259         if (self.data != 0 and relocationCount(relocations, .data) != 0) {
 260             self.rela[@backingInt(RelocationTarget.data)] =
 261                 self.add(".rela.data", relaHeader(self.data));
 262         }
 263     }
 264 
 265     fn relaHeader(target_section: u16) SectionHeader {
 266         return .{
 267             .section_type = std.elf.SHT_RELA,
 268             .info = target_section,
 269             .alignment = 8,
 270             .entry_size = rela_size,
 271         };
 272     }
 273 
 274     fn add(self: *Sections, comptime name: []const u8, header: SectionHeader) u16 {
 275         std.debug.assert(self.count < self.headers.len);
 276         const index = self.count;
 277         self.headers[index] = header;
 278         self.headers[index].name = comptime std.mem.indexOf(u8, names_text, name).?;
 279         self.count += 1;
 280         return index;
 281     }
 282 
 283     /// Assigns every section its file offset. A `SHT_NOBITS` section takes an offset and no
 284     /// bytes, because its size is what the loader zeroes rather than what the file carries.
 285     fn place(
 286         self: *Sections,
 287         symbols: *const Symbols,
 288         groups: RelocationGroups,
 289     ) ObjectError!Layout {
 290         self.headers[self.symbols].size = std.math.mul(usize, symbols.records.items.len, sym_size) catch {
 291             return error.ObjectSizeOverflow;
 292         };
 293         self.headers[self.symbols].info = symbols.first_global;
 294         self.headers[self.strings].size = symbols.strings.bytes().len;
 295         for (self.rela, groups.counts) |section, count| {
 296             if (section == 0) {
 297                 std.debug.assert(count == 0);
 298                 continue;
 299             }
 300             self.headers[section].size = std.math.mul(usize, count, rela_size) catch {
 301                 return error.ObjectSizeOverflow;
 302             };
 303         }
 304         var offset: usize = ehdr_size;
 305         for (self.headers[1..self.count]) |*header| {
 306             const mask = header.alignment - 1;
 307             const aligned = std.math.add(usize, offset, mask) catch return error.ObjectSizeOverflow;
 308             offset = aligned & ~mask;
 309             header.offset = offset;
 310             if (header.section_type == std.elf.SHT_NOBITS) continue;
 311             offset = std.math.add(usize, offset, header.size) catch return error.ObjectSizeOverflow;
 312         }
 313         const padded = std.math.add(usize, offset, 7) catch return error.ObjectSizeOverflow;
 314         const headers = padded & ~@as(usize, 7);
 315         const size = std.math.add(usize, headers, @as(usize, self.count) * shdr_size) catch {
 316             return error.ObjectSizeOverflow;
 317         };
 318         std.debug.assert(size >= headers);
 319         return .{ .headers = headers, .size = size };
 320     }
 321 
 322     fn write(self: *const Sections, buffer: []u8, table_offset: usize) void {
 323         std.debug.assert(self.count <= self.headers.len);
 324         for (self.headers[0..self.count], 0..) |header, index| {
 325             writeSectionHeader(buffer, table_offset, @intCast(index), header);
 326         }
 327     }
 328 };
 329 
 330 const Symbols = struct {
 331     records: std.ArrayListUnmanaged(SymbolRecord) = .empty,
 332     indices: std.StringHashMapUnmanaged(u32) = .{},
 333     strings: StringTable,
 334     first_global: u32 = 0,
 335 
 336     fn init(allocator: Allocator) Allocator.Error!Symbols {
 337         return .{ .strings = try StringTable.init(allocator) };
 338     }
 339 
 340     fn deinit(self: *Symbols, allocator: Allocator) void {
 341         self.records.deinit(allocator);
 342         self.indices.deinit(allocator);
 343         self.strings.deinit(allocator);
 344         self.* = undefined;
 345     }
 346 
 347     /// Writes the table in the one order ELF permits: every local symbol first, then
 348     /// `first_global` and the rest. A section symbol is local, so the data sections announce
 349     /// themselves before any datum does.
 350     fn collect(
 351         self: *Symbols,
 352         allocator: Allocator,
 353         sections: Sections,
 354         layouts: SymbolLayouts,
 355         text: []const TextSymbol,
 356         relocations: []const Relocation,
 357     ) ObjectError!void {
 358         std.debug.assert(self.records.items.len == 0);
 359         const data_sections = [_]DataSectionSymbols{
 360             .{ .section = sections.rodata, .symbols = layouts.rodata },
 361             .{ .section = sections.data, .symbols = layouts.data },
 362             .{ .section = sections.bss, .symbols = layouts.bss },
 363         };
 364         try self.add(allocator, .{});
 365         try self.add(allocator, .{ .kind = .section, .section = sections.text });
 366         for (data_sections) |entry| {
 367             if (entry.section == 0) continue;
 368             try self.add(allocator, .{ .kind = .section, .section = entry.section });
 369         }
 370         for (data_sections) |entry| {
 371             if (entry.section == 0) continue;
 372             try self.data(allocator, entry.section, entry.symbols, .local);
 373         }
 374         self.first_global = @intCast(self.records.items.len);
 375         for (data_sections) |entry| {
 376             if (entry.section == 0) continue;
 377             try self.data(allocator, entry.section, entry.symbols, .global);
 378         }
 379         for (text) |symbol| try self.add(allocator, .{
 380             .name = symbol.name,
 381             .binding = .global,
 382             .kind = .function,
 383             .section = sections.text,
 384             .value = symbol.offset,
 385             .size = symbol.size,
 386         });
 387         for (relocations) |relocation| {
 388             if (self.indices.contains(relocation.symbol)) continue;
 389             try self.add(allocator, .{
 390                 .name = relocation.symbol,
 391                 .binding = .global,
 392                 .kind = symbolKindForRelocation(relocation.kind),
 393                 .section = std.elf.SHN_UNDEF,
 394             });
 395         }
 396         std.debug.assert(self.first_global <= self.records.items.len);
 397     }
 398 
 399     fn data(
 400         self: *Symbols,
 401         allocator: Allocator,
 402         section: u16,
 403         symbols: []const DataSymbolLayout,
 404         binding: machine.DataSymbolBinding,
 405     ) ObjectError!void {
 406         for (symbols) |symbol| {
 407             if (symbol.binding != binding) continue;
 408             try self.add(allocator, .{
 409                 .name = symbol.name,
 410                 .binding = if (binding == .local) .local else .global,
 411                 .kind = .object,
 412                 .section = section,
 413                 .value = symbol.offset,
 414                 .size = symbol.size,
 415             });
 416         }
 417     }
 418 
 419     fn add(self: *Symbols, allocator: Allocator, fields: SymbolFields) ObjectError!void {
 420         try appendSymbol(allocator, &self.strings, &self.records, &self.indices, fields);
 421     }
 422 };
 423 
 424 /// The sections a relocation may name. `.bss` is absent on purpose: a `SHT_NOBITS` section
 425 /// occupies no file bytes, so there is no slot in the object to write an address into. A
 426 /// relocation naming `.bss` is refused rather than silently dropped.
 427 const RelocationTarget = enum(u2) { text = 0, rodata = 1, data = 2 };
 428 
 429 const relocation_targets = [_]RelocationTarget{ .text, .rodata, .data };
 430 const relocation_target_count = relocation_targets.len;
 431 
 432 /// The byte length of each target section, indexed by `RelocationTarget`.
 433 const TargetSizes = [relocation_target_count]usize;
 434 
 435 const PlacedBytes = struct { section: u16, target: RelocationTarget, bytes: []const u8 };
 436 
 437 const SymbolLayouts = struct {
 438     rodata: []const DataSymbolLayout,
 439     data: []const DataSymbolLayout,
 440     bss: []const DataSymbolLayout,
 441 };
 442 
 443 const DataSectionSymbols = struct { section: u16, symbols: []const DataSymbolLayout };
 444 
 445 fn relocationTarget(section: []const u8) ObjectError!RelocationTarget {
 446     if (std.mem.eql(u8, section, ".text")) return .text;
 447     if (std.mem.eql(u8, section, ".rodata")) return .rodata;
 448     if (std.mem.eql(u8, section, ".data")) return .data;
 449     return error.UnsupportedRelocation;
 450 }
 451 
 452 /// Counts the relocations landing in one target. A section header is only written for a
 453 /// target that has some, so this runs before any of them are built.
 454 fn relocationCount(relocations: []const Relocation, target: RelocationTarget) usize {
 455     var count: usize = 0;
 456     for (relocations) |relocation| {
 457         const found = relocationTarget(relocation.section) catch continue;
 458         if (found == target) count += 1;
 459     }
 460     return count;
 461 }
 462 
 463 /// Every relocation record, ordered so that one target's records are contiguous. A
 464 /// `SHT_RELA` section is a run of records, so grouping is what lets three sections share one
 465 /// allocation.
 466 const RelocationGroups = struct {
 467     records: []RelaRecord = &.{},
 468     starts: [relocation_target_count]usize = @splat(0),
 469     counts: [relocation_target_count]usize = @splat(0),
 470 
 471     fn slice(self: RelocationGroups, target: RelocationTarget) []const RelaRecord {
 472         const index = @backingInt(target);
 473         return self.records[self.starts[index]..][0..self.counts[index]];
 474     }
 475 
 476     fn deinit(self: *RelocationGroups, allocator: Allocator) void {
 477         if (self.records.len != 0) allocator.free(self.records);
 478         self.* = .{};
 479     }
 480 };
 481 
 482 fn collectRelocations(
 483     allocator: Allocator,
 484     input: RelocatableObject,
 485     sizes: TargetSizes,
 486     symbols: *const std.StringHashMapUnmanaged(u32),
 487 ) ObjectError!RelocationGroups {
 488     if (input.relocations.len == 0) return .{};
 489     const records = try allocator.alloc(RelaRecord, input.relocations.len);
 490     errdefer allocator.free(records);
 491     var groups: RelocationGroups = .{ .records = records };
 492     var written: usize = 0;
 493     for (relocation_targets) |target| {
 494         groups.starts[@backingInt(target)] = written;
 495         for (input.relocations) |relocation| {
 496             if ((try relocationTarget(relocation.section)) != target) continue;
 497             const symbol_index = symbols.get(relocation.symbol) orelse
 498                 return error.MissingRelocationSymbol;
 499             const relocation_type = try x86_64RelocationType(relocation);
 500             try validateRelocationSlot(sizes[@backingInt(target)], relocation);
 501             records[written] = .{
 502                 .offset = relocation.offset,
 503                 .info = (@as(u64, symbol_index) << 32) | relocation_type,
 504                 .addend = x86_64RelocationAddend(relocation),
 505             };
 506             written += 1;
 507         }
 508         groups.counts[@backingInt(target)] = written - groups.starts[@backingInt(target)];
 509     }
 510     std.debug.assert(written == input.relocations.len);
 511     return groups;
 512 }
 513 
 514 /// The bytes of one `SHT_PROGBITS` data section and where each of its symbols sits in them.
 515 /// One data section's placed contents: the bytes the file carries, the extent those bytes
 516 /// occupy once loaded, and where each symbol sits inside it.
 517 ///
 518 /// `.bss` uses this type too. There `bytes` is empty and `size` is the extent, which is the one
 519 /// difference between a section the file carries and a section the loader supplies.
 520 const DataSectionLayout = struct {
 521     bytes: []u8 = &.{},
 522     size: usize = 0,
 523     symbols: []DataSymbolLayout = &.{},
 524     alignment: usize = 1,
 525 
 526     /// Packs the symbols belonging to `section` by the same rule as JIT data mappings. The
 527     /// packing itself stays with `machine.DataLayout`, so no section can drift from another.
 528     fn build(
 529         allocator: Allocator,
 530         symbols: []const machine.DataSymbol,
 531         section: machine.DataSection,
 532     ) ObjectError!DataSectionLayout {
 533         var count: usize = 0;
 534         for (symbols) |symbol| {
 535             if (symbol.section == section) count += 1;
 536         }
 537         if (count == 0) return .{};
 538 
 539         const selected = try allocator.alloc(machine.DataSymbol, count);
 540         defer allocator.free(selected);
 541         var filled: usize = 0;
 542         for (symbols) |symbol| {
 543             if (symbol.section != section) continue;
 544             selected[filled] = symbol;
 545             filled += 1;
 546         }
 547         std.debug.assert(filled == count);
 548 
 549         var layout = machine.DataLayout.init(allocator, selected) catch |err| return switch (err) {
 550             error.OutOfMemory => error.OutOfMemory,
 551             error.InvalidDataSymbol => error.InvalidDataSymbol,
 552             error.DataTooLarge => error.ObjectSizeOverflow,
 553         };
 554         defer layout.deinit(allocator);
 555 
 556         const placed = try allocator.alloc(DataSymbolLayout, count);
 557         errdefer allocator.free(placed);
 558         for (selected, layout.offsets, placed) |symbol, offset, *entry| {
 559             entry.* = .{
 560                 .name = symbol.name,
 561                 .binding = symbol.binding,
 562                 .offset = offset,
 563                 .size = symbol.size(),
 564             };
 565         }
 566         if (!section.carriesBytes()) {
 567             return .{ .size = layout.size, .symbols = placed, .alignment = layout.alignment };
 568         }
 569 
 570         const bytes = try allocator.alloc(u8, layout.size);
 571         layout.write(selected, bytes);
 572         return .{
 573             .bytes = bytes,
 574             .size = layout.size,
 575             .symbols = placed,
 576             .alignment = layout.alignment,
 577         };
 578     }
 579 
 580     fn deinit(self: *DataSectionLayout, allocator: Allocator) void {
 581         if (self.bytes.len != 0) allocator.free(self.bytes);
 582         if (self.symbols.len != 0) allocator.free(self.symbols);
 583         self.* = .{};
 584     }
 585 };
 586 
 587 const DataSymbolLayout = struct {
 588     name: []const u8,
 589     binding: machine.DataSymbolBinding,
 590     offset: u64,
 591     size: u64,
 592 };
 593 
 594 const StringTable = struct {
 595     data: std.ArrayListUnmanaged(u8) = .empty,
 596 
 597     fn init(allocator: Allocator) Allocator.Error!StringTable {
 598         var table = StringTable{};
 599         try table.data.append(allocator, 0);
 600         return table;
 601     }
 602 
 603     fn deinit(self: *StringTable, allocator: Allocator) void {
 604         self.data.deinit(allocator);
 605     }
 606 
 607     fn add(self: *StringTable, allocator: Allocator, name: []const u8) Allocator.Error!u32 {
 608         const offset: u32 = @intCast(self.data.items.len);
 609         try self.data.appendSlice(allocator, name);
 610         try self.data.append(allocator, 0);
 611         return offset;
 612     }
 613 
 614     fn bytes(self: *const StringTable) []const u8 {
 615         return self.data.items;
 616     }
 617 };
 618 
 619 const SymbolBinding = enum {
 620     local,
 621     global,
 622 };
 623 
 624 const SymbolKind = enum {
 625     none,
 626     section,
 627     function,
 628     object,
 629 };
 630 
 631 const SymbolFields = struct {
 632     name: []const u8 = "",
 633     binding: SymbolBinding = .local,
 634     kind: SymbolKind = .none,
 635     section: u16 = std.elf.SHN_UNDEF,
 636     value: u64 = 0,
 637     size: u64 = 0,
 638 };
 639 
 640 const SymbolRecord = struct {
 641     name: u32 = 0,
 642     info: u8 = 0,
 643     other: u8 = 0,
 644     section: u16 = std.elf.SHN_UNDEF,
 645     value: u64 = 0,
 646     size: u64 = 0,
 647 };
 648 
 649 fn appendSymbol(
 650     allocator: Allocator,
 651     strtab: *StringTable,
 652     symbols: *std.ArrayListUnmanaged(SymbolRecord),
 653     symbol_indices: *std.StringHashMapUnmanaged(u32),
 654     fields: SymbolFields,
 655 ) ObjectError!void {
 656     if (fields.name.len != 0 and symbol_indices.contains(fields.name)) return error.DuplicateSymbol;
 657 
 658     const index: u32 = @intCast(symbols.items.len);
 659     const name_offset = if (fields.name.len == 0) 0 else try strtab.add(allocator, fields.name);
 660     try symbols.append(allocator, .{
 661         .name = name_offset,
 662         .info = (@as(u8, elfBinding(fields.binding)) << 4) | elfSymbolKind(fields.kind),
 663         .section = fields.section,
 664         .value = fields.value,
 665         .size = fields.size,
 666     });
 667     if (fields.name.len != 0) {
 668         try symbol_indices.putNoClobber(allocator, fields.name, index);
 669     }
 670 }
 671 
 672 const RelaRecord = struct {
 673     offset: u64,
 674     info: u64,
 675     addend: i64,
 676 };
 677 
 678 const ElfHeaderSpec = struct {
 679     section_header_offset: usize,
 680     section_count: u16,
 681     section_string_table_index: u16,
 682 };
 683 
 684 fn writeElfHeader(buffer: []u8, spec: ElfHeaderSpec) void {
 685     std.mem.copyForwards(u8, buffer[0..4], std.elf.MAGIC);
 686     buffer[std.elf.EI_CLASS] = std.elf.ELFCLASS64;
 687     buffer[std.elf.EI_DATA] = std.elf.ELFDATA2LSB;
 688     buffer[std.elf.EI_VERSION] = 1;
 689     buffer[std.elf.EI_OSABI] = 0;
 690 
 691     writeU16(buffer, 16, @backingInt(std.elf.ET.REL));
 692     writeU16(buffer, 18, @backingInt(std.elf.EM.X86_64));
 693     writeU32(buffer, 20, 1);
 694     writeU64(buffer, 24, 0);
 695     writeU64(buffer, 32, 0);
 696     writeU64(buffer, 40, @intCast(spec.section_header_offset));
 697     writeU32(buffer, 48, 0);
 698     writeU16(buffer, 52, ehdr_size);
 699     writeU16(buffer, 54, 0);
 700     writeU16(buffer, 56, 0);
 701     writeU16(buffer, 58, shdr_size);
 702     writeU16(buffer, 60, spec.section_count);
 703     writeU16(buffer, 62, spec.section_string_table_index);
 704 }
 705 
 706 const SectionHeader = struct {
 707     name: u32 = 0,
 708     section_type: u32 = std.elf.SHT_NULL,
 709     flags: u64 = 0,
 710     address: u64 = 0,
 711     offset: usize = 0,
 712     size: usize = 0,
 713     link: u32 = 0,
 714     info: u32 = 0,
 715     alignment: usize = 0,
 716     entry_size: usize = 0,
 717 
 718     const null_header: SectionHeader = .{};
 719 };
 720 
 721 fn writeSectionHeader(buffer: []u8, table_offset: usize, section_index: u16, header: SectionHeader) void {
 722     const offset = table_offset + @as(usize, section_index) * shdr_size;
 723     writeU32(buffer, offset + 0, header.name);
 724     writeU32(buffer, offset + 4, header.section_type);
 725     writeU64(buffer, offset + 8, header.flags);
 726     writeU64(buffer, offset + 16, header.address);
 727     writeU64(buffer, offset + 24, @intCast(header.offset));
 728     writeU64(buffer, offset + 32, @intCast(header.size));
 729     writeU32(buffer, offset + 40, header.link);
 730     writeU32(buffer, offset + 44, header.info);
 731     writeU64(buffer, offset + 48, @intCast(header.alignment));
 732     writeU64(buffer, offset + 56, @intCast(header.entry_size));
 733 }
 734 
 735 fn writeSymbolRecords(buffer: []u8, offset: usize, symbols: []const SymbolRecord) void {
 736     for (symbols, 0..) |symbol, index| {
 737         const start = offset + index * sym_size;
 738         writeU32(buffer, start + 0, symbol.name);
 739         buffer[start + 4] = symbol.info;
 740         buffer[start + 5] = symbol.other;
 741         writeU16(buffer, start + 6, symbol.section);
 742         writeU64(buffer, start + 8, symbol.value);
 743         writeU64(buffer, start + 16, symbol.size);
 744     }
 745 }
 746 
 747 fn writeRelaRecords(buffer: []u8, offset: usize, records: []const RelaRecord) void {
 748     for (records, 0..) |record, index| {
 749         const start = offset + index * rela_size;
 750         writeU64(buffer, start + 0, record.offset);
 751         writeU64(buffer, start + 8, record.info);
 752         writeU64(buffer, start + 16, @bitCast(record.addend));
 753     }
 754 }
 755 
 756 fn copyInto(buffer: []u8, offset: usize, bytes: []const u8) void {
 757     if (bytes.len == 0) return;
 758     std.mem.copyForwards(u8, buffer[offset .. offset + bytes.len], bytes);
 759 }
 760 
 761 fn validateAlignment(alignment: usize) ObjectError!void {
 762     if (alignment == 0 or (alignment & (alignment - 1)) != 0) return error.InvalidAlignment;
 763 }
 764 
 765 fn validateTextSymbol(text_len: usize, symbol: TextSymbol) ObjectError!void {
 766     if (symbol.name.len == 0) return error.InvalidTextSymbol;
 767     if (symbol.offset > std.math.maxInt(usize) or symbol.size > std.math.maxInt(usize)) {
 768         return error.InvalidTextSymbol;
 769     }
 770     const offset: usize = @intCast(symbol.offset);
 771     const size: usize = @intCast(symbol.size);
 772     if (offset > text_len or size > text_len - offset) return error.InvalidTextSymbol;
 773 }
 774 
 775 fn validateRelocationSlot(section_len: usize, relocation: Relocation) ObjectError!void {
 776     const width_bytes = relocation.width_bits / 8;
 777     if (width_bytes == 0 or relocation.width_bits % 8 != 0) return error.UnsupportedRelocation;
 778     if (relocation.offset > std.math.maxInt(usize)) return error.InvalidRelocationOffset;
 779     const offset: usize = @intCast(relocation.offset);
 780     if (offset > section_len or width_bytes > section_len - offset) {
 781         return error.InvalidRelocationOffset;
 782     }
 783 }
 784 
 785 /// Zeroes every slot the linker will write in one section, so a stale value cannot be read
 786 /// as an address if the relocation is never applied.
 787 fn scrubRelocationSlots(
 788     bytes: []u8,
 789     relocations: []const Relocation,
 790     target: RelocationTarget,
 791 ) void {
 792     for (relocations) |relocation| {
 793         const found = relocationTarget(relocation.section) catch continue;
 794         if (found != target) continue;
 795         const width_bytes = relocation.width_bits / 8;
 796         const offset: usize = @intCast(relocation.offset);
 797         @memset(bytes[offset .. offset + width_bytes], 0);
 798     }
 799 }
 800 
 801 fn symbolKindForRelocation(kind: artifact.RelocationKind) SymbolKind {
 802     return switch (kind) {
 803         .call, .plt => .function,
 804         else => .none,
 805     };
 806 }
 807 
 808 fn elfBinding(binding: SymbolBinding) u8 {
 809     return switch (binding) {
 810         .local => std.elf.STB_LOCAL,
 811         .global => std.elf.STB_GLOBAL,
 812     };
 813 }
 814 
 815 fn elfSymbolKind(kind: SymbolKind) u8 {
 816     return switch (kind) {
 817         .none => std.elf.STT_NOTYPE,
 818         .section => std.elf.STT_SECTION,
 819         .function => std.elf.STT_FUNC,
 820         .object => std.elf.STT_OBJECT,
 821     };
 822 }
 823 
 824 fn x86_64RelocationType(relocation: Relocation) ObjectError!u64 {
 825     return switch (relocation.kind) {
 826         .call => switch (relocation.width_bits) {
 827             64 => @as(u64, @backingInt(std.elf.R_X86_64.@"64")),
 828             32 => @as(u64, @backingInt(std.elf.R_X86_64.PLT32)),
 829             else => error.UnsupportedRelocation,
 830         },
 831         .absolute => switch (relocation.width_bits) {
 832             64 => @as(u64, @backingInt(std.elf.R_X86_64.@"64")),
 833             32 => @as(u64, @backingInt(std.elf.R_X86_64.@"32")),
 834             else => error.UnsupportedRelocation,
 835         },
 836         .relative => switch (relocation.width_bits) {
 837             32 => @as(u64, @backingInt(std.elf.R_X86_64.PC32)),
 838             else => error.UnsupportedRelocation,
 839         },
 840         .plt => switch (relocation.width_bits) {
 841             32 => @as(u64, @backingInt(std.elf.R_X86_64.PLT32)),
 842             else => error.UnsupportedRelocation,
 843         },
 844         .got => switch (relocation.width_bits) {
 845             32 => @as(u64, @backingInt(std.elf.R_X86_64.GOTPCREL)),
 846             else => error.UnsupportedRelocation,
 847         },
 848         else => error.UnsupportedRelocation,
 849     };
 850 }
 851 
 852 fn x86_64RelocationAddend(relocation: Relocation) i64 {
 853     if (relocation.addend != 0) return relocation.addend;
 854     return switch (relocation.kind) {
 855         .call, .plt => if (relocation.width_bits == 32) -4 else 0,
 856         else => 0,
 857     };
 858 }
 859 
 860 fn writeU16(buffer: []u8, offset: usize, value: u16) void {
 861     std.mem.writeInt(u16, buffer[offset..][0..2], value, .little);
 862 }
 863 
 864 fn writeU32(buffer: []u8, offset: usize, value: u32) void {
 865     std.mem.writeInt(u32, buffer[offset..][0..4], value, .little);
 866 }
 867 
 868 fn writeU64(buffer: []u8, offset: usize, value: u64) void {
 869     std.mem.writeInt(u64, buffer[offset..][0..8], value, .little);
 870 }
 871 
 872 fn readU16(bytes: []const u8, offset: usize) u16 {
 873     return std.mem.readInt(u16, bytes[offset..][0..2], .little);
 874 }
 875 
 876 fn readU32(bytes: []const u8, offset: usize) u32 {
 877     return std.mem.readInt(u32, bytes[offset..][0..4], .little);
 878 }
 879 
 880 fn readU64(bytes: []const u8, offset: usize) u64 {
 881     return std.mem.readInt(u64, bytes[offset..][0..8], .little);
 882 }
 883 
 884 const TestSection = struct {
 885     index: u16,
 886     name: []const u8,
 887     header_offset: usize,
 888     offset: usize,
 889     size: usize,
 890     section_type: u32,
 891     link: u32,
 892     info: u32,
 893     entry_size: usize,
 894 };
 895 
 896 fn findTestSection(object: []const u8, name: []const u8) ?TestSection {
 897     const shoff: usize = @intCast(readU64(object, 40));
 898     const shnum = readU16(object, 60);
 899     const shstrndx = readU16(object, 62);
 900     const shstr_header = shoff + @as(usize, shstrndx) * shdr_size;
 901     const shstr_offset: usize = @intCast(readU64(object, shstr_header + 24));
 902     const shstr_size: usize = @intCast(readU64(object, shstr_header + 32));
 903     const shstrtab = object[shstr_offset .. shstr_offset + shstr_size];
 904 
 905     for (0..shnum) |index| {
 906         const header_offset = shoff + index * shdr_size;
 907         const name_offset = readU32(object, header_offset);
 908         const actual_name = stringFromTable(shstrtab, name_offset);
 909         if (!std.mem.eql(u8, actual_name, name)) continue;
 910         return .{
 911             .index = @intCast(index),
 912             .name = actual_name,
 913             .header_offset = header_offset,
 914             .offset = @intCast(readU64(object, header_offset + 24)),
 915             .size = @intCast(readU64(object, header_offset + 32)),
 916             .section_type = readU32(object, header_offset + 4),
 917             .link = readU32(object, header_offset + 40),
 918             .info = readU32(object, header_offset + 44),
 919             .entry_size = @intCast(readU64(object, header_offset + 56)),
 920         };
 921     }
 922     return null;
 923 }
 924 
 925 fn testSymbolName(object: []const u8, symtab: TestSection, symbol_index: usize) []const u8 {
 926     const shoff: usize = @intCast(readU64(object, 40));
 927     const strtab_header = shoff + @as(usize, symtab.link) * shdr_size;
 928     const strtab_offset: usize = @intCast(readU64(object, strtab_header + 24));
 929     const strtab_size: usize = @intCast(readU64(object, strtab_header + 32));
 930     const strtab = object[strtab_offset .. strtab_offset + strtab_size];
 931     const symbol_offset = symtab.offset + symbol_index * sym_size;
 932     return stringFromTable(strtab, readU32(object, symbol_offset));
 933 }
 934 
 935 fn stringFromTable(table: []const u8, offset: u32) []const u8 {
 936     if (offset >= table.len) return "";
 937     const start: usize = @intCast(offset);
 938     const end = std.mem.indexOfScalarPos(u8, table, start, 0) orelse table.len;
 939     return table[start..end];
 940 }
 941 
 942 test "ELF64 objects declare their stack execution requirement" {
 943     for ([_]bool{ false, true }) |executable| {
 944         const bytes = try buildX86_64MachineCodeObject(std.testing.allocator, .{
 945             .entry_symbol = "value",
 946             .code = &.{0xc3},
 947             .executable_stack = executable,
 948         });
 949         defer std.testing.allocator.free(bytes);
 950         const section = findTestSection(bytes, ".note.GNU-stack").?;
 951         try std.testing.expectEqual(std.elf.SHT_PROGBITS, section.section_type);
 952         try std.testing.expectEqual(@as(usize, 0), section.size);
 953         const flags = readU64(bytes, section.header_offset + 8);
 954         try std.testing.expectEqual(
 955             @as(u64, if (executable) std.elf.SHF_EXECINSTR else 0),
 956             flags,
 957         );
 958     }
 959 }
 960 
 961 test "ELF64 object layout rejects unrepresentable section offsets before allocation" {
 962     var sections = Sections.init(
 963         .{ .entry_symbol = "value", .text = &.{0xc3} },
 964         .{},
 965         .{},
 966         .{},
 967     );
 968     sections.headers[sections.text].size = std.math.maxInt(usize);
 969     var symbols = try Symbols.init(std.testing.allocator);
 970     defer symbols.deinit(std.testing.allocator);
 971     try std.testing.expectError(error.ObjectSizeOverflow, sections.place(&symbols, .{}));
 972 }
 973 
 974 test "ELF64 relocatable object records x86_64 call slots" {
 975     const code = [_]u8{
 976         0x48, 0xb8,
 977         0,    0,
 978         0,    0,
 979         0,    0,
 980         0,    0,
 981         0xff, 0xd0,
 982         0xc3,
 983     };
 984     const relocations = [_]machine.CallRelocation{
 985         .{ .offset = 2, .target = "__tiny_runtime_call" },
 986     };
 987     const object = try buildX86_64MachineCodeObject(std.testing.allocator, .{
 988         .entry_symbol = "tiny_entry",
 989         .code = &code,
 990         .relocations = &relocations,
 991     });
 992     defer std.testing.allocator.free(object);
 993 
 994     try std.testing.expectEqualSlices(u8, std.elf.MAGIC, object[0..4]);
 995     try std.testing.expectEqual(@backingInt(std.elf.ET.REL), readU16(object, 16));
 996     try std.testing.expectEqual(@backingInt(std.elf.EM.X86_64), readU16(object, 18));
 997 
 998     const text = findTestSection(object, ".text").?;
 999     try std.testing.expectEqual(std.elf.SHT_PROGBITS, text.section_type);
1000     try std.testing.expectEqualSlices(u8, &code, object[text.offset .. text.offset + text.size]);
1001 
1002     const rela_text = findTestSection(object, ".rela.text").?;
1003     try std.testing.expectEqual(std.elf.SHT_RELA, rela_text.section_type);
1004     try std.testing.expectEqual(@as(u32, text.index), rela_text.info);
1005     try std.testing.expectEqual(@as(usize, rela_size), rela_text.entry_size);
1006     try std.testing.expectEqual(@as(u64, 2), readU64(object, rela_text.offset));
1007 
1008     const info = readU64(object, rela_text.offset + 8);
1009     const symbol_index: usize = @intCast(info >> 32);
1010     try std.testing.expectEqual(@as(u32, @backingInt(std.elf.R_X86_64.@"64")), @as(u32, @truncate(info)));
1011     try std.testing.expectEqual(@as(u64, 0), readU64(object, rela_text.offset + 16));
1012 
1013     const symtab = findTestSection(object, ".symtab").?;
1014     try std.testing.expectEqualStrings("__tiny_runtime_call", testSymbolName(object, symtab, symbol_index));
1015 }
1016 
1017 test "ELF64 relocatable object supports direct PLT32 call relocations" {
1018     const object = try buildRelocatableObject(std.testing.allocator, .{
1019         .entry_symbol = "tiny_entry",
1020         .text = &.{ 0xe8, 0, 0, 0, 0, 0xc3 },
1021         .relocations = &.{.{
1022             .offset = 1,
1023             .symbol = "__tiny_runtime_call",
1024             .kind = .call,
1025             .width_bits = 32,
1026         }},
1027     });
1028     defer std.testing.allocator.free(object);
1029 
1030     const rela_text = findTestSection(object, ".rela.text").?;
1031     const info = readU64(object, rela_text.offset + 8);
1032     try std.testing.expectEqual(@as(u32, @backingInt(std.elf.R_X86_64.PLT32)), @as(u32, @truncate(info)));
1033     try std.testing.expectEqual(@as(u64, @bitCast(@as(i64, -4))), readU64(object, rela_text.offset + 16));
1034 }
1035 
1036 test "ELF64 relocatable object defines multiple text symbols" {
1037     const text_symbols = [_]TextSymbol{
1038         .{ .name = "tiny_entry", .offset = 0, .size = 4 },
1039         .{ .name = "tiny_helper", .offset = 16, .size = 3 },
1040     };
1041     const object = try buildRelocatableObject(std.testing.allocator, .{
1042         .entry_symbol = "tiny_entry",
1043         .text = &.{
1044             0x90, 0x90, 0x90, 0xc3,
1045             0,    0,    0,    0,
1046             0,    0,    0,    0,
1047             0,    0,    0,    0,
1048             0x90, 0x90, 0xc3,
1049         },
1050         .text_symbols = &text_symbols,
1051         .relocations = &.{.{
1052             .offset = 1,
1053             .symbol = "tiny_helper",
1054             .kind = .call,
1055             .width_bits = 32,
1056         }},
1057     });
1058     defer std.testing.allocator.free(object);
1059 
1060     const text = findTestSection(object, ".text").?;
1061     const symtab = findTestSection(object, ".symtab").?;
1062     var found_entry = false;
1063     var found_helper = false;
1064     const symbol_count = symtab.size / sym_size;
1065     for (0..symbol_count) |index| {
1066         const symbol_offset = symtab.offset + index * sym_size;
1067         const symbol_name = testSymbolName(object, symtab, index);
1068         if (std.mem.eql(u8, symbol_name, "tiny_entry")) {
1069             found_entry = true;
1070             try std.testing.expectEqual(text.index, readU16(object, symbol_offset + 6));
1071             try std.testing.expectEqual(@as(u64, 0), readU64(object, symbol_offset + 8));
1072             try std.testing.expectEqual(@as(u64, 4), readU64(object, symbol_offset + 16));
1073         }
1074         if (std.mem.eql(u8, symbol_name, "tiny_helper")) {
1075             found_helper = true;
1076             try std.testing.expectEqual(text.index, readU16(object, symbol_offset + 6));
1077             try std.testing.expectEqual(@as(u64, 16), readU64(object, symbol_offset + 8));
1078             try std.testing.expectEqual(@as(u64, 3), readU64(object, symbol_offset + 16));
1079         }
1080     }
1081     try std.testing.expect(found_entry);
1082     try std.testing.expect(found_helper);
1083 }
1084 
1085 test "ELF64 relocatable object lays out rodata symbols" {
1086     const data_symbols = [_]machine.DataSymbol{
1087         .{ .name = ".Lstring0", .bytes = "abc", .alignment = 8 },
1088         .{ .name = ".Lsymbol0", .bytes = "xy", .alignment = 4 },
1089     };
1090     const object = try buildRelocatableObject(std.testing.allocator, .{
1091         .entry_symbol = "tiny_entry",
1092         .text = &.{0xc3},
1093         .data_symbols = &data_symbols,
1094     });
1095     defer std.testing.allocator.free(object);
1096 
1097     const rodata = findTestSection(object, ".rodata").?;
1098     try std.testing.expectEqual(std.elf.SHT_PROGBITS, rodata.section_type);
1099     try std.testing.expectEqual(@as(usize, 8), readU64(object, rodata.header_offset + 48));
1100     try std.testing.expectEqualSlices(u8, "abc\x00xy", object[rodata.offset .. rodata.offset + rodata.size]);
1101 
1102     const symtab = findTestSection(object, ".symtab").?;
1103     var found_string = false;
1104     var found_symbol = false;
1105     const symbol_count = symtab.size / sym_size;
1106     for (0..symbol_count) |index| {
1107         const symbol_offset = symtab.offset + index * sym_size;
1108         const symbol_name = testSymbolName(object, symtab, index);
1109         if (std.mem.eql(u8, symbol_name, ".Lstring0")) {
1110             found_string = true;
1111             try std.testing.expectEqual(rodata.index, readU16(object, symbol_offset + 6));
1112             try std.testing.expectEqual(@as(u64, 0), readU64(object, symbol_offset + 8));
1113             try std.testing.expectEqual(@as(u64, 3), readU64(object, symbol_offset + 16));
1114         }
1115         if (std.mem.eql(u8, symbol_name, ".Lsymbol0")) {
1116             found_symbol = true;
1117             try std.testing.expectEqual(rodata.index, readU16(object, symbol_offset + 6));
1118             try std.testing.expectEqual(@as(u64, 4), readU64(object, symbol_offset + 8));
1119             try std.testing.expectEqual(@as(u64, 2), readU64(object, symbol_offset + 16));
1120         }
1121     }
1122     try std.testing.expect(found_string);
1123     try std.testing.expect(found_symbol);
1124 }
1125 
1126 test "ELF64 relocatable object can export rodata symbols" {
1127     const data_symbols = [_]machine.DataSymbol{
1128         .{
1129             .name = "__tiny_aot_runtime_import_count",
1130             .bytes = "\x01\x00\x00\x00\x00\x00\x00\x00",
1131             .alignment = 8,
1132             .binding = .global,
1133         },
1134     };
1135     const object = try buildRelocatableObject(std.testing.allocator, .{
1136         .entry_symbol = "tiny_entry",
1137         .text = &.{0xc3},
1138         .data_symbols = &data_symbols,
1139     });
1140     defer std.testing.allocator.free(object);
1141 
1142     const rodata = findTestSection(object, ".rodata").?;
1143     const symtab = findTestSection(object, ".symtab").?;
1144 
1145     var found_symbol = false;
1146     const symbol_count = symtab.size / sym_size;
1147     for (0..symbol_count) |index| {
1148         const symbol_offset = symtab.offset + index * sym_size;
1149         const symbol_name = testSymbolName(object, symtab, index);
1150         if (std.mem.eql(u8, symbol_name, "__tiny_aot_runtime_import_count")) {
1151             found_symbol = true;
1152             try std.testing.expectEqual(rodata.index, readU16(object, symbol_offset + 6));
1153             try std.testing.expectEqual(std.elf.STB_GLOBAL, object[symbol_offset + 4] >> 4);
1154             try std.testing.expectEqual(std.elf.STT_OBJECT, object[symbol_offset + 4] & 0xf);
1155         }
1156     }
1157     try std.testing.expect(found_symbol);
1158 }
1159 
1160 test "ELF64 relocatable object relocates text slots to rodata symbols" {
1161     const data_symbols = [_]machine.DataSymbol{
1162         .{ .name = ".Lstring0", .bytes = "abc", .alignment = 1 },
1163     };
1164     const object = try buildRelocatableObject(std.testing.allocator, .{
1165         .entry_symbol = "tiny_entry",
1166         .text = &.{ 0x48, 0xb8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xc3 },
1167         .data_symbols = &data_symbols,
1168         .relocations = &.{.{
1169             .offset = 2,
1170             .symbol = ".Lstring0",
1171             .kind = .absolute,
1172             .width_bits = 64,
1173         }},
1174     });
1175     defer std.testing.allocator.free(object);
1176 
1177     const text = findTestSection(object, ".text").?;
1178     try std.testing.expectEqualSlices(
1179         u8,
1180         &.{ 0, 0, 0, 0, 0, 0, 0, 0 },
1181         object[text.offset + 2 .. text.offset + 10],
1182     );
1183 
1184     const rela_text = findTestSection(object, ".rela.text").?;
1185     const info = readU64(object, rela_text.offset + 8);
1186     const symbol_index: usize = @intCast(info >> 32);
1187     try std.testing.expectEqual(
1188         @as(u32, @backingInt(std.elf.R_X86_64.@"64")),
1189         @as(u32, @truncate(info)),
1190     );
1191     try std.testing.expectEqual(@as(u64, 0), readU64(object, rela_text.offset + 16));
1192 
1193     const symtab = findTestSection(object, ".symtab").?;
1194     try std.testing.expectEqualStrings(".Lstring0", testSymbolName(object, symtab, symbol_index));
1195 }
1196 
1197 /// One object the data-section tests agree on. A `.rodata` word holds the address of a `.data`
1198 /// word, that word holds the address of a second `.data` word, a `.bss` reservation sits beside
1199 /// them, and `_start` walks the chain and exits with what it finds. Every section and every
1200 /// relocation direction the writer gained appears once, so the structural check and the run
1201 /// check read the same artifact rather than two artifacts that might drift.
1202 const DataSectionProbe = struct {
1203     const counter_initial: u8 = 40;
1204     const counter_increment: u8 = 2;
1205     const expected_status: u8 = counter_initial + counter_increment;
1206     const exit_syscall: u8 = 60;
1207     const reservation_size: usize = 16;
1208     const word_alignment: usize = 8;
1209 
1210     /// `.text` byte offsets of the two slots the linker fills with an absolute address.
1211     const counter_ptr_slot: u64 = 2;
1212     const scratch_slot: u64 = 25;
1213     /// `.data` byte offset of `counter_alias`, which is the second word of the section.
1214     const alias_slot: u64 = 8;
1215 
1216     const text = [_]u8{
1217         0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1218         0x48, 0x8b, 0x00, 0x48, 0x8b, 0x00, 0x48, 0x83, 0x00, counter_increment,
1219         0x48, 0x8b, 0x38, 0x48, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00,
1220         0x00, 0x00, 0x00, 0x48, 0x03, 0x3b, 0x48, 0xc7, 0xc0, exit_syscall,
1221         0x00, 0x00, 0x00, 0x0f, 0x05,
1222     };
1223 
1224     const zero_word: [8]u8 = @splat(0);
1225     const counter_word = [_]u8{ counter_initial, 0, 0, 0, 0, 0, 0, 0 };
1226 
1227     const data_symbols = [_]machine.DataSymbol{
1228         .{
1229             .name = "counter_ptr",
1230             .bytes = &zero_word,
1231             .alignment = word_alignment,
1232             .binding = .global,
1233             .section = .rodata,
1234         },
1235         .{
1236             .name = "counter",
1237             .bytes = &counter_word,
1238             .alignment = word_alignment,
1239             .binding = .global,
1240             .section = .data,
1241         },
1242         .{
1243             .name = "counter_alias",
1244             .bytes = &zero_word,
1245             .alignment = word_alignment,
1246             .binding = .global,
1247             .section = .data,
1248         },
1249         .{
1250             .name = "scratch",
1251             .reserved_size = reservation_size,
1252             .alignment = word_alignment,
1253             .binding = .global,
1254             .section = .bss,
1255         },
1256     };
1257 
1258     const relocations = [_]Relocation{
1259         .{ .offset = counter_ptr_slot, .symbol = "counter_ptr", .kind = .absolute },
1260         .{ .offset = scratch_slot, .symbol = "scratch", .kind = .absolute },
1261         .{ .section = ".rodata", .offset = 0, .symbol = "counter_alias", .kind = .absolute },
1262         .{ .section = ".data", .offset = alias_slot, .symbol = "counter", .kind = .absolute },
1263     };
1264 
1265     fn build(allocator: Allocator) ObjectError![]u8 {
1266         return buildRelocatableObject(allocator, .{
1267             .entry_symbol = "_start",
1268             .text = &text,
1269             .data_symbols = &data_symbols,
1270             .relocations = &relocations,
1271         });
1272     }
1273 };
1274 
1275 fn expectSectionFlags(object: []const u8, section: TestSection, flags: u64) !void {
1276     try std.testing.expectEqual(flags, readU64(object, section.header_offset + 8));
1277 }
1278 
1279 test "ELF64 relocatable object carries writable data beside a zero reservation" {
1280     const object = try DataSectionProbe.build(std.testing.allocator);
1281     defer std.testing.allocator.free(object);
1282 
1283     const data = findTestSection(object, ".data").?;
1284     try std.testing.expectEqual(std.elf.SHT_PROGBITS, data.section_type);
1285     try expectSectionFlags(object, data, std.elf.SHF_ALLOC | std.elf.SHF_WRITE);
1286     try std.testing.expectEqual(@as(usize, 16), data.size);
1287     try std.testing.expectEqual(
1288         @as(u64, DataSectionProbe.counter_initial),
1289         readU64(object, data.offset),
1290     );
1291 
1292     const bss = findTestSection(object, ".bss").?;
1293     try std.testing.expectEqual(std.elf.SHT_NOBITS, bss.section_type);
1294     try expectSectionFlags(object, bss, std.elf.SHF_ALLOC | std.elf.SHF_WRITE);
1295     try std.testing.expectEqual(DataSectionProbe.reservation_size, bss.size);
1296 
1297     const rodata = findTestSection(object, ".rodata").?;
1298     try expectSectionFlags(object, rodata, std.elf.SHF_ALLOC);
1299 
1300     const symtab = findTestSection(object, ".symtab").?;
1301     var counter_section: u16 = 0;
1302     var scratch_section: u16 = 0;
1303     var scratch_value: u64 = 0;
1304     for (0..symtab.size / sym_size) |index| {
1305         const record = symtab.offset + index * sym_size;
1306         const name = testSymbolName(object, symtab, index);
1307         if (std.mem.eql(u8, name, "counter")) {
1308             counter_section = readU16(object, record + 6);
1309             const info = (@as(u8, std.elf.STB_GLOBAL) << 4) | @as(u8, std.elf.STT_OBJECT);
1310             try std.testing.expectEqual(info, object[record + 4]);
1311         }
1312         if (std.mem.eql(u8, name, "scratch")) {
1313             scratch_section = readU16(object, record + 6);
1314             scratch_value = readU64(object, record + 8);
1315             const size = readU64(object, record + 16);
1316             try std.testing.expectEqual(@as(u64, DataSectionProbe.reservation_size), size);
1317         }
1318     }
1319     try std.testing.expectEqual(data.index, counter_section);
1320     try std.testing.expectEqual(bss.index, scratch_section);
1321     try std.testing.expectEqual(@as(u64, 0), scratch_value);
1322 }
1323 
1324 test "ELF64 bss reservations cost no file bytes" {
1325     const text = [_]u8{0xc3};
1326     const small = try buildRelocatableObject(std.testing.allocator, .{
1327         .entry_symbol = "_start",
1328         .text = &text,
1329         .data_symbols = &.{
1330             .{ .name = "scratch", .reserved_size = 16, .alignment = 8, .section = .bss },
1331         },
1332     });
1333     defer std.testing.allocator.free(small);
1334     const huge_size = 1 << 20;
1335     const huge = try buildRelocatableObject(std.testing.allocator, .{
1336         .entry_symbol = "_start",
1337         .text = &text,
1338         .data_symbols = &.{
1339             .{ .name = "scratch", .reserved_size = huge_size, .alignment = 8, .section = .bss },
1340         },
1341     });
1342     defer std.testing.allocator.free(huge);
1343 
1344     try std.testing.expectEqual(small.len, huge.len);
1345     try std.testing.expectEqual(@as(usize, huge_size), findTestSection(huge, ".bss").?.size);
1346 }
1347 
1348 test "ELF64 relocatable object relocates from rodata and from data" {
1349     const object = try DataSectionProbe.build(std.testing.allocator);
1350     defer std.testing.allocator.free(object);
1351 
1352     const symtab = findTestSection(object, ".symtab").?;
1353     const absolute_64: u32 = @backingInt(std.elf.R_X86_64.@"64");
1354 
1355     const rela_text = findTestSection(object, ".rela.text").?;
1356     try std.testing.expectEqual(@as(u32, findTestSection(object, ".text").?.index), rela_text.info);
1357     try std.testing.expectEqual(@as(usize, 2 * rela_size), rela_text.size);
1358 
1359     const rela_rodata = findTestSection(object, ".rela.rodata").?;
1360     const rodata_index = findTestSection(object, ".rodata").?.index;
1361     try std.testing.expectEqual(@as(u32, rodata_index), rela_rodata.info);
1362     try std.testing.expectEqual(@as(usize, rela_size), rela_rodata.size);
1363     try std.testing.expectEqual(@as(u64, 0), readU64(object, rela_rodata.offset));
1364     const rodata_info = readU64(object, rela_rodata.offset + 8);
1365     try std.testing.expectEqual(absolute_64, @as(u32, @truncate(rodata_info)));
1366     try std.testing.expectEqualStrings(
1367         "counter_alias",
1368         testSymbolName(object, symtab, @intCast(rodata_info >> 32)),
1369     );
1370 
1371     const rela_data = findTestSection(object, ".rela.data").?;
1372     try std.testing.expectEqual(@as(u32, findTestSection(object, ".data").?.index), rela_data.info);
1373     try std.testing.expectEqual(@as(usize, rela_size), rela_data.size);
1374     try std.testing.expectEqual(DataSectionProbe.alias_slot, readU64(object, rela_data.offset));
1375     const data_info = readU64(object, rela_data.offset + 8);
1376     try std.testing.expectEqual(absolute_64, @as(u32, @truncate(data_info)));
1377     try std.testing.expectEqualStrings(
1378         "counter",
1379         testSymbolName(object, symtab, @intCast(data_info >> 32)),
1380     );
1381 
1382     try std.testing.expectEqual(@as(u32, symtab.index), rela_rodata.link);
1383     try std.testing.expectEqual(@as(u32, symtab.index), rela_data.link);
1384 }
1385 
1386 test "ELF64 writer refuses a relocation into a section with no file bytes" {
1387     try std.testing.expectError(error.UnsupportedRelocation, buildRelocatableObject(
1388         std.testing.allocator,
1389         .{
1390             .entry_symbol = "_start",
1391             .text = &.{0xc3},
1392             .data_symbols = &.{
1393                 .{ .name = "scratch", .reserved_size = 8, .alignment = 8, .section = .bss },
1394             },
1395             .relocations = &.{.{
1396                 .section = ".bss",
1397                 .offset = 0,
1398                 .symbol = "scratch",
1399                 .kind = .absolute,
1400             }},
1401         },
1402     ));
1403 }
1404 
1405 /// Where a caller names the `tldr-link` binary. The linker is another package's artifact and
1406 /// nothing in this package's build graph produces it, so the gate reads a path rather than
1407 /// guessing one, and reports absence as a skip rather than a pass.
1408 const tldr_link_env = "CHOIR_TLDR_LINK";
1409 
1410 fn expectExitCode(process_io: anytype, argv: []const []const u8, expected: i64) !void {
1411     var child = try sys.process.spawn(process_io, .{
1412         .argv = argv,
1413         .stdin = .ignore,
1414         .stdout = .ignore,
1415         .stderr = .inherit,
1416     });
1417     defer sys.process.killAndReap(&child, process_io);
1418     const termination = try sys.process.wait(&child, process_io);
1419     try std.testing.expectEqual(expected, sys.process.exitCode(termination));
1420 }
1421 
1422 test "ELF64 data sections link and run through tldr" {
1423     if (!sys.capabilities.current.isLinux()) return error.SkipZigTest;
1424     if (!sys.capabilities.current.supportsX86_64Execution()) return error.SkipZigTest;
1425     const allocator = std.testing.allocator;
1426     const linker = (try sys.env.getOwned(allocator, tldr_link_env)) orelse return error.SkipZigTest;
1427     defer allocator.free(linker);
1428 
1429     var tmp = std.testing.tmpDir(.{});
1430     defer tmp.cleanup();
1431     const root = try tmp.parent_dir.realPathFileAlloc(
1432         std.Options.debug_io,
1433         tmp.sub_path[0..],
1434         allocator,
1435     );
1436     defer allocator.free(root);
1437     const object_path = try std.fs.path.join(allocator, &.{ root, "probe.o" });
1438     defer allocator.free(object_path);
1439     const program_path = try std.fs.path.join(allocator, &.{ root, "probe" });
1440     defer allocator.free(program_path);
1441 
1442     const object = try DataSectionProbe.build(allocator);
1443     defer allocator.free(object);
1444     try sys.fs.writeFile(object_path, object);
1445 
1446     var io_state = sys.thread.initThreadedIo(allocator, .{});
1447     defer io_state.deinit();
1448     const process_io = io_state.io();
1449 
1450     const link_argv = [_][]const u8{ linker, "-o", program_path, "-e", "_start", object_path };
1451     try expectExitCode(process_io, &link_argv, 0);
1452     try expectExitCode(process_io, &.{program_path}, DataSectionProbe.expected_status);
1453 }
1454 
1455 comptime {
1456     alloc_phase.capacity.declareDynamicUnbounded("choir.elf_symbols", Symbols);
1457 }