lib/tldr/src/formats/elf/object/model.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const elf = @import("../root.zig");
3
4 const format = elf.format;
5 const group_word_size = format.group_word_size;
6
7 /// Names each defect that stops a description (the values describing a
8 /// relocatable object) from becoming an object file, so callers of `plan` and
9 /// `build` switch on these to tell a malformed description apart from running
10 /// out of memory. Allocation failure is kept out of this set, and `Error` joins
11 /// the two for `plan` and `build`.
12 pub const DescriptionError = error{
13 InvalidAlignment,
14 InvalidSize,
15 InvalidSectionIndex,
16 SymbolOrder,
17 TooManySections,
18 TooManySymbols,
19 SizeOverflow,
20 };
21
22 pub const Error = std.mem.Allocator.Error || DescriptionError;
23
24 /// The struct holds the file header fields a producer picks: object type
25 /// (relocatable by default), machine (x86-64 by default), entry address and
26 /// flags. A description (the values describing a relocatable object) carries
27 /// this struct to set the object's file header. The section table's offset,
28 /// count and name table index come from the layout (the file offsets `plan`
29 /// computes for each record), so a description has no fields for them.
30 pub const Header = struct {
31 type: std.elf.ET = .REL,
32 machine: std.elf.EM = .X86_64,
33 entry: u64 = 0,
34 flags: u32 = 0,
35 };
36
37 /// Describes one section of a description (the values describing a relocatable
38 /// object), so a caller lists one per section the object carries, in file
39 /// order. Each entry `sections[i]` gets file index `i + 1`, because the writer
40 /// places the empty section header at index 0 itself. A section of type
41 /// `SHT_NOBITS` has no bytes and reserves `size` bytes of memory. Any other
42 /// section is as long as its `bytes`, and `plan` refuses a nonzero `size` that
43 /// differs from that length.
44 pub const Section = struct {
45 name: []const u8,
46 section_type: u32 = std.elf.SHT_PROGBITS,
47 flags: u64 = 0,
48 alignment: u64 = 1,
49 bytes: []const u8 = &.{},
50 size: u64 = 0,
51 address: u64 = 0,
52 link: u32 = 0,
53 info: u32 = 0,
54 entry_size: u64 = 0,
55
56 pub fn progbits(name: []const u8, bytes: []const u8, flags: u64, alignment: u64) Section {
57 return .{
58 .name = name,
59 .section_type = std.elf.SHT_PROGBITS,
60 .flags = flags | std.elf.SHF_ALLOC,
61 .alignment = alignment,
62 .bytes = bytes,
63 };
64 }
65
66 pub fn nobits(name: []const u8, size: u64, flags: u64, alignment: u64) Section {
67 return .{
68 .name = name,
69 .section_type = std.elf.SHT_NOBITS,
70 .flags = flags | std.elf.SHF_ALLOC,
71 .alignment = alignment,
72 .size = size,
73 };
74 }
75
76 pub fn mergeable(
77 name: []const u8,
78 bytes: []const u8,
79 flags: u64,
80 alignment: u64,
81 entry_size: u64,
82 ) Section {
83 return .{
84 .name = name,
85 .section_type = std.elf.SHT_PROGBITS,
86 .flags = flags | std.elf.SHF_ALLOC | std.elf.SHF_MERGE,
87 .alignment = alignment,
88 .bytes = bytes,
89 .entry_size = entry_size,
90 };
91 }
92
93 pub fn strings(name: []const u8, bytes: []const u8, alignment: u64) Section {
94 return .{
95 .name = name,
96 .section_type = std.elf.SHT_PROGBITS,
97 .flags = std.elf.SHF_ALLOC | std.elf.SHF_MERGE | std.elf.SHF_STRINGS,
98 .alignment = alignment,
99 .bytes = bytes,
100 .entry_size = 1,
101 };
102 }
103
104 pub fn nonAlloc(
105 name: []const u8,
106 bytes: []const u8,
107 section_type: u32,
108 alignment: u64,
109 ) Section {
110 return .{
111 .name = name,
112 .section_type = section_type,
113 .alignment = alignment,
114 .bytes = bytes,
115 };
116 }
117
118 /// Builds a COMDAT group section whose payload is `bytes`, which the caller
119 /// fills with `groupBytes` first, for a caller that needs sections kept or
120 /// dropped together. The caller states only the signature symbol's index,
121 /// and the writer points the section's link at the symbol table.
122 pub fn group(name: []const u8, bytes: []const u8, signature_symbol: u32) Section {
123 return .{
124 .name = name,
125 .section_type = std.elf.SHT_GROUP,
126 .alignment = group_word_size,
127 .bytes = bytes,
128 .info = signature_symbol,
129 .entry_size = group_word_size,
130 };
131 }
132
133 pub fn isNoBits(self: Section) bool {
134 return self.section_type == std.elf.SHT_NOBITS;
135 }
136
137 /// Returns the number of bytes the section takes in the file: zero for a
138 /// reservation, else the length of `bytes`. The layout (the file offsets
139 /// `plan` computes) uses it to advance the file offset past a section.
140 pub fn fileSize(self: Section) u64 {
141 if (self.isNoBits()) return 0;
142 return self.bytes.len;
143 }
144
145 /// Returns the value written into the section header's `sh_size` field: the
146 /// reserved size for a NOBITS section, else the length of `bytes`.
147 pub fn wireSize(self: Section) u64 {
148 if (self.isNoBits()) return self.size;
149 return self.bytes.len;
150 }
151 };
152
153 /// Writes a COMDAT group payload into `out`: the COMDAT flag word, then one
154 /// 32-bit word per member section index. The destination slice `out` holds at
155 /// least `(members.len + 1) * 4` bytes, which a debug assertion checks. The
156 /// function returns the filled prefix of `out`, ready to pass to
157 /// `Section.group`.
158 pub fn groupBytes(out: []u8, members: []const u16) []u8 {
159 const needed = (members.len + 1) * group_word_size;
160 std.debug.assert(out.len >= needed);
161 const bytes = out[0..needed];
162 format.writeU32(bytes, 0, std.elf.GRP_COMDAT);
163 for (members, 0..) |member, index| {
164 format.writeU32(bytes, (index + 1) * group_word_size, member);
165 }
166 return bytes;
167 }
168
169 /// Describes one symbol of a description (the values describing a relocatable
170 /// object), so a description lists one per symbol table entry, locals first.
171 /// Each entry `symbols[i]` gets table index `i + 1`, because the writer places
172 /// the empty entry at index 0 itself. The writer adds no symbols of its own, so
173 /// a description that needs section symbols lists them. The design this
174 /// namespace follows names the section field `section`, but because Zig rejects
175 /// a struct with a field and a declaration of the same name and the `section`
176 /// constructor kept that name, the field is `section_index`, the name
177 /// `record.SymbolRecord` also uses.
178 pub const Symbol = struct {
179 name: []const u8 = "",
180 binding: u8 = std.elf.STB_LOCAL,
181 kind: u8 = std.elf.STT_NOTYPE,
182 other: u8 = 0,
183 section_index: u16 = std.elf.SHN_UNDEF,
184 value: u64 = 0,
185 size: u64 = 0,
186
187 pub fn section(index: u16) Symbol {
188 return .{ .kind = std.elf.STT_SECTION, .section_index = index };
189 }
190
191 pub fn function(name: []const u8, section_index: u16, value: u64, size: u64) Symbol {
192 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_FUNC, section_index, value, size);
193 }
194
195 pub fn weakFunction(name: []const u8, section_index: u16, value: u64, size: u64) Symbol {
196 return defined(name, std.elf.STB_WEAK, std.elf.STT_FUNC, section_index, value, size);
197 }
198
199 pub fn ifunc(name: []const u8, section_index: u16, value: u64, size: u64) Symbol {
200 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_GNU_IFUNC, section_index, value, size);
201 }
202
203 pub fn object(name: []const u8, section_index: u16, value: u64, size: u64) Symbol {
204 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_OBJECT, section_index, value, size);
205 }
206
207 pub fn localObject(name: []const u8, section_index: u16, value: u64, size: u64) Symbol {
208 return defined(name, std.elf.STB_LOCAL, std.elf.STT_OBJECT, section_index, value, size);
209 }
210
211 pub fn gnuUniqueObject(name: []const u8, section_index: u16, value: u64, size: u64) Symbol {
212 const unique = std.elf.STB_GNU_UNIQUE;
213 return defined(name, unique, std.elf.STT_OBJECT, section_index, value, size);
214 }
215
216 pub fn tlsObject(name: []const u8, section_index: u16, value: u64, size: u64) Symbol {
217 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_TLS, section_index, value, size);
218 }
219
220 pub fn absoluteObject(name: []const u8, value: u64, size: u64) Symbol {
221 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_OBJECT, std.elf.SHN_ABS, value, size);
222 }
223
224 /// Builds a global common symbol with section index `SHN_COMMON`, the
225 /// required alignment in `value`, and the block's size in `size`, for a
226 /// caller describing a C tentative definition or any common block. For a
227 /// symbol in `SHN_COMMON`, ELF defines the value field as the alignment.
228 pub fn commonObject(name: []const u8, alignment: u64, size: u64) Symbol {
229 const block = std.elf.SHN_COMMON;
230 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_OBJECT, block, alignment, size);
231 }
232
233 pub fn undefinedObject(name: []const u8) Symbol {
234 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_OBJECT, std.elf.SHN_UNDEF, 0, 0);
235 }
236
237 pub fn undefinedFunction(name: []const u8) Symbol {
238 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_FUNC, std.elf.SHN_UNDEF, 0, 0);
239 }
240
241 pub fn weakUndefinedFunction(name: []const u8) Symbol {
242 return defined(name, std.elf.STB_WEAK, std.elf.STT_FUNC, std.elf.SHN_UNDEF, 0, 0);
243 }
244
245 pub fn undefinedSymbol(name: []const u8) Symbol {
246 return defined(name, std.elf.STB_GLOBAL, std.elf.STT_NOTYPE, std.elf.SHN_UNDEF, 0, 0);
247 }
248
249 pub fn isLocal(self: Symbol) bool {
250 return self.binding == std.elf.STB_LOCAL;
251 }
252
253 pub fn info(self: Symbol) u8 {
254 return format.elfSymbolInfo(self.binding, self.kind);
255 }
256
257 fn defined(
258 name: []const u8,
259 binding: u8,
260 kind: u8,
261 section_index: u16,
262 value: u64,
263 size: u64,
264 ) Symbol {
265 return .{
266 .name = name,
267 .binding = binding,
268 .kind = kind,
269 .section_index = section_index,
270 .value = value,
271 .size = size,
272 };
273 }
274 };
275
276 /// Describes one relocation against a described section: the target section's
277 /// index, the offset inside it, the symbol index, the relocation type and the
278 /// addend. A caller lists one per relocation in the object. The one field
279 /// `plan` checks is the target index, since placing the record depends on it.
280 /// The symbol index, offset, type and addend reach the file unchecked, so a
281 /// test can build the malformed objects that the linker's own checks must
282 /// reject.
283 pub const Relocation = struct {
284 section: u16,
285 offset: u64,
286 symbol: u32,
287 type: u32,
288 addend: i64 = 0,
289
290 pub fn x86_64(
291 section: u16,
292 offset: u64,
293 symbol: u32,
294 kind: std.elf.R_X86_64,
295 addend: i64,
296 ) Relocation {
297 return .{
298 .section = section,
299 .offset = offset,
300 .symbol = symbol,
301 .type = @backingInt(kind),
302 .addend = addend,
303 };
304 }
305
306 pub fn info(self: Relocation) u64 {
307 return (@as(u64, self.symbol) << 32) | self.type;
308 }
309 };
310
311 pub const Description = struct {
312 header: Header = .{},
313 sections: []const Section,
314 symbols: []const Symbol = &.{},
315 relocations: []const Relocation = &.{},
316 };
317
318 test "ELF object sections separate their file size from their wire size" {
319 const progbits = Section.progbits(".text", "\x90\x90\x90\x90", std.elf.SHF_EXECINSTR, 16);
320 try std.testing.expectEqual(@as(u64, std.elf.SHT_PROGBITS), progbits.section_type);
321 const code_flags = std.elf.SHF_ALLOC | std.elf.SHF_EXECINSTR;
322 try std.testing.expectEqual(@as(u64, code_flags), progbits.flags);
323 try std.testing.expectEqual(@as(u64, 4), progbits.fileSize());
324 try std.testing.expectEqual(@as(u64, 4), progbits.wireSize());
325
326 const reserved = Section.nobits(".bss", 4096, std.elf.SHF_WRITE, 8);
327 try std.testing.expect(reserved.isNoBits());
328 try std.testing.expectEqual(@as(u64, 0), reserved.fileSize());
329 try std.testing.expectEqual(@as(u64, 4096), reserved.wireSize());
330 try std.testing.expectEqual(@as(u64, std.elf.SHF_ALLOC | std.elf.SHF_WRITE), reserved.flags);
331 }
332
333 test "ELF object sections carry their merge and group shape" {
334 const merged = Section.mergeable(".rodata.cst8", "01234567", 0, 8, 8);
335 try std.testing.expectEqual(@as(u64, std.elf.SHF_ALLOC | std.elf.SHF_MERGE), merged.flags);
336 try std.testing.expectEqual(@as(u64, 8), merged.entry_size);
337
338 const text = Section.strings(".rodata.str1.1", "hi\x00", 1);
339 try std.testing.expectEqual(
340 @as(u64, std.elf.SHF_ALLOC | std.elf.SHF_MERGE | std.elf.SHF_STRINGS),
341 text.flags,
342 );
343 try std.testing.expectEqual(@as(u64, 1), text.entry_size);
344
345 const notes = Section.nonAlloc(".comment", "tldr\x00", std.elf.SHT_PROGBITS, 1);
346 try std.testing.expectEqual(@as(u64, 0), notes.flags);
347
348 var storage: [3 * 4]u8 = undefined;
349 const payload = groupBytes(&storage, &.{ 1, 2 });
350 try std.testing.expectEqual(@as(usize, 12), payload.len);
351 try std.testing.expectEqual(@as(u32, std.elf.GRP_COMDAT), format.readU32(payload, 0));
352 try std.testing.expectEqual(@as(u32, 1), format.readU32(payload, 4));
353 try std.testing.expectEqual(@as(u32, 2), format.readU32(payload, 8));
354
355 const comdat = Section.group(".group", payload, 7);
356 try std.testing.expectEqual(@as(u32, std.elf.SHT_GROUP), comdat.section_type);
357 try std.testing.expectEqual(@as(u32, 7), comdat.info);
358 try std.testing.expectEqual(@as(u64, 4), comdat.alignment);
359 try std.testing.expectEqual(@as(u64, 4), comdat.entry_size);
360 }
361
362 test "ELF object symbols fold their binding and kind into one info byte" {
363 try std.testing.expectEqual(@as(u8, 0x03), Symbol.section(2).info());
364 try std.testing.expectEqual(@as(u16, 2), Symbol.section(2).section_index);
365 try std.testing.expect(Symbol.section(2).isLocal());
366
367 try std.testing.expectEqual(@as(u8, 0x12), Symbol.function("main", 1, 0, 4).info());
368 try std.testing.expectEqual(@as(u8, 0x22), Symbol.weakFunction("weak", 1, 0, 4).info());
369 try std.testing.expectEqual(@as(u8, 0x1a), Symbol.ifunc("resolve", 1, 0, 4).info());
370 try std.testing.expectEqual(@as(u8, 0x11), Symbol.object("data", 2, 0, 8).info());
371 try std.testing.expectEqual(@as(u8, 0x01), Symbol.localObject("private", 2, 0, 8).info());
372 try std.testing.expectEqual(@as(u8, 0xa1), Symbol.gnuUniqueObject("unique", 2, 0, 8).info());
373 try std.testing.expectEqual(@as(u8, 0x16), Symbol.tlsObject("thread", 3, 0, 8).info());
374 try std.testing.expectEqual(@as(u8, 0x10), Symbol.undefinedSymbol("plain").info() & 0xf0);
375
376 const fixed = Symbol.absoluteObject("fixed", 9, 0);
377 try std.testing.expectEqual(@as(u16, std.elf.SHN_ABS), fixed.section_index);
378 const common = Symbol.commonObject("shared", 16, 32);
379 try std.testing.expectEqual(@as(u16, std.elf.SHN_COMMON), common.section_index);
380 try std.testing.expectEqual(@as(u64, 16), common.value);
381 try std.testing.expectEqual(@as(u64, 32), common.size);
382 const undefined_object = Symbol.undefinedObject("missing");
383 const undefined_function = Symbol.undefinedFunction("missing");
384 try std.testing.expectEqual(@as(u16, std.elf.SHN_UNDEF), undefined_object.section_index);
385 try std.testing.expectEqual(@as(u16, std.elf.SHN_UNDEF), undefined_function.section_index);
386 try std.testing.expect(!Symbol.weakUndefinedFunction("maybe").isLocal());
387 }
388
389 test "ELF object relocations pack their symbol and type into one info word" {
390 const rela = Relocation.x86_64(1, 0x10, 3, .@"64", -8);
391 try std.testing.expectEqual(@as(u16, 1), rela.section);
392 try std.testing.expectEqual(@as(u64, 0x10), rela.offset);
393 try std.testing.expectEqual(@as(i64, -8), rela.addend);
394 try std.testing.expectEqual(@as(u64, (3 << 32) | 1), rela.info());
395 try std.testing.expectEqual(@as(u32, 3), format.Rela.symbolIndex(.{ .info = rela.info() }));
396 try std.testing.expectEqual(@as(u32, 1), format.Rela.relocationType(.{ .info = rela.info() }));
397 }