Class: Rubycc::ObjFile::ELFWriter

Inherits:
Object
  • Object
show all
Defined in:
lib/rubycc/objfile/elf_writer.rb

Overview

Writes a minimal ELF64 relocatable object (ET_REL) for Linux x86_64.

Section layout (in this order): NULL, .text, .rodata (only with string literals), .data (only with initialized globals), .bss (only with zero-initialized globals), .rela.text (only when there are text relocations), .rela.data (only when a .data pointer slot needs one), .note.GNU-stack, .symtab, .strtab, .shstrtab. Section indices are not hard-coded: the ordered name list is assembled in #to_binary and a name -> index lookup resolves the cross-references (symtab's sh_link, rela's sh_link/sh_info, and each symbol's st_shndx). Symbol table order is NULL, STT_FILE, the .text section symbol, the .rodata section symbol (when present), then the file-local (static) functions and objects — every STB_LOCAL must precede the first STB_GLOBAL — followed by the external symbols (defined functions, then defined file-scope objects, then undefined externals); r_info in .rela.text indexes into that final order.

.rela.text carries five machine-independent relocation kinds — :call, :func, :string, :global and :got — each translated through the injected machine description (see MachineDescription) into one or more concrete ELF relocation entries. On the default x86_64 machine each kind costs exactly one entry: a call site as an R_X86_64_PLT32 against its (defined or undefined) symbol, a taken function address the same way, a string reference as an R_X86_64_PC32 against the .rodata section symbol with the string's byte offset as its addend, a global reference as an R_X86_64_PC32 against that global's own object symbol, and a PIC GOT reference (a "-fPIC" access to a symbol this unit does not define) as an R_X86_64_REX_GOTPCRELX against that symbol — all four PC-relative kinds biased by -4 for the rel32 field's placement. On aarch64 an address-forming kind instead costs a pair of entries, one per instruction of the adrp/add (or adrp/ldr) sequence that machine needs; the writer emits whatever the description lists.

.rela.data carries the absolute R_X86_64_64 relocations that patch a .data pointer slot: one against another object's symbol (addend 0) for a "&other" or a decayed global array, and one against the .rodata section symbol (the string's byte offset as its addend) for a string-literal pointer.

.data holds the initialized globals' little-endian bytes; .bss is a NOBITS section that reserves space for the zero-initialized ones without occupying any file bytes. Both are writable (SHF_WRITE | SHF_ALLOC).

The empty .note.GNU-stack marks the stack as non-executable so the linker does not warn about a missing GNU_STACK note. Output is fully deterministic (N4): no timestamps or other varying data are embedded.

Defined Under Namespace

Classes: MachineDescription, RelocDesc

Constant Summary collapse

ELFCLASS64 =

ELF constants

2
ELFDATA2LSB =
1
EV_CURRENT =
1
ET_REL =
1
EM_X86_64 =
62
EM_AARCH64 =
183
SHN_UNDEF =
0
SHN_ABS =
0xFFF1
SHT_NULL =

Section header types

0
SHT_PROGBITS =
1
SHT_SYMTAB =
2
SHT_STRTAB =
3
SHT_RELA =
4
SHT_NOBITS =
8
SHT_INIT_ARRAY =

The two array section types the runtime walks: SHT_INIT_ARRAY holds pointers to the constructors it calls at startup/dlopen, SHT_FINI_ARRAY the destructors it calls at exit/dlclose. Distinct types (rather than PROGBITS with a magic name) because the linker groups them by type and advertises each run through DT_INIT_ARRAY / DT_FINI_ARRAY.

14
SHT_FINI_ARRAY =
15
SHF_WRITE =

Section header flags

0x1
SHF_ALLOC =
0x2
SHF_EXECINSTR =
0x4
0x40
STB_LOCAL =

Symbol binding/type (st_info = (bind << 4) | type)

0
STB_GLOBAL =
1
STT_NOTYPE =
0
STT_OBJECT =
1
STT_FUNC =
2
STT_SECTION =
3
STT_FILE =
4
STV =
{ default: 0, internal: 1, hidden: 2, protected: 3 }.freeze
R_X86_64_64 =

x86_64 relocation types: 64 for an absolute 64-bit address (a pointer slot in .data initialized to another object's address), PC32 for a plain PC-relative reference (e.g. a "lea rip" into .rodata), PLT32 for a near call (PC-relative, PLT-aware) and REX_GOTPCRELX for a PIC data access (a "mov rax, sym@GOTPCREL(rip)" reading the symbol's GOT slot; the "REX" form marks the REX.W-prefixed mov a linker may relax back to a lea).

