Class: Rubycc::Link::SharedLinker
- Inherits:
-
Object
- Object
- Rubycc::Link::SharedLinker
- Includes:
- ObjFile
- Defined in:
- lib/rubycc/link/shared_linker.rb
Overview
The final-link core that turns an ordered set of relocatable inputs into a
loadable ELF64 shared object (ET_DYN, a .so) for Linux x86_64 or aarch64.
It is the
counterpart of the ld -r static core: where PartialLinker merges inputs
into another ET_REL and only retargets relocations, this stage assigns
load-time virtual addresses, applies the relocations by patching bytes,
and synthesizes the dynamic-linking metadata a runtime loader (glibc's
dlopen, in particular) reads to bind and run the object.
It handles both a self-contained object — one that calls neither libc nor
any other shared library — and one that imports functions and data from
other shared libraries. Every relocation against an internal definition is
resolved within this object; a relocation against an undefined (imported)
symbol is bound through the standard dynamic mechanisms: an external call
goes through a .plt stub whose .got.plt slot the loader fills from a
JUMP_SLOT relocation, and an external data reference goes through a GOT slot
the loader fills from a GLOB_DAT relocation. Each dependency .so that
actually supplies at least one resolved symbol is recorded as a DT_NEEDED
(an --as-needed-style trim); a still-undefined reference is left undefined,
since a shared object may legitimately be completed by the runtime scope.
Binding is eager (BIND_NOW): DF_BIND_NOW / DF_1_NOW ask the loader to
resolve every JUMP_SLOT and GLOB_DAT at load time, so the .plt stub is just
an indirect jmp *slot(%rip) and the lazy-resolution trampoline (the
reserved .got.plt/[2] and PLT) is unneeded.
Pipeline: the inputs are first merged into one ET_REL image by PartialLinker
(reusing its section concatenation, symbol resolution and archive pull-in),
then read back through ELFReader so this stage works from resolved
Section/Symbol/Relocation values. From there it (1) selects the exported and
imported dynamic symbols and resolves the imports against the dependency
.sos; (2) lays the allocatable sections into three page-aligned PT_LOAD
segments by permission — r-x (text + .plt), r-- (rodata + the read-only
dynamic tables), rw- (data + .got/.got.plt + .dynamic) — choosing
p_vaddr == p_offset for every placed section so the p_vaddr ≡ p_offset (mod page) load constraint holds trivially; (3) builds the dynamic tables
(.dynsym/.dynstr, a SysV .hash, .rela.dyn, .rela.plt and .dynamic); and (4)
applies each relocation against the assigned addresses, filling internal GOT
slots (rebased with R_X86_64_RELATIVE), external GOT slots (GLOB_DAT) and
the .got.plt (JUMP_SLOT).
Output is deterministic (N4): sections keep the merged object's order, the
dynamic symbol table follows the merged symbol order (exports then imports),
the hash bucket count is derived from the export count, the relocation tables
are ordered by slot, and no timestamp or address randomness is embedded —
identical inputs yield byte-identical .so output.
Direct Known Subclasses
Defined Under Namespace
Classes: Dependency, Placed
Constant Summary collapse
- PAGE =
0x1000- ELFCLASS64 =
ELF header encodings.
2- ELFDATA2LSB =
1- EV_CURRENT =
1- ET_DYN =
3- EM_X86_64 =
62- EM_AARCH64 =
183- EHDR_SIZE =
64- PHDR_SIZE =
56- SHDR_SIZE =
64- SHT_NULL =
Section header types and the section flag bits that classify a section into its load segment.
0- SHT_PROGBITS =
1- SHT_SYMTAB =
2- SHT_STRTAB =
3- SHT_RELA =
4- SHT_HASH =
5- SHT_DYNAMIC =
6- SHT_NOBITS =
8- SHT_DYNSYM =
11- SHT_INIT_ARRAY =
The initializer/finalizer pointer arrays. Each is an array of function pointers (entsize 8, the target pointer width) the runtime loader calls after mapping the object (init, in order) and before unmapping it (fini, in reverse order); they are ordinary SHF_ALLOC|SHF_WRITE data reached through DT_INIT_ARRAY / DT_FINI_ARRAY rather than through a symbol.
14- SHT_FINI_ARRAY =
15- SHF_WRITE =
0x1- SHF_ALLOC =
0x2- SHF_EXECINSTR =
0x4- PT_LOAD =
Program header types and permission flags.
1- PT_DYNAMIC =
2- PT_GNU_STACK =
0x6474E551- PF_X =
0x1- PF_W =
0x2- PF_R =
0x4- SHN_UNDEF =
Reserved section index for an undefined symbol reference.
0- SHN_LORESERVE =
0xFF00- SHN_ABS =
0xFFF1- SHN_COMMON =
0xFFF2- STB =
Symbol binding/type/visibility encodings for the .dynsym entries.
{ local: 0, global: 1, weak: 2 }.freeze
- STT =
{ notype: 0, object: 1, func: 2, section: 3, file: 4, tls: 6, ifunc: 10 }.freeze
- STV =
{ default: 0, internal: 1, hidden: 2, protected: 3 }.freeze
- R_X86_64_64 =
x86_64 relocation types this stage applies. The GOT-relative family (9/41/42) all address a symbol's GOT slot PC-relatively and are handled alike. Of the dynamic relocations emitted, R_X86_64_RELATIVE (8) rebases an absolute address (an internal GOT slot or an R_X86_64_64 initializer), R_X86_64_GLOB_DAT (6) fills an external data GOT slot, and R_X86_64_JUMP_SLOT (7) fills a .got.plt slot for an external function.
1- R_X86_64_PC32 =
2- R_X86_64_PLT32 =
4- R_X86_64_GOTPCREL =
9- R_X86_64_32 =
10- R_X86_64_32S =
11- R_X86_64_GOTPCRELX =
41- R_X86_64_REX_GOTPCRELX =
42- R_X86_64_RELATIVE =
8- R_X86_64_GLOB_DAT =
6- R_X86_64_JUMP_SLOT =
7- GOT_RELOC_TYPES =
[R_X86_64_GOTPCREL, R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX].freeze
- R_AARCH64_ABS64 =
aarch64 relocation types this stage applies. AArch64 forms an address in two instructions, so an address reference arrives as a pair of static relocations the reader hands over separately: ADR_PREL_PG_HI21 patches an
adrp's 21-bit page immediate and ADD_ABS_LO12_NC the followingadd's 12-bit within-page offset; ADR_GOT_PAGE / LD64_GOT_LO12_NC are the same split addressing a symbol's GOT slot (the second anldr's scaled immediate). CALL26 is abl's 26-bit branch word-displacement. ABS64 is the absolute 64-bit pointer slot, aarch64's spelling of R_X86_64_64. Of the dynamic relocations, R_AARCH64_RELATIVE rebases an absolute address (a PIC image; a non-PIE executable writes it directly), GLOB_DAT fills an external data GOT slot, and JUMP_SLOT fills a .got.plt slot. The numbers are the ELF-for-the-Arm-64-bit-Architecture values, confirmed against real aarch64-linux-gnu tool output. 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- R_AARCH64_GLOB_DAT =
1025- R_AARCH64_JUMP_SLOT =
1026- R_AARCH64_RELATIVE =
1027- AARCH64_GOT_RELOC_TYPES =
[R_AARCH64_ADR_GOT_PAGE, R_AARCH64_LD64_GOT_LO12_NC].freeze
- AARCH64_SUPPORTED_RELOC_TYPES =
[ R_AARCH64_ABS64, R_AARCH64_ADR_PREL_PG_HI21, R_AARCH64_ADD_ABS_LO12_NC, R_AARCH64_CALL26, *AARCH64_GOT_RELOC_TYPES ].freeze
- AARCH64_MAX_PAGE =
The maximum page size used to align load segments and stamp p_align: 4 KiB on x86_64, 64 KiB on aarch64 (matching the target toolchain default, so the image loads on a 64 KiB-page kernel as well as a 4 KiB one). ADRP page arithmetic is always 4 KiB regardless.
0x10000- DT_NULL =
Dynamic array tags emitted into .dynamic.
0- DT_NEEDED =
1- DT_PLTRELSZ =
2- DT_PLTGOT =
3- DT_HASH =
4- DT_STRTAB =
5- DT_SYMTAB =
6- DT_RELA =
7- DT_RELASZ =
8- DT_RELAENT =
9- DT_STRSZ =
10- DT_SYMENT =
11- DT_SONAME =
14- DT_PLTREL =
20- DT_JMPREL =
23- DT_INIT_ARRAY =
The initializer/finalizer array pointers and their sizes in bytes (not element counts), the loader's entry points into .init_array / .fini_array.
25- DT_FINI_ARRAY =
26- DT_INIT_ARRAYSZ =
27- DT_FINI_ARRAYSZ =
28- DT_FLAGS =
30- DT_RELACOUNT =
0x6FFFFFF9- DT_FLAGS_1 =
0x6FFFFFFB- DF_BIND_NOW =
Dynamic flags requesting eager binding.
0x8- DF_1_NOW =
0x1- SYM_ENTSIZE =
24- RELA_ENTSIZE =
24- DYN_ENTSIZE =
16- PLT_ENTSIZE =
Each .plt stub is a 16-byte-aligned entry; a .got.plt reserves three leading slots (spec convention: [0] = &_DYNAMIC, [1]/[2] the lazy-resolver hooks left zero under BIND_NOW) before the per-function slots.
16- GOTPLT_RESERVED =
3- DSO_HANDLE_SYMBOL =
The C runtime's per-object identity word (see #build_dso_handle_object) and the archive member it is delivered in.
"__dso_handle"- DSO_HANDLE_MEMBER =
"__dso_handle.o"- CXA_FINALIZE_SYMBOL =
The other half of that member (see #add_dso_finalizer): the C runtime entry point that drops the handlers registered under a handle, the local symbol naming the synthesized routine that calls it, and the priority-numbered finalizer-array section the routine is reached through. Priority 0 is inside the range the ABI reserves for the implementation, which is exactly what this is, and it puts the slot at the front of the array — the runtime walks .fini_array backwards, so front means last. (A translation unit that spends the reserved priority itself, with
__attribute__((destructor(0))), lands in the same section and, being ahead of the supplier in link order, ends up running after this slot rather than before it. gcc warns about that priority range for exactly this kind of reason; rubycc has no warning channel, so the ordering there is simply implementation-defined.) "__cxa_finalize"- DSO_FINALIZER_SYMBOL =
"__rubycc_dso_finalize"- DSO_FINI_ARRAY_SECTION =
".fini_array.00000"- DSO_FINALIZE_CODE =
The synthesized finalizer for x86_64, assembled from the System V AMD64 instruction encodings (not copied from any crt implementation — R11) to the shape measured by disassembling a
gcc -shared -fPICoutput: test __cxa_finalize's GOT slot for NULL, and only then load the value of __dso_handle into the first argument register and call through the .plt.Both details are the measurement, not a preference. __cxa_finalize is referenced as a WEAK undefined symbol, so a C library that does not supply it leaves the slot zero and the call must not happen; the test reads the GOT slot itself rather than the .plt stub, because the stub would be an address whether or not the symbol resolved. And the argument is
mov(the word's contents), notlea(its address): the two agree for the word this linker synthesizes, butmovis what the C__cxa_finalize(__dso_handle)— passing avoid *variable — means, and an input that defines its own __dso_handle would tell them apart.Three operands the linker fills in: the GOT slot's PC-relative offset (R_X86_64_GOTPCREL, addend -5 because a one-byte immediate follows the displacement), the PC-relative reference to the internal __dso_handle (R_X86_64_PC32, addend -4) and the call to the import (R_X86_64_PLT32, addend -4). rsp is 8 mod 16 on entry, so one push realigns it for the call; rbp is merely the register pushed, not a frame.
What gcc has and this does not is the
completedguard that makes the routine run at most once. gcc needs it because its routine is reachable from both .fini_array and the legacy _fini/DT_FINI path; this linker emits neither _fini nor DT_FINI, so .fini_array is the only way in and the guard would never fire. [ 0x55, # push %rbp ; realign rsp to 16 for the call 0x48, 0x83, 0x3D, 0, 0, 0, 0, 0x00, # cmpq $0, __cxa_finalize@GOTPCREL(%rip) 0x74, 0x0C, # je +12 ; unresolved: nothing to finalize 0x48, 0x8B, 0x3D, 0, 0, 0, 0, # mov __dso_handle(%rip), %rdi 0xE8, 0, 0, 0, 0, # call __cxa_finalize ; through the .plt 0x5D, # pop %rbp 0xC3 # ret ].pack("C*").freeze
- DSO_FINALIZE_GOT_OFFSET =
Byte offsets of the three operands patched in the x86_64 finalizer: the cmp's disp32, the mov's disp32 and the call's rel32.
4- DSO_FINALIZE_HANDLE_OFFSET =
14- DSO_FINALIZE_CALL_OFFSET =
19- AARCH64_DSO_FINALIZE_CODE =
The aarch64 finalizer: the same measured shape in AArch64 encodings (ARM DDI 0487). The GOT slot is loaded into a register and tested with
cbz(aarch64 has no memory-operand compare), then the handle's address is formed by the usual adrp/add pair and its contents loaded into x0 before the .plt call. x29/x30 are saved because the routine calls; the stp/ldp pair also keeps sp 16-byte aligned. [ 0xA9BF7BFD, # stp x29, x30, [sp, #-16]! 0x90000000, # adrp x0, :got:__cxa_finalize (ADR_GOT_PAGE) 0xF9400000, # ldr x0, [x0, #:got_lo12:...] (LD64_GOT_LO12_NC) 0xB40000A0, # cbz x0, +0x14 ; unresolved: nothing to finalize 0x90000001, # adrp x1, __dso_handle (ADR_PREL_PG_HI21) 0x91000021, # add x1, x1, #:lo12:__dso_handle (ADD_ABS_LO12_NC) 0xF9400020, # ldr x0, [x1] ; the handle's value, not its address 0x94000000, # bl __cxa_finalize (CALL26, through the .plt) 0xA8C17BFD, # ldp x29, x30, [sp], #16 0xD65F03C0 # ret ].pack("L<*").freeze
- AARCH64_DSO_FINALIZE_GOT_ADRP_OFFSET =
Byte offsets of the five operands patched in the aarch64 finalizer: the GOT adrp/ldr pair, the __dso_handle adrp/add pair, and the bl.
4- AARCH64_DSO_FINALIZE_GOT_LO12_OFFSET =
8- AARCH64_DSO_FINALIZE_HANDLE_ADRP_OFFSET =
16- AARCH64_DSO_FINALIZE_HANDLE_ADD_OFFSET =
20- AARCH64_DSO_FINALIZE_CALL_OFFSET =
28
Constants included from ObjFile
Class Method Summary collapse
-
.dso_handle_archive(machine) ⇒ Object
The one-member archive supplying __dso_handle for
machine(an ELF e_machine value), memoized per machine: the bytes are deterministic (N4) and every link appends the same archive. -
.link(inputs, needed: [], soname: nil) ⇒ Object
Links
inputs(an ordered array; each element a filesystem path, or the raw bytes of an ET_REL object or an ar archive — the same shapes PartialLinker accepts) into a shared object, returned as an ASCII-8BIT String. -
.link_to(inputs, path, needed: [], soname: nil) ⇒ Object
Convenience: link and write the shared object to
path.
Instance Method Summary collapse
-
#initialize(inputs, needed: [], soname: nil) ⇒ SharedLinker
constructor
A new instance of SharedLinker.
- #link ⇒ Object
Constructor Details
#initialize(inputs, needed: [], soname: nil) ⇒ SharedLinker
Returns a new instance of SharedLinker.
442 443 444 445 446 447 448 449 450 451 452 453 |
# File 'lib/rubycc/link/shared_linker.rb', line 442 def initialize(inputs, needed: [], soname: nil) @inputs = inputs @needed = needed @soname = soname # The target machine is settled before the merge — the synthesized inputs # (the __dso_handle supplier here, the crt in the executable subclass) and # that subclass's interpreter/libc defaults all need it — so it is read # off the first input object's header rather than from the (not-yet-built) # merged reader. #link replaces it with the merged reader's machine, which # agrees with it. @em = detect_machine end |
Class Method Details
.dso_handle_archive(machine) ⇒ Object
The one-member archive supplying __dso_handle for machine (an ELF
e_machine value), memoized per machine: the bytes are deterministic (N4)
and every link appends the same archive.
315 316 317 318 |
# File 'lib/rubycc/link/shared_linker.rb', line 315 def dso_handle_archive(machine) @dso_handle_archives ||= {} @dso_handle_archives[machine] ||= build_dso_handle_archive(machine) end |
.link(inputs, needed: [], soname: nil) ⇒ Object
Links inputs (an ordered array; each element a filesystem path, or the
raw bytes of an ET_REL object or an ar archive — the same shapes
PartialLinker accepts) into a shared object, returned as an ASCII-8BIT
String. needed lists the dependency shared libraries to resolve
imports against (each a .so filesystem path or an already-parsed
ELFReader); soname sets this object's DT_SONAME.
303 304 305 |
# File 'lib/rubycc/link/shared_linker.rb', line 303 def link(inputs, needed: [], soname: nil) new(inputs, needed: needed, soname: soname).link end |
.link_to(inputs, path, needed: [], soname: nil) ⇒ Object
Convenience: link and write the shared object to path.
308 309 310 |
# File 'lib/rubycc/link/shared_linker.rb', line 308 def link_to(inputs, path, needed: [], soname: nil) File.binwrite(path, link(inputs, needed: needed, soname: soname)) end |
Instance Method Details
#link ⇒ Object
455 456 457 458 459 460 461 462 463 464 465 466 |
# File 'lib/rubycc/link/shared_linker.rb', line 455 def link @reader = ELFReader.read(PartialLinker.link(link_inputs)) @em = @reader.machine check_machine! after_merge plan_dynamic_symbols scan_relocations resolve_imports place_sections apply_relocations assemble end |