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 }