Class: Rubycc::Link::SharedLinker

Inherits:
Object
  • Object
show all
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

ExecutableLinker

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 following add'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 an ldr's scaled immediate). CALL26 is a bl'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 -fPIC output: 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), not lea (its address): the two agree for the word this linker synthesizes, but mov is what the C __cxa_finalize(__dso_handle) — passing a void * 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 completed guard 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

ObjFile::AR_MAGIC

Class Method Summary collapse

Instance Method Summary collapse

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

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

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



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