1
R_X86_64_PC32 =
2
R_X86_64_PLT32 =
4
R_X86_64_REX_GOTPCRELX =
42
R_AARCH64_ABS64 =

aarch64 relocation types. CALL26 patches the 26-bit immediate of a bl (or b) with the PC-relative word distance to its target. The remaining four come in pairs, because aarch64 forms a symbol's address in two instructions rather than one: ADR_PREL_PG_HI21 fills the 21-bit page-offset immediate of an adrp (the distance from the referring instruction's own 4 KiB page to the symbol's), and ADD_ABS_LO12_NC fills the 12-bit immediate of the following add with the symbol's offset within that page. The GOT pair is the same split applied to the symbol's Global Offset Table slot: ADR_GOT_PAGE names the slot's page and LD64_GOT_LO12_NC the scaled 12-bit immediate of the ldr reading it. "NC" is "no check": the low half cannot overflow, so the linker does not range-check it. The numbers were read off real aarch64-linux-gnu-gcc output rather than transcribed. ABS64 is aarch64's absolute 64-bit pointer slot, the counterpart of R_X86_64_64, used in .data rather than .text.

257
R_AARCH64_ADR_PREL_PG_HI21 =
275
R_AARCH64_ADD_ABS_LO12_NC =
277
R_AARCH64_CALL26 =
283
R_AARCH64_ADR_GOT_PAGE =
311
R_AARCH64_LD64_GOT_LO12_NC =
312
X86_64_NOP =

Inter-function padding: x86_64's one-byte nop (0x90), and aarch64's four-byte nop word 0xD503201F stored little-endian (every aarch64 instruction is four bytes, so the unit of padding is a whole word and the 16-byte function alignment is always a whole number of them).

"\x90".b
AARCH64_NOP =
[0xD503201F].pack("L<")
X86_64 =

The default machine: x86_64. Its relocation table fixes the exact ELF types and addend conventions the System V AMD64 psABI defines for each kind. Every .text kind patches a rel32 field measured from the end of the instruction it belongs to, hence the uniform -4 bias; a :call and a taken :func address are both near PC-relative references and share PLT32. A string reference carries the interned string's .rodata byte offset as its recorded addend, which the same -4 then biases.

MachineDescription.new(
  e_machine: EM_X86_64,
  relocations: {
    call:   [RelocDesc.new(type: R_X86_64_PLT32, addend: 0, symbol: :named, addend_bias: -4)],
    func:   [RelocDesc.new(type: R_X86_64_PLT32, addend: 0, symbol: :named, addend_bias: -4)],
    string: [RelocDesc.new(type: R_X86_64_PC32, addend: :recorded, symbol: :rodata_section,
                           addend_bias: -4)],
    global: [RelocDesc.new(type: R_X86_64_PC32, addend: 0, symbol: :named, addend_bias: -4)],
    got:    [RelocDesc.new(type: R_X86_64_REX_GOTPCRELX, addend: 0, symbol: :named,
                           addend_bias: -4)],
    symbol: [RelocDesc.new(type: R_X86_64_64, addend: :recorded, symbol: :named)],
    rodata: [RelocDesc.new(type: R_X86_64_64, addend: :recorded, symbol: :rodata_section)]
  }.freeze,
  text_padding: X86_64_NOP
)
AARCH64 =

The aarch64 machine. A direct :call is a single bl and so a single CALL26; every other .text kind forms an address across two instructions and therefore takes two entries, the second four bytes past the first.

