Class: Rubycc::Link::ExecutableLinker

Inherits:
SharedLinker show all
Defined in:
lib/rubycc/link/executable_linker.rb

Overview

The final-link core for a runnable program: it turns rubycc-compiled relocatable objects into a dynamically-linked, non-PIE ET_EXEC that the kernel and a runtime loader can map and run. Its purpose is narrow — passing mkmf's conftest probes (try_link / try_run) — not producing a general-quality executable, so it takes the simplest correct choices at every fork: a fixed load address, no PIE, and a minimal crt.

It reuses SharedLinker's machinery wholesale — the relocation scan, the import resolution against dependency .sos, the three-segment page-aligned layout, the byte-patching apply engine, and the .dynsym/.hash/.dynamic tables — and only overrides the handful of decisions where an executable differs from a shared object, expressed through SharedLinker's subclass hooks. Those differences are:

* ET_EXEC at a fixed non-PIE base (0x400000). Because the program is mapped
at exactly this address, every internal absolute reference (R_X86_64_64 /
32 / 32S) is resolved to its final value at link time and needs no
R_X86_64_RELATIVE base relocation — the single largest simplification a
non-PIE image buys. External functions and data still bind through the
PLT/GOT with JUMP_SLOT / GLOB_DAT, exactly as in a shared object.

* A PT_INTERP segment naming the dynamic loader, and a synthesized crt
whose _start hands control to libc through __libc_start_main so the C
runtime (TLS, atexit, stdio, the environment) is initialized before main
runs. _start is entered at e_entry and lives at the front of .text.

* libc is a default dependency: _start's call to __libc_start_main makes
the C library a necessary import, so it is added to `needed` unless the
caller already supplied it (mirroring how a compiler driver links libc
implicitly).

* The executable exports nothing — _start and main are reached internally,
never looked up by a loader — so .dynsym holds only the imports.

Output is deterministic (N4): the crt bytes are fixed, the layout and tables inherit SharedLinker's deterministic order, and the recorded DT_NEEDED is the dependency's SONAME string, independent of where libc was found on the host.

Constant Summary collapse

ET_EXEC =

ELF type of a runnable program and the load base a non-PIE executable is mapped at by convention (page-aligned, so the p_vaddr ≡ p_offset (mod page) constraint holds trivially).

2
LOAD_BASE =
0x400000
PT_INTERP =

Program header type naming the dynamic loader to run this executable.

3
GLIBC_INTERP =

The dynamic loaders this recognizes, tried in order; the first that exists on the host is chosen unless the caller names one explicitly.

"/lib64/ld-linux-x86-64.so.2"
MUSL_INTERP =
"/lib/ld-musl-x86_64.so.1"
AARCH64_INTERP =

The aarch64 dynamic loader paths. When cross-linking on an x86_64 host the target loader is not present locally, so — unlike the x86_64 case — the glibc path is used as the canonical on-target location without a host existence check (the emulator/target resolves it through its own sysroot).

That reasoning stops holding the moment aarch64 is the host: on Alpine arm64 the loader is musl's, and naming glibc's would produce an executable nothing can start. So the musl loader is preferred when it is actually present, which can only be true on a musl aarch64 host, and the glibc name remains the answer everywhere else including every cross link.

"/lib/ld-linux-aarch64.so.1"
AARCH64_MUSL_INTERP =
"/lib/ld-musl-aarch64.so.1"
DEFAULT_LIBC_PATHS =

The usual filesystem locations of the C library, consulted (in order) to add libc as a default dependency. Only the SONAME the chosen file carries affects the output, so which path matches does not disturb determinism.