:string, :global and :func all use the adrp/add pair, differing only in what they resolve against — the .rodata section symbol for a string (biased by the string's own byte offset, which the linker folds into the page computation for both halves alike) and the named symbol otherwise. :got uses the adrp/ldr pair addressing the symbol's GOT slot. None of them takes a bias: aarch64's relocations name the symbol directly rather than a displacement from the end of a field.

The .data kinds (:symbol, :rodata) are absolute 64-bit pointer slots and so are not machine-shaped at all beyond the type number; ABS64 is aarch64's spelling of the same thing R_X86_64_64 does.

MachineDescription.new(
  e_machine: EM_AARCH64,
  relocations: {
    call:   [RelocDesc.new(type: R_AARCH64_CALL26, addend: 0, symbol: :named)],
    string: [RelocDesc.new(type: R_AARCH64_ADR_PREL_PG_HI21, addend: :recorded,
                           symbol: :rodata_section),
             RelocDesc.new(type: R_AARCH64_ADD_ABS_LO12_NC, addend: :recorded,
                           symbol: :rodata_section, offset_delta: 4)],
    global: [RelocDesc.new(type: R_AARCH64_ADR_PREL_PG_HI21, addend: 0, symbol: :named),
             RelocDesc.new(type: R_AARCH64_ADD_ABS_LO12_NC, addend: 0, symbol: :named,
                           offset_delta: 4)],
    func:   [RelocDesc.new(type: R_AARCH64_ADR_PREL_PG_HI21, addend: 0, symbol: :named),
             RelocDesc.new(type: R_AARCH64_ADD_ABS_LO12_NC, addend: 0, symbol: :named,
                           offset_delta: 4)],
    got:    [RelocDesc.new(type: R_AARCH64_ADR_GOT_PAGE, addend: 0, symbol: :named),
             RelocDesc.new(type: R_AARCH64_LD64_GOT_LO12_NC, addend: 0, symbol: :named,
                           offset_delta: 4)],
    symbol: [RelocDesc.new(type: R_AARCH64_ABS64, addend: :recorded, symbol: :named)],
    rodata: [RelocDesc.new(type: R_AARCH64_ABS64, addend: :recorded, symbol: :rodata_section)]
  }.freeze,
  text_padding: AARCH64_NOP
)
ARRAY_ENTSIZE =

--- initializer / finalizer arrays ------------------------------------ An array section is a flat vector of 8-byte function pointers, each slot filled in by an absolute 64-bit relocation against the function it names. Its shape is fixed by the ABI and by what the linker's array pass demands (SharedLinker#split_array_sections): writable and allocatable, entsize 8, 8-byte aligned.

8
ARRAY_ALIGN =
8
ARRAY_SECTION_BASE =
{ init: ".init_array", fini: ".fini_array" }.freeze
ARRAY_SECTION_TYPE =
{ init: SHT_INIT_ARRAY, fini: SHT_FINI_ARRAY }.freeze
ARRAY_KIND_ORDER =

Constructors before destructors, so grouping the entries is deterministic whatever order the caller registered them in.

%i[init fini].freeze
DEFAULT_ARRAY_PRIORITY =

How a priority is spelled. A run-order number below the default goes into its own section named ".NNNNN", which is how the linker learns the order (a priority is nowhere in the section's contents). Both numbers were measured off gcc's own objects rather than assumed: priority 101 emits .init_array.00101, so the field is zero-padded to five digits, and priority 65535 emits the plain, unnumbered .init_array, so 65535 is the default. The five digits are exactly what the linker's array_priority regexp (\.\d+) reads back.

65535
ARRAY_PRIORITY_DIGITS =
5
SYM_ENTSIZE =
24
RELA_ENTSIZE =
24
SHDR_ENTSIZE =
64
EHDR_SIZE =
64

Instance Method Summary collapse

Constructor Details

#initialize(machine: X86_64) ⇒ ELFWriter

machine is the injected MachineDescription selecting the target's e_machine value and relocation-type table; it defaults to x86_64 so an existing caller needs no change.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/rubycc/objfile/elf_writer.rb', line 271

def initialize(machine: X86_64)
  @machine = machine
  @text_bytes = "".b
  @rodata_bytes = nil
  @data_bytes = nil
  @data_align = 1
  @bss_size = 0
  @bss_align = 1
  @file_symbol = nil
  @func_symbols = []
  @object_symbols = []
  # Kept as an Array (in first-added order) because that order feeds the
  # symbol table's layout, and the layout must be deterministic (DESIGN
  # N4: identical input -> identical binary). @undefined_symbol_set
  # mirrors its contents purely for O(1) membership checks, so a large
  # translation unit's undefined-symbol lookups stay linear overall
  # instead of quadratic.
  @undefined_symbols = []
  @undefined_symbol_set = Set.new
  @relocations = []
  @data_relocations = []
  # Constructor/destructor registrations, in registration order (which is
  # the order their slots are laid out within a section, and so the order
  # the runtime calls them in).
  @array_entries = []
end

Instance Method Details

#add_array_entry(kind:, symbol:, priority: DEFAULT_ARRAY_PRIORITY) ⇒ Object

Registers symbol — a function this object defines — as a constructor (kind :init) or a destructor (kind :fini). It becomes one 8-byte slot in the matching array section, filled by an absolute 64-bit relocation against that symbol. priority selects the section: the default goes to the plain ".init_array"/".fini_array", a lower number to its own ".init_array.NNNNN". The symbol may be file-local (a static constructor, which is the common case), since an absolute relocation against a local symbol resolves within the object just as well.

Raises:

  • (ArgumentError)


446
447
448
449
450
451
# File 'lib/rubycc/objfile/elf_writer.rb', line 446

def add_array_entry(kind:, symbol:, priority: DEFAULT_ARRAY_PRIORITY)
  raise ArgumentError, "unknown array kind: #{kind.inspect}" unless ARRAY_SECTION_BASE.key?(kind)

  @array_entries << { kind: kind, priority: priority, symbol: symbol }
  self
end

#add_data_relocation(offset:, symbol:, addend: 0) ⇒ Object

Records an absolute 64-bit reference inside .data to the named file-scope object symbol (a pointer global initialized with "&other", a decayed global array, or a computed address constant like "&arr"). offset is the pointer slot's byte offset within .data and addend the constant byte displacement past the symbol (0 for a bare "&other"). Resolved against that symbol as R_X86_64_64 with that addend.



419
420
421
422
# File 'lib/rubycc/objfile/elf_writer.rb', line 419

def add_data_relocation(offset:, symbol:, addend: 0)
  @data_relocations << { kind: :symbol, offset: offset, symbol: symbol, addend: addend }
  self
end

#add_data_rodata_relocation(offset:, addend:) ⇒ Object

Records an absolute 64-bit reference inside .data into .rodata (a pointer global initialized with a string literal). offset is the pointer slot's byte offset within .data and addend the string's byte offset within .rodata. Resolved against the .rodata section symbol as R_X86_64_64.



428
429
430
431
# File 'lib/rubycc/objfile/elf_writer.rb', line 428

def add_data_rodata_relocation(offset:, addend:)
  @data_relocations << { kind: :rodata, offset: offset, addend: addend }
  self
end

#add_file_symbol(filename) ⇒ Object



433
434
435
436
# File 'lib/rubycc/objfile/elf_writer.rb', line 433

def add_file_symbol(filename)
  @file_symbol = filename
  self
end

#add_func_relocation(offset:, symbol:) ⇒ Object

Records a taken function address in .text (a function pointer value, as opposed to a call site). x86_64 resolves it exactly as a call does, but aarch64 does not — a bl's CALL26 and an adrp/add address pair are different sequences — so the two kinds stay distinct here and the machine description decides whether they coincide.



380
381
382
383
# File 'lib/rubycc/objfile/elf_writer.rb', line 380

def add_func_relocation(offset:, symbol:)
  @relocations << { kind: :func, offset: offset, symbol: symbol }
  self
end

#add_global_func(name, offset, size, visibility: :default) ⇒ Object



329
330
331
332
333
# File 'lib/rubycc/objfile/elf_writer.rb', line 329

def add_global_func(name, offset, size, visibility: :default)
  @func_symbols << { name: name, offset: offset, size: size, bind: STB_GLOBAL,
                      visibility: STV.fetch(visibility, visibility) }
  self
end

#add_global_object(name, section, offset, size, visibility: :default) ⇒ Object

Registers a defined file-scope variable as a global STT_OBJECT symbol. section is :data or :bss, offset its byte offset within that section (st_value) and size its storage width (st_size).



346
347
348
349
350
# File 'lib/rubycc/objfile/elf_writer.rb', line 346

def add_global_object(name, section, offset, size, visibility: :default)
  @object_symbols << { name: name, section: section, offset: offset, size: size,
                       bind: STB_GLOBAL, visibility: STV.fetch(visibility, visibility) }
  self
end

#add_global_relocation(offset:, symbol:) ⇒ Object

Records a PC-relative reference from .text to the named file-scope variable symbol (a "lea rip" displacement addressing a global). Resolved against that symbol as R_X86_64_PC32 with an addend of -4.



398
399
400
401
# File 'lib/rubycc/objfile/elf_writer.rb', line 398

def add_global_relocation(offset:, symbol:)
  @relocations << { kind: :global, offset: offset, symbol: symbol }
  self
end

#add_got_relocation(offset:, symbol:) ⇒ Object

Records a PIC reference from .text to the named symbol's Global Offset Table slot (a "mov rax, sym@GOTPCREL(rip)" that loads the symbol's run-time address). offset is the rel32 field within .text. Resolved against that symbol as R_X86_64_REX_GOTPCRELX with an addend of -4, the same PC-relative bias a "lea rip" uses.



408
409
410
411
# File 'lib/rubycc/objfile/elf_writer.rb', line 408

def add_got_relocation(offset:, symbol:)
  @relocations << { kind: :got, offset: offset, symbol: symbol }
  self
end

#add_local_func(name, offset, size) ⇒ Object

Registers a defined static function as a file-local STT_FUNC symbol (STB_LOCAL): private to this object, so a same-named function elsewhere does not collide with it. Laid out in .text like any other function.



338
339
340
341
# File 'lib/rubycc/objfile/elf_writer.rb', line 338

def add_local_func(name, offset, size)
  @func_symbols << { name: name, offset: offset, size: size, bind: STB_LOCAL }
  self
end

#add_local_object(name, section, offset, size) ⇒ Object

Registers a static file-scope variable (or a block-scope static lowered to a uniquely named one) as a file-local STT_OBJECT symbol (STB_LOCAL). Placed in .data/.bss exactly like a global object.



355
356
357
358
# File 'lib/rubycc/objfile/elf_writer.rb', line 355

def add_local_object(name, section, offset, size)
  @object_symbols << { name: name, section: section, offset: offset, size: size, bind: STB_LOCAL }
  self
end

#add_rodata_relocation(offset:, addend:) ⇒ Object

Records a reference from .text into .rodata: offset is the start of the referring instruction sequence within .text and addend the string's plain byte offset within .rodata, with no target-specific bias applied — the machine description supplies that (see RelocDesc#addend_bias). Resolved against the .rodata section symbol.



390
391
392
393
# File 'lib/rubycc/objfile/elf_writer.rb', line 390

def add_rodata_relocation(offset:, addend:)
  @relocations << { kind: :string, offset: offset, addend: addend }
  self
end

#add_text_relocation(offset:, symbol:) ⇒ Object



370
371
372
373
# File 'lib/rubycc/objfile/elf_writer.rb', line 370

def add_text_relocation(offset:, symbol:)
  @relocations << { kind: :call, offset: offset, symbol: symbol }
  self
end

#add_text_section(bytes) ⇒ Object



298
299
300
301
# File 'lib/rubycc/objfile/elf_writer.rb', line 298

def add_text_section(bytes)
  @text_bytes = bytes.b
  self
end

#add_undefined_symbol(name) ⇒ Object

Registers an external symbol (a call target defined elsewhere). Repeated names collapse to a single symbol so several call sites share one entry.



362
363
364
365
366
367
368
# File 'lib/rubycc/objfile/elf_writer.rb', line 362

def add_undefined_symbol(name)
  unless @undefined_symbol_set.include?(name)
    @undefined_symbols << name
    @undefined_symbol_set << name
  end
  self
end

#set_bss(size, align: 1) ⇒ Object

Sets the .bss size in bytes (the zero-initialized file-scope variables) and the section's alignment. The section, a writable NOBITS occupying no file space, is emitted only when the size is positive.



323
324
325
326
327
# File 'lib/rubycc/objfile/elf_writer.rb', line 323

def set_bss(size, align: 1)
  @bss_size = size
  @bss_align = align
  self
end

#set_data(bytes, align: 1) ⇒ Object

Sets the .data payload (the initialized file-scope variables laid out in order) and the section's alignment. The section, a writable PROGBITS, is emitted only when this is set to a non-empty value.



314
315
316
317
318
# File 'lib/rubycc/objfile/elf_writer.rb', line 314

def set_data(bytes, align: 1)
  @data_bytes = bytes.b
  @data_align = align
  self
end

#set_rodata(bytes) ⇒ Object

Sets the .rodata payload (the NUL-terminated, concatenated string pool). The section, and its section symbol, are emitted only when this is set to a non-empty value.



306
307
308
309
# File 'lib/rubycc/objfile/elf_writer.rb', line 306

def set_rodata(bytes)
  @rodata_bytes = bytes.b
  self
end

#to_binaryObject

Assembles and returns the ELF object as an ASCII-8BIT String.



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/rubycc/objfile/elf_writer.rb', line 454

def to_binary
  # Fixed first: the section name list, the layout and the .rela payloads
  # all read it, and they must agree on one grouping.
  @array_sections = build_array_sections
  @symbols = build_symbol_list
  @section_names = build_section_names
  symbol_indices = index_symbols_by_name(@symbols)
  rodata_sym_index = @symbols.index { |sym| sym[:type] == STT_SECTION && sym[:shndx] == :rodata }

  strtab, sym_name_offsets = build_strtab
  symtab = build_symtab(@symbols, sym_name_offsets)
  rela = relocations? ? build_rela(symbol_indices, rodata_sym_index) : nil
  rela_data = data_relocations? ? build_rela_data(symbol_indices, rodata_sym_index) : nil
  rela_arrays = @array_sections.to_h do |group|
    [group[:name], build_rela_array(group[:entries], symbol_indices, rodata_sym_index)]
  end

  sections = section_layout(symtab: symtab, strtab: strtab, rela: rela, rela_data: rela_data,
                            rela_arrays: rela_arrays)
  assemble(sections)
end