The glibc spellings come first, so a glibc host resolves exactly as it did before musl was added to the list. The musl entries are last and are a different shape on purpose: musl ships its loader and its C library as one file, so the path that answers here is the same MUSL_INTERP used as the program interpreter (Alpine's /usr/lib/libc.so is a symlink to it). There is no libc.so.6 on such a host, which is why every extconf probe that links an executable failed there until this list learned the musl name (measured on Alpine in CI, docs/development/STEPS.md Step 190).

[
  "/lib/x86_64-linux-gnu/libc.so.6",
  "/lib64/libc.so.6",
  "/usr/lib/x86_64-linux-gnu/libc.so.6",
  "/usr/lib/libc.so.6",
  "/lib/libc.so.6",
  "/lib/ld-musl-x86_64.so.1",
  "/usr/lib/libc.so"
].freeze
AARCH64_LIBC_PATHS =

The aarch64 C library locations, including the cross-toolchain sysroot the linker reads the dependency's exports (and SONAME) from when cross-linking. The musl entries mirror what Step 190 added to the x86-64 list above, for the same reason and one architecture later: musl ships its C library and its program interpreter as one file, so there is no libc.so.6 to find and every extconf probe that links an executable failed on Alpine arm64 with "cannot locate the C library" until this list learned the names. That only surfaced once an aarch64 musl suite could be run at all (m4-aarch64-acceptance-4); the x86-64 list had carried its musl entry since Step 190, and nothing copied it across.

The libc.musl-* spelling comes before the loader's own name because it is the stable library name in Alpine images, matching LibraryResolver's preference; both resolve to the same ELF.

[
  "/lib/aarch64-linux-gnu/libc.so.6",
  "/usr/lib/aarch64-linux-gnu/libc.so.6",
  "/usr/aarch64-linux-gnu/lib/libc.so.6",
  "/lib/libc.musl-aarch64.so.1",
  "/usr/lib/libc.musl-aarch64.so.1",
  "/lib/ld-musl-aarch64.so.1"
].freeze
START_CODE =

The synthesized _start, assembled from the System V x86-64 process-startup ABI (not copied from any crt implementation): the loader enters here with argc/argv/envp on the stack and rdx = rtld_fini, and this marshals them into the classic __libc_start_main(main, argc, argv, init, fini, rtld_fini, stack_end) call that transfers control to libc — which initializes the C runtime and eventually calls main. The two operands the linker fills in are main's absolute address (non-PIE, R_X86_64_32) and the PC-relative call to the imported __libc_start_main (R_X86_64_PLT32). Stack alignment tracks the ABI: rsp is 16-byte aligned at entry, so after popping argc it is realigned and re-padded so that %rsp is 16-byte aligned at the call site.

[
  0x31, 0xED,             # xor  ebp, ebp          ; mark the outermost stack frame
  0x49, 0x89, 0xD1,       # mov  r9, rdx           ; r9 = rtld_fini (loader-supplied)
  0x5E,                   # pop  rsi               ; rsi = argc (top of the entry stack)
  0x48, 0x89, 0xE2,       # mov  rdx, rsp          ; rdx = argv (just above argc)
  0x48, 0x83, 0xE4, 0xF0, # and  rsp, -16          ; realign the stack down to 16 bytes
  0x50,                   # push rax               ; padding to preserve 16-byte alignment
  0x54,                   # push rsp               ; stack_end (7th arg, passed on the stack)
  0x45, 0x31, 0xC0,       # xor  r8d, r8d          ; fini = NULL
  0x31, 0xC9,             # xor  ecx, ecx          ; init = NULL
  0xBF, 0, 0, 0, 0,       # mov  edi, main         ; edi = main's absolute address (R_X86_64_32)
  0xE8, 0, 0, 0, 0,       # call __libc_start_main ; through the PLT (R_X86_64_PLT32)
  0xF4                    # hlt                    ; __libc_start_main does not return
].pack("C*").freeze
MAIN_IMM_OFFSET =

Byte offsets of the two operands the linker patches: the mov's imm32 and the call's rel32, each four bytes wide.

21
LIBC_START_REL_OFFSET =
26
AARCH64_START_CODE =

The synthesized aarch64 _start, assembled from the AArch64 process-startup ABI (ARM DDI 0487 encodings; not copied from any crt). The loader enters with argc/argv/envp on the stack and x0 = rtld_fini, and this marshals the classic __libc_start_main(main, argc, argv, init, fini, rtld_fini, stack_end) call in x0-x6: x5 saves rtld_fini before x0 is reloaded with main's address, x1 = argc = [sp], x2 = argv = sp + 8, x6 = stack_end = sp, x3/x4 = init/fini = 0. main's address is formed by an adrp/add pair (a non-PIE image, so the page/lo12 relocations resolve to a fixed address) and the call reaches __libc_start_main through the .plt (CALL26). sp is 16-byte aligned at entry and nothing is pushed, so no realignment is needed.

[
  0xAA0003E5, # mov  x5, x0            ; x5 = rtld_fini (saved before x0 is reused)
  0xD280001D, # mov  x29, #0           ; outermost frame pointer
  0xD280001E, # mov  x30, #0           ; outermost link register
  0xF94003E1, # ldr  x1, [sp]          ; x1 = argc
  0x910023E2, # add  x2, sp, #8        ; x2 = argv
  0x910003E6, # mov  x6, sp            ; x6 = stack_end
  0x90000000, # adrp x0, main          ; x0 = page(main)   (ADR_PREL_PG_HI21)
  0x91000000, # add  x0, x0, :lo12:main; x0 = &main         (ADD_ABS_LO12_NC)
  0xD2800003, # mov  x3, #0            ; init = NULL
  0xD2800004, # mov  x4, #0            ; fini = NULL
  0x94000000, # bl   __libc_start_main ; through the .plt   (CALL26)
  0xD4200000  # brk  #0                ; __libc_start_main does not return
].pack("L<*").freeze
AARCH64_MAIN_ADRP_OFFSET =

Byte offsets of the three operands the linker patches in the aarch64 crt: the adrp and add forming main's address, and the bl to __libc_start_main.

24
AARCH64_MAIN_ADD_OFFSET =

0x18

28
AARCH64_LIBC_START_OFFSET =

0x1c

40

Constants inherited from SharedLinker

SharedLinker::AARCH64_DSO_FINALIZE_CALL_OFFSET, SharedLinker::AARCH64_DSO_FINALIZE_CODE, SharedLinker::AARCH64_DSO_FINALIZE_GOT_ADRP_OFFSET, SharedLinker::AARCH64_DSO_FINALIZE_GOT_LO12_OFFSET, SharedLinker::AARCH64_DSO_FINALIZE_HANDLE_ADD_OFFSET, SharedLinker::AARCH64_DSO_FINALIZE_HANDLE_ADRP_OFFSET, SharedLinker::AARCH64_GOT_RELOC_TYPES, SharedLinker::AARCH64_MAX_PAGE, SharedLinker::AARCH64_SUPPORTED_RELOC_TYPES, SharedLinker::CXA_FINALIZE_SYMBOL, SharedLinker::DF_1_NOW, SharedLinker::DF_BIND_NOW, SharedLinker::DSO_FINALIZER_SYMBOL, SharedLinker::DSO_FINALIZE_CALL_OFFSET, SharedLinker::DSO_FINALIZE_CODE, SharedLinker::DSO_FINALIZE_GOT_OFFSET, SharedLinker::DSO_FINALIZE_HANDLE_OFFSET, SharedLinker::DSO_FINI_ARRAY_SECTION, SharedLinker::DSO_HANDLE_MEMBER, SharedLinker::DSO_HANDLE_SYMBOL, SharedLinker::DT_FINI_ARRAY, SharedLinker::DT_FINI_ARRAYSZ, SharedLinker::DT_FLAGS, SharedLinker::DT_FLAGS_1, SharedLinker::DT_HASH, SharedLinker::DT_INIT_ARRAY, SharedLinker::DT_INIT_ARRAYSZ, SharedLinker::DT_JMPREL, SharedLinker::DT_NEEDED, SharedLinker::DT_NULL, SharedLinker::DT_PLTGOT, SharedLinker::DT_PLTREL, SharedLinker::DT_PLTRELSZ, SharedLinker::DT_RELA, SharedLinker::DT_RELACOUNT, SharedLinker::DT_RELAENT, SharedLinker::DT_RELASZ, SharedLinker::DT_SONAME, SharedLinker::DT_STRSZ, SharedLinker::DT_STRTAB, SharedLinker::DT_SYMENT, SharedLinker::DT_SYMTAB, SharedLinker::DYN_ENTSIZE, SharedLinker::EHDR_SIZE, SharedLinker::ELFCLASS64, SharedLinker::ELFDATA2LSB, SharedLinker::EM_AARCH64, SharedLinker::EM_X86_64, SharedLinker::ET_DYN, SharedLinker::EV_CURRENT, SharedLinker::GOTPLT_RESERVED, SharedLinker::GOT_RELOC_TYPES, SharedLinker::PAGE, SharedLinker::PF_R, SharedLinker::PF_W, SharedLinker::PF_X, SharedLinker::PHDR_SIZE, SharedLinker::PLT_ENTSIZE, SharedLinker::PT_DYNAMIC, SharedLinker::PT_GNU_STACK, SharedLinker::PT_LOAD, SharedLinker::RELA_ENTSIZE, SharedLinker::R_AARCH64_ABS64, SharedLinker::R_AARCH64_ADD_ABS_LO12_NC, SharedLinker::R_AARCH64_ADR_GOT_PAGE, SharedLinker::R_AARCH64_ADR_PREL_PG_HI21, SharedLinker::R_AARCH64_CALL26, SharedLinker::R_AARCH64_GLOB_DAT, SharedLinker::R_AARCH64_JUMP_SLOT, SharedLinker::R_AARCH64_LD64_GOT_LO12_NC, SharedLinker::R_AARCH64_RELATIVE, SharedLinker::R_X86_64_32, SharedLinker::R_X86_64_32S, SharedLinker::R_X86_64_64, SharedLinker::R_X86_64_GLOB_DAT, SharedLinker::R_X86_64_GOTPCREL, SharedLinker::R_X86_64_GOTPCRELX, SharedLinker::R_X86_64_JUMP_SLOT, SharedLinker::R_X86_64_PC32, SharedLinker::R_X86_64_PLT32, SharedLinker::R_X86_64_RELATIVE, SharedLinker::R_X86_64_REX_GOTPCRELX, SharedLinker::SHDR_SIZE, SharedLinker::SHF_ALLOC, SharedLinker::SHF_EXECINSTR, SharedLinker::SHF_WRITE, SharedLinker::SHN_ABS, SharedLinker::SHN_COMMON, SharedLinker::SHN_LORESERVE, SharedLinker::SHN_UNDEF, SharedLinker::SHT_DYNAMIC, SharedLinker::SHT_DYNSYM, SharedLinker::SHT_FINI_ARRAY, SharedLinker::SHT_HASH, SharedLinker::SHT_INIT_ARRAY, SharedLinker::SHT_NOBITS, SharedLinker::SHT_NULL, SharedLinker::SHT_PROGBITS, SharedLinker::SHT_RELA, SharedLinker::SHT_STRTAB, SharedLinker::SHT_SYMTAB, SharedLinker::STB, SharedLinker::STT, SharedLinker::STV, SharedLinker::SYM_ENTSIZE

Constants included from ObjFile

ObjFile::AR_MAGIC

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from SharedLinker

dso_handle_archive, #link

Constructor Details

#initialize(inputs, needed: [], interpreter: nil, libc: :auto) ⇒ ExecutableLinker

Returns a new instance of ExecutableLinker.



192
193
194
195
196
197
198
199
# File 'lib/rubycc/link/executable_linker.rb', line 192

def initialize(inputs, needed: [], interpreter: nil, libc: :auto)
  # SharedLinker#initialize settles the target machine off the first input
  # object before the merge; the crt and the interpreter/libc defaults
  # chosen here all read it.
  super(inputs, needed: needed)
  @interpreter = choose_interpreter(interpreter)
  add_default_libc(libc)
end

Class Method Details

Links inputs (paths or raw ET_REL/ar bytes, as PartialLinker accepts) into an executable, returned as an ASCII-8BIT String. needed lists dependency shared objects to bind imports against; interpreter overrides the dynamic-loader path; libc names the C library (:auto discovers it, nil assumes the caller supplied it through needed, a path uses it).



182
183
184
# File 'lib/rubycc/link/executable_linker.rb', line 182

def link(inputs, needed: [], interpreter: nil, libc: :auto)
  new(inputs, needed: needed, interpreter: interpreter, libc: libc).link
end

Convenience: link and write the executable to path.



187
188
189
# File 'lib/rubycc/link/executable_linker.rb', line 187

def link_to(inputs, path, needed: [], interpreter: nil, libc: :auto)
  File.binwrite(path, link(inputs, needed: needed, interpreter: interpreter, libc: libc))
end