rubycc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +56 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +52 -0
  5. data/README.md +208 -0
  6. data/data/README.md +117 -0
  7. data/data/r10_corpus_scan.json +4082 -0
  8. data/data/r10_manual_classification.json +5342 -0
  9. data/data/r10_verification_m4a.json +1531 -0
  10. data/data/r10_verification_m4b.json +1818 -0
  11. data/data/r10_verification_m4c.json +1489 -0
  12. data/data/r10_verification_m4d.json +318 -0
  13. data/data/r10_verification_rbs.json +192 -0
  14. data/data/verified_gems.json +397 -0
  15. data/exe/rmake +16 -0
  16. data/exe/rubycc +9 -0
  17. data/exe/rubycc-ar +114 -0
  18. data/exe/rubycc-doctor +14 -0
  19. data/exe/rubycc-pkgconf +8 -0
  20. data/include/float.h +87 -0
  21. data/include/iso646.h +18 -0
  22. data/include/libc/alloca.h +18 -0
  23. data/include/libc/arpa/inet.h +61 -0
  24. data/include/libc/assert.h +43 -0
  25. data/include/libc/dirent.h +72 -0
  26. data/include/libc/dlfcn.h +58 -0
  27. data/include/libc/features.h +205 -0
  28. data/include/libc/glibc/aarch64/ctype.h +108 -0
  29. data/include/libc/glibc/aarch64/endian.h +54 -0
  30. data/include/libc/glibc/aarch64/errno.h +154 -0
  31. data/include/libc/glibc/aarch64/fcntl.h +144 -0
  32. data/include/libc/glibc/aarch64/inttypes.h +182 -0
  33. data/include/libc/glibc/aarch64/limits.h +63 -0
  34. data/include/libc/glibc/aarch64/pthread.h +131 -0
  35. data/include/libc/glibc/aarch64/setjmp.h +84 -0
  36. data/include/libc/glibc/aarch64/stdint.h +174 -0
  37. data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
  38. data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
  39. data/include/libc/glibc/aarch64/sys/select.h +75 -0
  40. data/include/libc/glibc/aarch64/sys/stat.h +136 -0
  41. data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
  42. data/include/libc/glibc/aarch64/sys/time.h +74 -0
  43. data/include/libc/glibc/aarch64/sys/types.h +127 -0
  44. data/include/libc/glibc/aarch64/time.h +113 -0
  45. data/include/libc/glibc/x86_64/ctype.h +108 -0
  46. data/include/libc/glibc/x86_64/endian.h +54 -0
  47. data/include/libc/glibc/x86_64/errno.h +154 -0
  48. data/include/libc/glibc/x86_64/fcntl.h +134 -0
  49. data/include/libc/glibc/x86_64/inttypes.h +182 -0
  50. data/include/libc/glibc/x86_64/limits.h +61 -0
  51. data/include/libc/glibc/x86_64/pthread.h +111 -0
  52. data/include/libc/glibc/x86_64/setjmp.h +82 -0
  53. data/include/libc/glibc/x86_64/stdint.h +163 -0
  54. data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
  55. data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
  56. data/include/libc/glibc/x86_64/sys/select.h +75 -0
  57. data/include/libc/glibc/x86_64/sys/stat.h +130 -0
  58. data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
  59. data/include/libc/glibc/x86_64/sys/time.h +74 -0
  60. data/include/libc/glibc/x86_64/sys/types.h +123 -0
  61. data/include/libc/glibc/x86_64/time.h +113 -0
  62. data/include/libc/grp.h +50 -0
  63. data/include/libc/langinfo.h +126 -0
  64. data/include/libc/link.h +18 -0
  65. data/include/libc/locale.h +83 -0
  66. data/include/libc/math.h +191 -0
  67. data/include/libc/netinet/in.h +126 -0
  68. data/include/libc/netinet/tcp.h +44 -0
  69. data/include/libc/poll.h +46 -0
  70. data/include/libc/pwd.h +58 -0
  71. data/include/libc/regex.h +53 -0
  72. data/include/libc/sched.h +35 -0
  73. data/include/libc/signal.h +204 -0
  74. data/include/libc/stdio.h +157 -0
  75. data/include/libc/stdlib.h +92 -0
  76. data/include/libc/string.h +74 -0
  77. data/include/libc/strings.h +25 -0
  78. data/include/libc/sys/cdefs.h +126 -0
  79. data/include/libc/sys/inotify.h +111 -0
  80. data/include/libc/sys/ioctl.h +36 -0
  81. data/include/libc/sys/mman.h +65 -0
  82. data/include/libc/sys/param.h +41 -0
  83. data/include/libc/sys/resource.h +109 -0
  84. data/include/libc/sys/socket.h +167 -0
  85. data/include/libc/sys/statfs.h +82 -0
  86. data/include/libc/sys/timerfd.h +55 -0
  87. data/include/libc/sys/uio.h +40 -0
  88. data/include/libc/sys/un.h +25 -0
  89. data/include/libc/sys/utsname.h +35 -0
  90. data/include/libc/sys/wait.h +135 -0
  91. data/include/libc/termios.h +179 -0
  92. data/include/libc/unistd.h +194 -0
  93. data/include/stdalign.h +16 -0
  94. data/include/stdarg.h +31 -0
  95. data/include/stdatomic.h +158 -0
  96. data/include/stdbool.h +15 -0
  97. data/include/stdckdint.h +28 -0
  98. data/include/stddef.h +60 -0
  99. data/include/stdnoreturn.h +18 -0
  100. data/include/x86intrin.h +16 -0
  101. data/lib/rubycc/backend/aarch64.rb +1724 -0
  102. data/lib/rubycc/backend/x86_64.rb +1369 -0
  103. data/lib/rubycc/compile_error.rb +36 -0
  104. data/lib/rubycc/compiler.rb +305 -0
  105. data/lib/rubycc/doctor/builder.rb +151 -0
  106. data/lib/rubycc/doctor/cli.rb +190 -0
  107. data/lib/rubycc/doctor/fetcher.rb +84 -0
  108. data/lib/rubycc/doctor/gemfile.rb +115 -0
  109. data/lib/rubycc/doctor/verified_gems.rb +114 -0
  110. data/lib/rubycc/doctor.rb +16 -0
  111. data/lib/rubycc/driver.rb +463 -0
  112. data/lib/rubycc/front/ast.rb +528 -0
  113. data/lib/rubycc/front/constant_evaluator.rb +631 -0
  114. data/lib/rubycc/front/initializer_resolver.rb +592 -0
  115. data/lib/rubycc/front/lexeme_reader.rb +460 -0
  116. data/lib/rubycc/front/lexer.rb +232 -0
  117. data/lib/rubycc/front/parser.rb +4122 -0
  118. data/lib/rubycc/front/token.rb +49 -0
  119. data/lib/rubycc/ir/call_convention.rb +486 -0
  120. data/lib/rubycc/ir/generator.rb +6036 -0
  121. data/lib/rubycc/ir/ir.rb +417 -0
  122. data/lib/rubycc/link/compat_runtime.rb +89 -0
  123. data/lib/rubycc/link/errors.rb +13 -0
  124. data/lib/rubycc/link/executable_linker.rb +405 -0
  125. data/lib/rubycc/link/library_resolver.rb +437 -0
  126. data/lib/rubycc/link/partial_linker.rb +546 -0
  127. data/lib/rubycc/link/shared_linker.rb +1732 -0
  128. data/lib/rubycc/mkmf_shim.rb +83 -0
  129. data/lib/rubycc/objfile/ar_archive.rb +437 -0
  130. data/lib/rubycc/objfile/elf_reader.rb +646 -0
  131. data/lib/rubycc/objfile/elf_writer.rb +891 -0
  132. data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
  133. data/lib/rubycc/pkgconf/cli.rb +139 -0
  134. data/lib/rubycc/pkgconf/errors.rb +39 -0
  135. data/lib/rubycc/pkgconf/model.rb +28 -0
  136. data/lib/rubycc/pkgconf/parser.rb +109 -0
  137. data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
  138. data/lib/rubycc/pkgconf/resolver.rb +70 -0
  139. data/lib/rubycc/pkgconf/search_path.rb +38 -0
  140. data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
  141. data/lib/rubycc/preprocess/constant_expression.rb +219 -0
  142. data/lib/rubycc/preprocess/glibc_version.rb +124 -0
  143. data/lib/rubycc/preprocess/pp_token.rb +62 -0
  144. data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
  145. data/lib/rubycc/preprocess/scanner.rb +290 -0
  146. data/lib/rubycc/preprocess/token_converter.rb +157 -0
  147. data/lib/rubycc/rmake/cli.rb +143 -0
  148. data/lib/rubycc/rmake/errors.rb +71 -0
  149. data/lib/rubycc/rmake/executor.rb +818 -0
  150. data/lib/rubycc/rmake/expander.rb +251 -0
  151. data/lib/rubycc/rmake/makefile.rb +352 -0
  152. data/lib/rubycc/rmake/model.rb +137 -0
  153. data/lib/rubycc/rmake/parser.rb +226 -0
  154. data/lib/rubycc/rmake/rmake.rb +15 -0
  155. data/lib/rubycc/type.rb +1236 -0
  156. data/lib/rubycc/version.rb +5 -0
  157. data/lib/rubycc.rb +36 -0
  158. data/lib/rubygems_plugin.rb +102 -0
  159. metadata +219 -0
@@ -0,0 +1,1732 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module Link
5
+ # The final-link core that turns an ordered set of relocatable inputs into a
6
+ # loadable ELF64 shared object (ET_DYN, a `.so`) for Linux x86_64 or aarch64.
7
+ # It is the
8
+ # counterpart of the `ld -r` static core: where PartialLinker merges inputs
9
+ # into another ET_REL and only *retargets* relocations, this stage assigns
10
+ # load-time virtual addresses, *applies* the relocations by patching bytes,
11
+ # and synthesizes the dynamic-linking metadata a runtime loader (glibc's
12
+ # dlopen, in particular) reads to bind and run the object.
13
+ #
14
+ # It handles both a self-contained object — one that calls neither libc nor
15
+ # any other shared library — and one that imports functions and data from
16
+ # other shared libraries. Every relocation against an internal definition is
17
+ # resolved within this object; a relocation against an *undefined* (imported)
18
+ # symbol is bound through the standard dynamic mechanisms: an external call
19
+ # goes through a .plt stub whose .got.plt slot the loader fills from a
20
+ # JUMP_SLOT relocation, and an external data reference goes through a GOT slot
21
+ # the loader fills from a GLOB_DAT relocation. Each dependency `.so` that
22
+ # actually supplies at least one resolved symbol is recorded as a DT_NEEDED
23
+ # (an --as-needed-style trim); a still-undefined reference is left undefined,
24
+ # since a shared object may legitimately be completed by the runtime scope.
25
+ #
26
+ # Binding is eager (BIND_NOW): DF_BIND_NOW / DF_1_NOW ask the loader to
27
+ # resolve every JUMP_SLOT and GLOB_DAT at load time, so the .plt stub is just
28
+ # an indirect `jmp *slot(%rip)` and the lazy-resolution trampoline (the
29
+ # reserved .got.plt[1]/[2] and PLT[0]) is unneeded.
30
+ #
31
+ # Pipeline: the inputs are first merged into one ET_REL image by PartialLinker
32
+ # (reusing its section concatenation, symbol resolution and archive pull-in),
33
+ # then read back through ELFReader so this stage works from resolved
34
+ # Section/Symbol/Relocation values. From there it (1) selects the exported and
35
+ # imported dynamic symbols and resolves the imports against the dependency
36
+ # `.so`s; (2) lays the allocatable sections into three page-aligned PT_LOAD
37
+ # segments by permission — r-x (text + .plt), r-- (rodata + the read-only
38
+ # dynamic tables), rw- (data + .got/.got.plt + .dynamic) — choosing
39
+ # `p_vaddr == p_offset` for every placed section so the `p_vaddr ≡ p_offset
40
+ # (mod page)` load constraint holds trivially; (3) builds the dynamic tables
41
+ # (.dynsym/.dynstr, a SysV .hash, .rela.dyn, .rela.plt and .dynamic); and (4)
42
+ # applies each relocation against the assigned addresses, filling internal GOT
43
+ # slots (rebased with R_X86_64_RELATIVE), external GOT slots (GLOB_DAT) and
44
+ # the .got.plt (JUMP_SLOT).
45
+ #
46
+ # Output is deterministic (N4): sections keep the merged object's order, the
47
+ # dynamic symbol table follows the merged symbol order (exports then imports),
48
+ # the hash bucket count is derived from the export count, the relocation tables
49
+ # are ordered by slot, and no timestamp or address randomness is embedded —
50
+ # identical inputs yield byte-identical `.so` output.
51
+ class SharedLinker
52
+ include ObjFile
53
+
54
+ PAGE = 0x1000
55
+
56
+ # ELF header encodings.
57
+ ELFCLASS64 = 2
58
+ ELFDATA2LSB = 1
59
+ EV_CURRENT = 1
60
+ ET_DYN = 3
61
+ EM_X86_64 = 62
62
+ EM_AARCH64 = 183
63
+ EHDR_SIZE = 64
64
+ PHDR_SIZE = 56
65
+ SHDR_SIZE = 64
66
+
67
+ # Section header types and the section flag bits that classify a section
68
+ # into its load segment.
69
+ SHT_NULL = 0
70
+ SHT_PROGBITS = 1
71
+ SHT_SYMTAB = 2
72
+ SHT_STRTAB = 3
73
+ SHT_RELA = 4
74
+ SHT_HASH = 5
75
+ SHT_DYNAMIC = 6
76
+ SHT_NOBITS = 8
77
+ SHT_DYNSYM = 11
78
+ # The initializer/finalizer pointer arrays. Each is an array of function
79
+ # pointers (entsize 8, the target pointer width) the runtime loader calls
80
+ # after mapping the object (init, in order) and before unmapping it (fini,
81
+ # in reverse order); they are ordinary SHF_ALLOC|SHF_WRITE data reached
82
+ # through DT_INIT_ARRAY / DT_FINI_ARRAY rather than through a symbol.
83
+ SHT_INIT_ARRAY = 14
84
+ SHT_FINI_ARRAY = 15
85
+
86
+ SHF_WRITE = 0x1
87
+ SHF_ALLOC = 0x2
88
+ SHF_EXECINSTR = 0x4
89
+
90
+ # Program header types and permission flags.
91
+ PT_LOAD = 1
92
+ PT_DYNAMIC = 2
93
+ PT_GNU_STACK = 0x6474E551
94
+ PF_X = 0x1
95
+ PF_W = 0x2
96
+ PF_R = 0x4
97
+
98
+ # Reserved section index for an undefined symbol reference.
99
+ SHN_UNDEF = 0
100
+ SHN_LORESERVE = 0xFF00
101
+ SHN_ABS = 0xFFF1
102
+ SHN_COMMON = 0xFFF2
103
+
104
+ # Symbol binding/type/visibility encodings for the .dynsym entries.
105
+ STB = { local: 0, global: 1, weak: 2 }.freeze
106
+ STT = { notype: 0, object: 1, func: 2, section: 3, file: 4, tls: 6, ifunc: 10 }.freeze
107
+ STV = { default: 0, internal: 1, hidden: 2, protected: 3 }.freeze
108
+
109
+ # x86_64 relocation types this stage applies. The GOT-relative family
110
+ # (9/41/42) all address a symbol's GOT slot PC-relatively and are handled
111
+ # alike. Of the *dynamic* relocations emitted, R_X86_64_RELATIVE (8) rebases
112
+ # an absolute address (an internal GOT slot or an R_X86_64_64 initializer),
113
+ # R_X86_64_GLOB_DAT (6) fills an external data GOT slot, and
114
+ # R_X86_64_JUMP_SLOT (7) fills a .got.plt slot for an external function.
115
+ R_X86_64_64 = 1
116
+ R_X86_64_PC32 = 2
117
+ R_X86_64_PLT32 = 4
118
+ R_X86_64_GOTPCREL = 9
119
+ R_X86_64_32 = 10
120
+ R_X86_64_32S = 11
121
+ R_X86_64_GOTPCRELX = 41
122
+ R_X86_64_REX_GOTPCRELX = 42
123
+ R_X86_64_RELATIVE = 8
124
+ R_X86_64_GLOB_DAT = 6
125
+ R_X86_64_JUMP_SLOT = 7
126
+ GOT_RELOC_TYPES = [R_X86_64_GOTPCREL, R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX].freeze
127
+
128
+ # aarch64 relocation types this stage applies. AArch64 forms an address in
129
+ # two instructions, so an address reference arrives as a *pair* of static
130
+ # relocations the reader hands over separately: ADR_PREL_PG_HI21 patches an
131
+ # `adrp`'s 21-bit page immediate and ADD_ABS_LO12_NC the following `add`'s
132
+ # 12-bit within-page offset; ADR_GOT_PAGE / LD64_GOT_LO12_NC are the same
133
+ # split addressing a symbol's GOT slot (the second an `ldr`'s scaled
134
+ # immediate). CALL26 is a `bl`'s 26-bit branch word-displacement. ABS64 is
135
+ # the absolute 64-bit pointer slot, aarch64's spelling of R_X86_64_64. Of
136
+ # the *dynamic* relocations, R_AARCH64_RELATIVE rebases an absolute address
137
+ # (a PIC image; a non-PIE executable writes it directly), GLOB_DAT fills an
138
+ # external data GOT slot, and JUMP_SLOT fills a .got.plt slot. The numbers
139
+ # are the ELF-for-the-Arm-64-bit-Architecture values, confirmed against real
140
+ # aarch64-linux-gnu tool output.
141
+ R_AARCH64_ABS64 = 257
142
+ R_AARCH64_ADR_PREL_PG_HI21 = 275
143
+ R_AARCH64_ADD_ABS_LO12_NC = 277
144
+ R_AARCH64_CALL26 = 283
145
+ R_AARCH64_ADR_GOT_PAGE = 311
146
+ R_AARCH64_LD64_GOT_LO12_NC = 312
147
+ R_AARCH64_GLOB_DAT = 1025
148
+ R_AARCH64_JUMP_SLOT = 1026
149
+ R_AARCH64_RELATIVE = 1027
150
+ AARCH64_GOT_RELOC_TYPES = [R_AARCH64_ADR_GOT_PAGE, R_AARCH64_LD64_GOT_LO12_NC].freeze
151
+ AARCH64_SUPPORTED_RELOC_TYPES = [
152
+ R_AARCH64_ABS64, R_AARCH64_ADR_PREL_PG_HI21, R_AARCH64_ADD_ABS_LO12_NC,
153
+ R_AARCH64_CALL26, *AARCH64_GOT_RELOC_TYPES
154
+ ].freeze
155
+
156
+ # The maximum page size used to align load segments and stamp p_align: 4 KiB
157
+ # on x86_64, 64 KiB on aarch64 (matching the target toolchain default, so
158
+ # the image loads on a 64 KiB-page kernel as well as a 4 KiB one). ADRP page
159
+ # arithmetic is always 4 KiB regardless.
160
+ AARCH64_MAX_PAGE = 0x10000
161
+
162
+ # Dynamic array tags emitted into .dynamic.
163
+ DT_NULL = 0
164
+ DT_NEEDED = 1
165
+ DT_PLTRELSZ = 2
166
+ DT_PLTGOT = 3
167
+ DT_HASH = 4
168
+ DT_STRTAB = 5
169
+ DT_SYMTAB = 6
170
+ DT_RELA = 7
171
+ DT_RELASZ = 8
172
+ DT_RELAENT = 9
173
+ DT_STRSZ = 10
174
+ DT_SYMENT = 11
175
+ DT_SONAME = 14
176
+ DT_PLTREL = 20
177
+ DT_JMPREL = 23
178
+ # The initializer/finalizer array pointers and their sizes *in bytes*
179
+ # (not element counts), the loader's entry points into .init_array /
180
+ # .fini_array.
181
+ DT_INIT_ARRAY = 25
182
+ DT_FINI_ARRAY = 26
183
+ DT_INIT_ARRAYSZ = 27
184
+ DT_FINI_ARRAYSZ = 28
185
+ DT_FLAGS = 30
186
+ DT_RELACOUNT = 0x6FFFFFF9
187
+ DT_FLAGS_1 = 0x6FFFFFFB
188
+
189
+ # Dynamic flags requesting eager binding.
190
+ DF_BIND_NOW = 0x8
191
+ DF_1_NOW = 0x1
192
+
193
+ SYM_ENTSIZE = 24
194
+ RELA_ENTSIZE = 24
195
+ DYN_ENTSIZE = 16
196
+
197
+ # Each .plt stub is a 16-byte-aligned entry; a .got.plt reserves three
198
+ # leading slots (spec convention: [0] = &_DYNAMIC, [1]/[2] the lazy-resolver
199
+ # hooks left zero under BIND_NOW) before the per-function slots.
200
+ PLT_ENTSIZE = 16
201
+ GOTPLT_RESERVED = 3
202
+
203
+ # The C runtime's per-object identity word (see #build_dso_handle_object)
204
+ # and the archive member it is delivered in.
205
+ DSO_HANDLE_SYMBOL = "__dso_handle"
206
+ DSO_HANDLE_MEMBER = "__dso_handle.o"
207
+
208
+ # The other half of that member (see #add_dso_finalizer): the C runtime
209
+ # entry point that drops the handlers registered under a handle, the local
210
+ # symbol naming the synthesized routine that calls it, and the
211
+ # priority-numbered finalizer-array section the routine is reached through.
212
+ # Priority 0 is inside the range the ABI reserves for the implementation,
213
+ # which is exactly what this is, and it puts the slot at the *front* of the
214
+ # array — the runtime walks .fini_array backwards, so front means last.
215
+ # (A translation unit that spends the reserved priority itself, with
216
+ # `__attribute__((destructor(0)))`, lands in the same section and, being
217
+ # ahead of the supplier in link order, ends up running *after* this slot
218
+ # rather than before it. gcc warns about that priority range for exactly
219
+ # this kind of reason; rubycc has no warning channel, so the ordering there
220
+ # is simply implementation-defined.)
221
+ CXA_FINALIZE_SYMBOL = "__cxa_finalize"
222
+ DSO_FINALIZER_SYMBOL = "__rubycc_dso_finalize"
223
+ DSO_FINI_ARRAY_SECTION = ".fini_array.00000"
224
+
225
+ # The synthesized finalizer for x86_64, assembled from the System V AMD64
226
+ # instruction encodings (not copied from any crt implementation — R11) to
227
+ # the shape measured by disassembling a `gcc -shared -fPIC` output: test
228
+ # __cxa_finalize's GOT slot for NULL, and only then load the *value* of
229
+ # __dso_handle into the first argument register and call through the .plt.
230
+ #
231
+ # Both details are the measurement, not a preference. __cxa_finalize is
232
+ # referenced as a WEAK undefined symbol, so a C library that does not
233
+ # supply it leaves the slot zero and the call must not happen; the test
234
+ # reads the GOT slot itself rather than the .plt stub, because the stub
235
+ # would be an address whether or not the symbol resolved. And the argument
236
+ # is `mov` (the word's contents), not `lea` (its address): the two agree
237
+ # for the word this linker synthesizes, but `mov` is what the C
238
+ # `__cxa_finalize(__dso_handle)` — passing a `void *` variable — means, and
239
+ # an input that defines its own __dso_handle would tell them apart.
240
+ #
241
+ # Three operands the linker fills in: the GOT slot's PC-relative offset
242
+ # (R_X86_64_GOTPCREL, addend -5 because a one-byte immediate follows the
243
+ # displacement), the PC-relative reference to the internal __dso_handle
244
+ # (R_X86_64_PC32, addend -4) and the call to the import (R_X86_64_PLT32,
245
+ # addend -4). rsp is 8 mod 16 on entry, so one push realigns it for the
246
+ # call; rbp is merely the register pushed, not a frame.
247
+ #
248
+ # What gcc has and this does not is the `completed` guard that makes the
249
+ # routine run at most once. gcc needs it because its routine is reachable
250
+ # from both .fini_array and the legacy _fini/DT_FINI path; this linker
251
+ # emits neither _fini nor DT_FINI, so .fini_array is the only way in and
252
+ # the guard would never fire.
253
+ DSO_FINALIZE_CODE = [
254
+ 0x55, # push %rbp ; realign rsp to 16 for the call
255
+ 0x48, 0x83, 0x3D, 0, 0, 0, 0, 0x00, # cmpq $0, __cxa_finalize@GOTPCREL(%rip)
256
+ 0x74, 0x0C, # je +12 ; unresolved: nothing to finalize
257
+ 0x48, 0x8B, 0x3D, 0, 0, 0, 0, # mov __dso_handle(%rip), %rdi
258
+ 0xE8, 0, 0, 0, 0, # call __cxa_finalize ; through the .plt
259
+ 0x5D, # pop %rbp
260
+ 0xC3 # ret
261
+ ].pack("C*").freeze
262
+
263
+ # Byte offsets of the three operands patched in the x86_64 finalizer: the
264
+ # cmp's disp32, the mov's disp32 and the call's rel32.
265
+ DSO_FINALIZE_GOT_OFFSET = 4
266
+ DSO_FINALIZE_HANDLE_OFFSET = 14
267
+ DSO_FINALIZE_CALL_OFFSET = 19
268
+
269
+ # The aarch64 finalizer: the same measured shape in AArch64 encodings (ARM
270
+ # DDI 0487). The GOT slot is loaded into a register and tested with `cbz`
271
+ # (aarch64 has no memory-operand compare), then the handle's address is
272
+ # formed by the usual adrp/add pair and its *contents* loaded into x0
273
+ # before the .plt call. x29/x30 are saved because the routine calls; the
274
+ # stp/ldp pair also keeps sp 16-byte aligned.
275
+ AARCH64_DSO_FINALIZE_CODE = [
276
+ 0xA9BF7BFD, # stp x29, x30, [sp, #-16]!
277
+ 0x90000000, # adrp x0, :got:__cxa_finalize (ADR_GOT_PAGE)
278
+ 0xF9400000, # ldr x0, [x0, #:got_lo12:...] (LD64_GOT_LO12_NC)
279
+ 0xB40000A0, # cbz x0, +0x14 ; unresolved: nothing to finalize
280
+ 0x90000001, # adrp x1, __dso_handle (ADR_PREL_PG_HI21)
281
+ 0x91000021, # add x1, x1, #:lo12:__dso_handle (ADD_ABS_LO12_NC)
282
+ 0xF9400020, # ldr x0, [x1] ; the handle's value, not its address
283
+ 0x94000000, # bl __cxa_finalize (CALL26, through the .plt)
284
+ 0xA8C17BFD, # ldp x29, x30, [sp], #16
285
+ 0xD65F03C0 # ret
286
+ ].pack("L<*").freeze
287
+
288
+ # Byte offsets of the five operands patched in the aarch64 finalizer: the
289
+ # GOT adrp/ldr pair, the __dso_handle adrp/add pair, and the bl.
290
+ AARCH64_DSO_FINALIZE_GOT_ADRP_OFFSET = 4
291
+ AARCH64_DSO_FINALIZE_GOT_LO12_OFFSET = 8
292
+ AARCH64_DSO_FINALIZE_HANDLE_ADRP_OFFSET = 16
293
+ AARCH64_DSO_FINALIZE_HANDLE_ADD_OFFSET = 20
294
+ AARCH64_DSO_FINALIZE_CALL_OFFSET = 28
295
+
296
+ class << self
297
+ # Links `inputs` (an ordered array; each element a filesystem path, or the
298
+ # raw bytes of an ET_REL object or an ar archive — the same shapes
299
+ # PartialLinker accepts) into a shared object, returned as an ASCII-8BIT
300
+ # String. `needed` lists the dependency shared libraries to resolve
301
+ # imports against (each a `.so` filesystem path or an already-parsed
302
+ # ELFReader); `soname` sets this object's DT_SONAME.
303
+ def link(inputs, needed: [], soname: nil)
304
+ new(inputs, needed: needed, soname: soname).link
305
+ end
306
+
307
+ # Convenience: link and write the shared object to `path`.
308
+ def link_to(inputs, path, needed: [], soname: nil)
309
+ File.binwrite(path, link(inputs, needed: needed, soname: soname))
310
+ end
311
+
312
+ # The one-member archive supplying __dso_handle for `machine` (an ELF
313
+ # e_machine value), memoized per machine: the bytes are deterministic (N4)
314
+ # and every link appends the same archive.
315
+ def dso_handle_archive(machine)
316
+ @dso_handle_archives ||= {}
317
+ @dso_handle_archives[machine] ||= build_dso_handle_archive(machine)
318
+ end
319
+
320
+ private
321
+
322
+ def build_dso_handle_archive(machine)
323
+ writer = ObjFile::ArWriter.new
324
+ writer.add_member(DSO_HANDLE_MEMBER, build_dso_handle_object(machine))
325
+ writer.to_binary
326
+ end
327
+
328
+ # The __dso_handle supplier as a small ET_REL object: an 8-byte .data
329
+ # word, a hidden global OBJECT symbol naming it, and an absolute-64
330
+ # relocation of the word against that same symbol — so the word ends up
331
+ # holding its own address — plus the finalizer that hands the word back
332
+ # to the C runtime when the object is unloaded (#add_dso_finalizer).
333
+ #
334
+ # Why this shape, measured from a `gcc -shared -fPIC` output rather than
335
+ # copied from any crt implementation (R11): __dso_handle is the opaque
336
+ # per-DSO cookie passed to __cxa_atexit / __register_atfork to say which
337
+ # object registered a handler, so it must be an address unique to this
338
+ # object (zero would claim to be the main program and would make
339
+ # __cxa_finalize at dlclose skip — or wrongly run — the handlers). gcc's
340
+ # `.so` shows exactly that: an 8-byte .data word at the section start
341
+ # holding its own link-time address, carrying one R_X86_64_RELATIVE whose
342
+ # offset and addend are both that address, and reachable only internally
343
+ # — the symbol never enters .dynsym, so no other object can interpose it.
344
+ # Hidden visibility is what produces that: it is the binding the
345
+ # referencing member itself uses (glibc's libc_nonshared.a members name
346
+ # __dso_handle as a GLOBAL HIDDEN undefined), it keeps the definition out
347
+ # of .dynsym (#plan_dynamic_symbols exports only default/protected
348
+ # visibility), and unlike a local symbol it can still resolve another
349
+ # object's undefined reference during the merge.
350
+ #
351
+ # The self-reference is expressed as an ordinary absolute-64 relocation,
352
+ # so the existing apply engine produces the rest for free: an internal
353
+ # R_X86_64_64 / R_AARCH64_ABS64 is patched with the final address and — in
354
+ # a shared object, where #rebase_internal? holds — rebased at load time by
355
+ # one RELATIVE entry, which is precisely the measured gcc output.
356
+ #
357
+ # The word and its finalizer travel together in one member because they
358
+ # answer one question between them — "which object is this, and when is
359
+ # it gone" — and because the condition for needing them is the same: a
360
+ # link that never mentions __dso_handle never extracts the member and so
361
+ # gains neither. That is the same pairing gcc's crt file has, reached
362
+ # without a second lazily-extracted member or any linker-side special
363
+ # case.
364
+ def build_dso_handle_object(machine)
365
+ aarch64 = machine == EM_AARCH64
366
+ writer = ObjFile::RelocatableWriter.new(machine: machine)
367
+ data = writer.add_section(name: ".data", type: SHT_PROGBITS,
368
+ flags: SHF_ALLOC | SHF_WRITE, addralign: 8,
369
+ data: "\0".b * 8)
370
+ handle = writer.add_symbol(name: DSO_HANDLE_SYMBOL, bind: :global, type: :object,
371
+ visibility: :hidden, section: data, value: 0, size: 8)
372
+ writer.add_relocation(target: data, offset: 0, symbol: handle,
373
+ type: aarch64 ? R_AARCH64_ABS64 : R_X86_64_64, addend: 0)
374
+ add_dso_finalizer(writer, handle, aarch64)
375
+ writer.to_binary
376
+ end
377
+
378
+ # Adds the finalizer half to the supplier object: the routine itself in
379
+ # .text (see DSO_FINALIZE_CODE for its shape and why it is that shape),
380
+ # the operand relocations, and the one-slot .fini_array.00000 section
381
+ # pointing at it.
382
+ #
383
+ # This is what makes glibc drop the handlers an object registered under
384
+ # its handle — __cxa_atexit / __register_atfork record the handle as the
385
+ # owner, and __cxa_finalize(handle) is the call that runs and unregisters
386
+ # them. Without it a dlclosed object leaves its handlers pointing into
387
+ # the unmapped image (measured before this existed: calling
388
+ # __cxa_finalize by hand cleared them, so only the automatic call was
389
+ # missing).
390
+ #
391
+ # The routine is a *local* symbol: it is reached only from this object's
392
+ # own array slot, so it never needs to be visible to another object or to
393
+ # the loader, and a local definition cannot collide with an input's.
394
+ # __cxa_finalize is referenced WEAK so that a C library without it leaves
395
+ # the reference unresolved instead of failing the link — the NULL test in
396
+ # the code is the other half of that decision.
397
+ def add_dso_finalizer(writer, handle, aarch64)
398
+ code = aarch64 ? AARCH64_DSO_FINALIZE_CODE : DSO_FINALIZE_CODE
399
+ text = writer.add_section(name: ".text", type: SHT_PROGBITS,
400
+ flags: SHF_ALLOC | SHF_EXECINSTR, addralign: 16,
401
+ data: code)
402
+ routine = writer.add_symbol(name: DSO_FINALIZER_SYMBOL, bind: :local, type: :func,
403
+ section: text, value: 0, size: code.bytesize)
404
+ # The argument is the object's own definition of __dso_handle (the
405
+ # `handle` symbol the caller passes in), so that reference is internal
406
+ # and resolves without a text relocation even in a shared object; only
407
+ # the callee is an import.
408
+ cxa = writer.add_symbol(name: CXA_FINALIZE_SYMBOL, bind: :weak, type: :func)
409
+ if aarch64
410
+ add_dso_finalizer_relocations_aarch64(writer, text, handle, cxa)
411
+ else
412
+ add_dso_finalizer_relocations_x86_64(writer, text, handle, cxa)
413
+ end
414
+
415
+ slot = writer.add_section(name: DSO_FINI_ARRAY_SECTION, type: SHT_FINI_ARRAY,
416
+ flags: SHF_ALLOC | SHF_WRITE, addralign: 8, entsize: 8,
417
+ data: "\0".b * 8)
418
+ writer.add_relocation(target: slot, offset: 0, symbol: routine,
419
+ type: aarch64 ? R_AARCH64_ABS64 : R_X86_64_64, addend: 0)
420
+ end
421
+
422
+ def add_dso_finalizer_relocations_x86_64(writer, text, handle, cxa)
423
+ writer.add_relocation(target: text, offset: DSO_FINALIZE_GOT_OFFSET, symbol: cxa,
424
+ type: R_X86_64_GOTPCREL, addend: -5)
425
+ writer.add_relocation(target: text, offset: DSO_FINALIZE_HANDLE_OFFSET, symbol: handle,
426
+ type: R_X86_64_PC32, addend: -4)
427
+ writer.add_relocation(target: text, offset: DSO_FINALIZE_CALL_OFFSET, symbol: cxa,
428
+ type: R_X86_64_PLT32, addend: -4)
429
+ end
430
+
431
+ def add_dso_finalizer_relocations_aarch64(writer, text, handle, cxa)
432
+ [[AARCH64_DSO_FINALIZE_GOT_ADRP_OFFSET, cxa, R_AARCH64_ADR_GOT_PAGE],
433
+ [AARCH64_DSO_FINALIZE_GOT_LO12_OFFSET, cxa, R_AARCH64_LD64_GOT_LO12_NC],
434
+ [AARCH64_DSO_FINALIZE_HANDLE_ADRP_OFFSET, handle, R_AARCH64_ADR_PREL_PG_HI21],
435
+ [AARCH64_DSO_FINALIZE_HANDLE_ADD_OFFSET, handle, R_AARCH64_ADD_ABS_LO12_NC],
436
+ [AARCH64_DSO_FINALIZE_CALL_OFFSET, cxa, R_AARCH64_CALL26]].each do |offset, sym, type|
437
+ writer.add_relocation(target: text, offset: offset, symbol: sym, type: type, addend: 0)
438
+ end
439
+ end
440
+ end
441
+
442
+ def initialize(inputs, needed: [], soname: nil)
443
+ @inputs = inputs
444
+ @needed = needed
445
+ @soname = soname
446
+ # The target machine is settled before the merge — the synthesized inputs
447
+ # (the __dso_handle supplier here, the crt in the executable subclass) and
448
+ # that subclass's interpreter/libc defaults all need it — so it is read
449
+ # off the first input object's header rather than from the (not-yet-built)
450
+ # merged reader. #link replaces it with the merged reader's machine, which
451
+ # agrees with it.
452
+ @em = detect_machine
453
+ end
454
+
455
+ def link
456
+ @reader = ELFReader.read(PartialLinker.link(link_inputs))
457
+ @em = @reader.machine
458
+ check_machine!
459
+ after_merge
460
+ plan_dynamic_symbols
461
+ scan_relocations
462
+ resolve_imports
463
+ place_sections
464
+ apply_relocations
465
+ assemble
466
+ end
467
+
468
+ private
469
+
470
+ # --- subclass hooks ----------------------------------------------------
471
+ # The final-link core is shared with the executable writer (ExecutableLinker),
472
+ # which differs from a shared object only in a handful of decisions expressed
473
+ # through these hooks. A shared object links the given inputs and the
474
+ # __dso_handle supplier below (an executable prepends a synthesized crt on
475
+ # top of them), needs no post-merge validation,
476
+ # loads at virtual address 0 so p_vaddr == p_offset (an executable loads at a
477
+ # fixed non-PIE base), rebases every internal absolute address at load time
478
+ # through R_X86_64_RELATIVE (an executable, mapped at a fixed address, writes
479
+ # the final address directly and needs no base relocation), places no section
480
+ # ahead of .text (an executable places .interp), and is an entry-less ET_DYN
481
+ # (an executable is an ET_EXEC entered at _start). Each default keeps the
482
+ # shared-object behavior byte-for-byte.
483
+
484
+ # Every link ends with the __dso_handle supplier archive. It is an archive,
485
+ # not an object, so the merge's lazy member extraction is the condition:
486
+ # the member joins only when something ahead of it still leaves
487
+ # __dso_handle undefined (glibc's libc_nonshared.a members reference it,
488
+ # and libc supplies it from a crt file rubycc has no counterpart of), and
489
+ # never when the link already has a definition of its own — an input that
490
+ # defines __dso_handle keeps it, exactly as with the compiler-support
491
+ # runtime the driver appends the same way. A link that does not mention the
492
+ # symbol gets no .data word and no relocation, so existing output is
493
+ # unchanged byte for byte.
494
+ def link_inputs = @inputs + [SharedLinker.dso_handle_archive(@em)]
495
+ def after_merge; end
496
+ def load_base = 0
497
+
498
+ # --- target machine ----------------------------------------------------
499
+ # The final-link core is written for both x86_64 and aarch64: a shared
500
+ # object and the executable writer share it. Each target-dependent decision
501
+ # — the machine id, the segment page size, the relocation scan/apply, the
502
+ # PLT stub bytes and the dynamic relocation type numbers — branches on this
503
+ # flag; the whole section/symbol/table layer above it is machine-independent.
504
+ # An aarch64 `.so` binds through R_AARCH64_RELATIVE rebasing of its internal
505
+ # absolute addresses (internal GOT slots and ABS64 initializers) and through
506
+ # the same eager (BIND_NOW) per-function PLT the x86_64 `.so` uses, so no
507
+ # lazy-resolver PLT0 header is needed.
508
+
509
+ def aarch64? = @em == EM_AARCH64
510
+ def e_machine_id = aarch64? ? EM_AARCH64 : EM_X86_64
511
+ def seg_align = aarch64? ? AARCH64_MAX_PAGE : PAGE
512
+
513
+ # The set of machines this writer accepts: x86_64 and aarch64, for both the
514
+ # shared object and the executable subclass.
515
+ def supported_machines = [EM_X86_64, EM_AARCH64]
516
+
517
+ def check_machine!
518
+ return if supported_machines.include?(@em)
519
+
520
+ raise LinkError, "linking for machine #{@em} is not supported by this linker"
521
+ end
522
+
523
+ # The ELF e_machine of the first relocatable input object, defaulting to
524
+ # x86_64 when no object header is readable (e.g. only archives, which a
525
+ # link always has a compiled object ahead of). Used to pick the synthesized
526
+ # inputs up front; the post-merge reader's machine agrees with it.
527
+ def detect_machine
528
+ @inputs.each do |raw|
529
+ header = input_elf_header(raw)
530
+ next unless header
531
+
532
+ return header.byteslice(18, 2).unpack1("S<")
533
+ end
534
+ EM_X86_64
535
+ end
536
+
537
+ # The leading bytes of an input if it is an ELF object (not an ar archive),
538
+ # or nil. A String input may be raw object bytes or a filesystem path.
539
+ def input_elf_header(raw)
540
+ bytes =
541
+ if raw.is_a?(String) && raw.b.start_with?("\x7FELF".b)
542
+ raw.b
543
+ elsif raw.is_a?(String) && raw.b.start_with?("!<arch>\n".b)
544
+ nil
545
+ else
546
+ File.binread(raw.to_s, 20)
547
+ end
548
+ bytes if bytes && bytes.bytesize >= 20 && bytes.b.start_with?("\x7FELF".b)
549
+ rescue SystemCallError
550
+ nil
551
+ end
552
+
553
+ # The dynamic relocation type numbers, chosen per target. x86_64 keeps its
554
+ # own constants so its output stays byte-for-byte identical.
555
+ def reloc_relative = aarch64? ? R_AARCH64_RELATIVE : R_X86_64_RELATIVE
556
+ def reloc_glob_dat = aarch64? ? R_AARCH64_GLOB_DAT : R_X86_64_GLOB_DAT
557
+ def reloc_jump_slot = aarch64? ? R_AARCH64_JUMP_SLOT : R_X86_64_JUMP_SLOT
558
+ def reloc_abs64 = aarch64? ? R_AARCH64_ABS64 : R_X86_64_64
559
+ def rebase_internal? = true
560
+ def leading_sections = []
561
+ def e_type = ET_DYN
562
+ def e_entry = 0
563
+
564
+ # A section placed into the image: its ELF section-header fields plus the
565
+ # assigned load address and file offset. `data` holds the file bytes (nil
566
+ # for a NOBITS section, which reserves memory only). `index` is its position
567
+ # in the emitted section header table, resolved during layout.
568
+ Placed = Struct.new(
569
+ :name, :type, :flags, :addralign, :entsize, :size, :data,
570
+ :vaddr, :offset, :link, :info, :index, keyword_init: true
571
+ )
572
+
573
+ # A dependency shared library the imports resolve against: its DT_NEEDED
574
+ # name (its own DT_SONAME, or the base filename when it carries none) and
575
+ # the set of symbol names it defines and exports.
576
+ Dependency = Struct.new(:name, :provides, keyword_init: true)
577
+
578
+ # --- dynamic symbols ---------------------------------------------------
579
+
580
+ # Selects the symbols to export: every defined global or weak with default
581
+ # or protected visibility, in the merged symbol table's order (deterministic).
582
+ # A hidden/internal symbol is deliberately not exported, matching the linker
583
+ # default that only externally visible definitions enter .dynsym.
584
+ def plan_dynamic_symbols
585
+ @exports = @reader.symbols.select do |sym|
586
+ (sym.bind == :global || sym.bind == :weak) &&
587
+ sym.defined? && !sym.name.to_s.empty? &&
588
+ (sym.visibility == :default || sym.visibility == :protected)
589
+ end
590
+ end
591
+
592
+ # --- relocation scan (sizing pass) -------------------------------------
593
+
594
+ # Walks every relocation to size the tables before addresses are assigned:
595
+ # it collects the ordered set of imported (undefined) symbols, the symbols
596
+ # needing a data GOT slot (internal or external), the external functions
597
+ # needing a .plt stub, and the counts of absolute-64 initializers (internal
598
+ # rebased by RELATIVE, external bound by a symbolic R_X86_64_64).
599
+ def scan_relocations
600
+ @got_order = [] # symbols needing a data GOT slot, first-seen order
601
+ @got_index = {} # got key => slot index
602
+ @plt_order = [] # external functions needing a .plt stub, first-seen
603
+ @plt_index = {} # function name => stub index
604
+ @import_order = [] # imported (undefined) symbols, first-seen order
605
+ @import_index = {} # import name => position
606
+ @data64_count = 0 # internal R_X86_64_64 -> RELATIVE
607
+ @data64_dyn_count = 0 # external R_X86_64_64 -> symbolic
608
+ allocatable_relocation_sections.each do |rs|
609
+ rs.relocations.each { |reloc| scan_relocation(reloc) }
610
+ end
611
+ end
612
+
613
+ def scan_relocation(reloc)
614
+ return scan_relocation_aarch64(reloc) if aarch64?
615
+
616
+ type = reloc.type
617
+ unless supported_relocation?(type)
618
+ raise LinkError, "unsupported relocation type #{reloc.type_name || type} in a shared object"
619
+ end
620
+
621
+ sym = reloc.symbol
622
+ external = external_import?(sym)
623
+ if external
624
+ if [R_X86_64_PC32, R_X86_64_32, R_X86_64_32S].include?(type)
625
+ raise LinkError, "unsupported text relocation against external symbol " \
626
+ "'#{sym.name}' in a shared object"
627
+ end
628
+ register_import(sym)
629
+ end
630
+
631
+ if GOT_RELOC_TYPES.include?(type)
632
+ register_got(sym)
633
+ elsif type == R_X86_64_PLT32
634
+ register_plt(sym) if external
635
+ elsif type == R_X86_64_64
636
+ external ? (@data64_dyn_count += 1) : (@data64_count += 1)
637
+ end
638
+ end
639
+
640
+ def supported_relocation?(type)
641
+ [R_X86_64_64, R_X86_64_PC32, R_X86_64_PLT32, R_X86_64_32, R_X86_64_32S,
642
+ *GOT_RELOC_TYPES].include?(type)
643
+ end
644
+
645
+ # The aarch64 sizing pass, the counterpart of #scan_relocation: it registers
646
+ # the same import/GOT/PLT/absolute-64 sets from aarch64's relocation
647
+ # vocabulary. A CALL26 to an external symbol needs a .plt stub; the GOT pair
648
+ # needs a data GOT slot; an ABS64 is an absolute-64 initializer (internal
649
+ # rebased by RELATIVE in a PIC image, external bound symbolically). An
650
+ # adrp/add pair against an external symbol would be a text relocation a
651
+ # dynamic image cannot satisfy, diagnosed like x86_64's external PC32.
652
+ def scan_relocation_aarch64(reloc)
653
+ type = reloc.type
654
+ unless AARCH64_SUPPORTED_RELOC_TYPES.include?(type)
655
+ raise LinkError, "unsupported relocation type #{reloc.type_name || type} in an aarch64 link"
656
+ end
657
+
658
+ sym = reloc.symbol
659
+ external = external_import?(sym)
660
+ if external && [R_AARCH64_ADR_PREL_PG_HI21, R_AARCH64_ADD_ABS_LO12_NC].include?(type)
661
+ raise LinkError, "unsupported text relocation against external symbol " \
662
+ "'#{sym.name}' (build the reference through the GOT)"
663
+ end
664
+ register_import(sym) if external
665
+
666
+ if AARCH64_GOT_RELOC_TYPES.include?(type)
667
+ register_got(sym)
668
+ elsif type == R_AARCH64_CALL26
669
+ register_plt(sym) if external
670
+ elsif type == R_AARCH64_ABS64
671
+ external ? (@data64_dyn_count += 1) : (@data64_count += 1)
672
+ end
673
+ end
674
+
675
+ # An imported reference: an undefined named symbol (a section symbol always
676
+ # names a section within this object and so is never an import).
677
+ def external_import?(sym)
678
+ sym && sym.type != :section && sym.undefined?
679
+ end
680
+
681
+ def register_import(sym)
682
+ return if @import_index.key?(sym.name)
683
+
684
+ @import_index[sym.name] = @import_order.size
685
+ @import_order << sym
686
+ end
687
+
688
+ def register_got(sym)
689
+ key = got_key(sym)
690
+ return if @got_index.key?(key)
691
+
692
+ @got_index[key] = @got_order.size
693
+ @got_order << sym
694
+ end
695
+
696
+ def register_plt(sym)
697
+ return if @plt_index.key?(sym.name)
698
+
699
+ @plt_index[sym.name] = @plt_order.size
700
+ @plt_order << sym
701
+ end
702
+
703
+ # A stable key identifying the symbol a GOT slot stands for: a named symbol
704
+ # by its name, a section reference by its section name.
705
+ def got_key(sym)
706
+ name = sym.name.to_s
707
+ name.empty? ? "\0sec:#{sym.section&.name}" : "g:#{name}"
708
+ end
709
+
710
+ # The relocation sections whose target lands in the loaded image; a
711
+ # relocation against a non-allocated section is not applied.
712
+ def allocatable_relocation_sections
713
+ @reader.relocation_sections.select { |rs| rs.target && allocatable?(rs.target) }
714
+ end
715
+
716
+ def allocatable?(section)
717
+ (section.flags & SHF_ALLOC) != 0
718
+ end
719
+
720
+ # --- import resolution -------------------------------------------------
721
+
722
+ # Resolves each imported symbol against the dependency `.so`s, in dependency
723
+ # order, recording (as-needed) only the dependencies that actually supply a
724
+ # resolved symbol as DT_NEEDED. A still-unresolved import is left undefined:
725
+ # a shared object may be completed by the runtime scope, so this is not an
726
+ # error.
727
+ def resolve_imports
728
+ @deps = build_dependencies
729
+ used = {}
730
+ @import_order.each do |sym|
731
+ dep = @deps.find { |d| d.provides.key?(sym.name) }
732
+ used[dep] = true if dep
733
+ end
734
+ @used_deps = @deps.select { |d| used[d] }
735
+ end
736
+
737
+ # Parses each dependency into a Dependency: its DT_NEEDED name and the set
738
+ # (a name => true hash) of the global/weak symbols it defines and exports.
739
+ def build_dependencies
740
+ @needed.map do |entry|
741
+ reader, name = load_dependency(entry)
742
+ provides = {}
743
+ reader.dynamic_symbols.each do |sym|
744
+ next unless sym.defined? && !sym.name.to_s.empty? &&
745
+ (sym.bind == :global || sym.bind == :weak)
746
+
747
+ provides[sym.name] = true
748
+ end
749
+ Dependency.new(name: name, provides: provides)
750
+ end
751
+ end
752
+
753
+ # A dependency given either as an already-parsed ELFReader or a `.so`
754
+ # filesystem path; its DT_NEEDED name is its DT_SONAME, falling back to the
755
+ # base filename for a path input (a reader without a SONAME has no name).
756
+ def load_dependency(entry)
757
+ if entry.is_a?(ELFReader)
758
+ soname = entry.soname or
759
+ raise LinkError, "dependency shared object carries no DT_SONAME; pass its path instead"
760
+ [entry, soname]
761
+ else
762
+ reader = ELFReader.read_file(entry)
763
+ [reader, reader.soname || File.basename(entry)]
764
+ end
765
+ end
766
+
767
+ # --- address assignment ------------------------------------------------
768
+
769
+ # Lays the allocatable input sections and the synthesized dynamic sections
770
+ # into three page-aligned PT_LOAD segments by permission, assigning each a
771
+ # file offset and an equal virtual address. The r-x segment begins at file
772
+ # offset 0 so the ELF header and program headers it contains are mapped;
773
+ # the r-- and rw- segments each start on a fresh page. .bss (NOBITS) sits
774
+ # last in the rw- segment, extending its memory size past its file size.
775
+ def place_sections
776
+ rx = leading_sections + input_sections { |s| executable?(s) } + plt_sections
777
+ ro = dynamic_ro_sections + input_sections { |s| !executable?(s) && !writable?(s) }
778
+ rw_input = input_sections { |s| writable?(s) && s.type != SHT_NOBITS }
779
+ plain, arrays = split_array_sections(rw_input)
780
+ rw_files = plain + arrays + writable_dynamic_sections
781
+ bss = input_sections { |s| writable?(s) && s.type == SHT_NOBITS }
782
+
783
+ cursor = EHDR_SIZE + phnum * PHDR_SIZE
784
+ cursor, @rx = lay(rx, cursor)
785
+ cursor = align(cursor, seg_align)
786
+ cursor, @ro = lay(ro, cursor)
787
+ cursor = align(cursor, seg_align)
788
+ rw_start = cursor
789
+ cursor, rw_placed = lay(rw_files, cursor)
790
+ check_array_runs_contiguous
791
+ @file_end = cursor
792
+ cursor, bss_placed = lay(bss, cursor)
793
+ @rw = rw_placed + bss_placed
794
+ @rw_start = rw_start
795
+ @mem_end = cursor
796
+
797
+ index_sections
798
+ build_dynamic_contents
799
+ end
800
+
801
+ def executable?(section) = (section.flags & SHF_EXECINSTR) != 0
802
+ def writable?(section) = (section.flags & SHF_WRITE) != 0
803
+
804
+ # The allocatable input sections matching a predicate, in merged order,
805
+ # each turned into a Placed carrying a mutable copy of its bytes so the
806
+ # relocation pass can patch them.
807
+ def input_sections
808
+ @reader.sections.select { |s| allocatable?(s) && yield(s) }.map do |s|
809
+ Placed.new(name: s.name, type: s.type, flags: s.flags, addralign: [s.addralign, 1].max,
810
+ entsize: s.entsize, size: s.size, data: s.type == SHT_NOBITS ? nil : s.data.b)
811
+ end
812
+ end
813
+
814
+ # --- initializer / finalizer arrays ------------------------------------
815
+ # .init_array / .fini_array are ordinary writable allocatable data, so the
816
+ # merge concatenates them by name and the apply engine patches their
817
+ # pointer slots like any other absolute-64 initializer (RELATIVE-rebased
818
+ # in a shared object, written final in a non-PIE executable). Only two
819
+ # things are specific to them: the loader reaches them through DT_*_ARRAY
820
+ # rather than a symbol, so each must be one contiguous run, and the run's
821
+ # *order* is the order the initializers run in.
822
+ #
823
+ # gcc spells a priority (`__attribute__((constructor(101)))`) as a separate
824
+ # `.init_array.00101` input section. Observed by linking such objects with
825
+ # gcc and reading back the resulting array (readelf -x) and the runtime
826
+ # order: the priority-numbered sections come first, sorted by ascending
827
+ # priority number across all inputs, and the unnumbered `.init_array`
828
+ # sections follow in input order. .fini_array is laid out by the same rule
829
+ # (the runtime walks it backwards, which mirrors the constructor order).
830
+ #
831
+ # Splits the writable input sections into the ordinary ones (kept in merged
832
+ # order) and the initializer/finalizer array run, which is appended after
833
+ # them so it ends up adjacent to the synthesized dynamic sections. A link
834
+ # with no array section leaves the first list exactly as it was, so its
835
+ # layout — and its output bytes — are unchanged.
836
+ def split_array_sections(sections)
837
+ reject_unwritable_array_sections!
838
+ plain = sections.reject { |s| array_section?(s) }
839
+ @init_array_run = order_array_run(sections.select { |s| s.type == SHT_INIT_ARRAY })
840
+ @fini_array_run = order_array_run(sections.select { |s| s.type == SHT_FINI_ARRAY })
841
+ [plain, @init_array_run + @fini_array_run]
842
+ end
843
+
844
+ def array_section?(section)
845
+ section.type == SHT_INIT_ARRAY || section.type == SHT_FINI_ARRAY
846
+ end
847
+
848
+ # Orders one array run by the rule above. The sort key pairs the priority
849
+ # with the section's position in the merged order, so equal priorities keep
850
+ # input order and the result is deterministic (N4) whatever Ruby's sort
851
+ # does. An unnumbered section sorts after every numbered one.
852
+ def order_array_run(sections)
853
+ sections.each_with_index.sort_by { |sec, i| [array_priority(sec), i] }.map(&:first)
854
+ end
855
+
856
+ # The priority encoded in an array section's name: `.init_array.00101` has
857
+ # priority 101, plain `.init_array` has none (Float::INFINITY, sorting
858
+ # last). Any other suffix is a form this linker has not been shown, and
859
+ # guessing its position would silently run the initializers in the wrong
860
+ # order, so it is refused instead.
861
+ def array_priority(section)
862
+ base = section.type == SHT_INIT_ARRAY ? ".init_array" : ".fini_array"
863
+ name = section.name.to_s
864
+ return Float::INFINITY if name == base
865
+ return ::Regexp.last_match(1).to_i if name =~ /\A#{::Regexp.escape(base)}\.(\d+)\z/
866
+
867
+ raise LinkError, "unsupported initializer array section '#{name}': " \
868
+ "only '#{base}' and a priority-numbered '#{base}.NNNNN' are understood"
869
+ end
870
+
871
+ # DT_INIT_ARRAY / DT_FINI_ARRAY address one array, so its sections must have
872
+ # been laid without a gap between them. They all carry alignment 8 and a
873
+ # size that is a whole number of 8-byte pointers, so the placement above is
874
+ # contiguous; this refuses the case rather than emitting a range with a
875
+ # zero (null-pointer) hole the loader would call.
876
+ def check_array_runs_contiguous
877
+ [@init_array_run, @fini_array_run].each do |run|
878
+ run.each_cons(2) do |a, b|
879
+ next if a.vaddr + a.size == b.vaddr
880
+
881
+ raise LinkError, "initializer array sections '#{a.name}' and '#{b.name}' " \
882
+ "were not laid contiguously"
883
+ end
884
+ end
885
+ end
886
+
887
+ def init_array_addr = @init_array_run.first&.vaddr
888
+ def fini_array_addr = @fini_array_run.first&.vaddr
889
+ def init_array_size = @init_array_run.sum(&:size)
890
+ def fini_array_size = @fini_array_run.sum(&:size)
891
+
892
+ # Whether the link has a non-empty array. Read off the run assembled above,
893
+ # which #place_sections fills in before it sizes .dynamic, so the tag count
894
+ # and the tag values can never disagree. An absent — or present but empty —
895
+ # array emits no tag at all, keeping the dynamic array of a link without
896
+ # initializers byte-for-byte as before.
897
+ def init_array? = init_array_size.positive?
898
+ def fini_array? = fini_array_size.positive?
899
+
900
+ # An array section the ABI marks SHF_ALLOC without SHF_WRITE would be laid
901
+ # into the read-only segment, dropping out of the run and taking its
902
+ # initializers with it silently. Nothing this linker accepts emits that
903
+ # shape (gcc marks both arrays WA), so it is refused rather than ignored.
904
+ def reject_unwritable_array_sections!
905
+ @reader.sections.each do |sec|
906
+ next unless allocatable?(sec) && array_section?(sec) && !writable?(sec)
907
+
908
+ raise LinkError, "initializer array section '#{sec.name}' is not writable"
909
+ end
910
+ end
911
+
912
+ # Assigns offsets/addresses to a run of sections starting at `cursor`, which
913
+ # tracks the virtual/memory position (equal to the file offset for file-
914
+ # backed sections). The segment's file offset (`offset`) may precede
915
+ # `cursor` when the header sits in front of the first section (the r-x
916
+ # segment). A NOBITS section advances the memory cursor like any other — its
917
+ # bytes are simply skipped when the file is written — so the caller captures
918
+ # the file-end cursor before laying the .bss run and reads the memory-end
919
+ # cursor after it. Returns [end_cursor, placed].
920
+ def lay(sections, cursor)
921
+ placed = []
922
+ sections.each do |sec|
923
+ cursor = align(cursor, sec.addralign)
924
+ # The file offset is the raw cursor; the virtual address adds the load
925
+ # base (zero for a shared object, so vaddr == offset as before; a fixed
926
+ # base for a non-PIE executable).
927
+ sec.offset = cursor
928
+ sec.vaddr = load_base + cursor
929
+ cursor += sec.size
930
+ placed << sec
931
+ end
932
+ [cursor, placed]
933
+ end
934
+
935
+ # Assigns the final section-header index to every placed section (NULL is 0,
936
+ # then r-x, r--, rw- runs in placement order, then .shstrtab) and builds the
937
+ # section-name -> index and section-name -> vaddr maps the symbol and
938
+ # dynamic tables resolve through.
939
+ def index_sections
940
+ @placed = @rx + @ro + @rw
941
+ @section_index = { nil => 0 }
942
+ @vaddr = {}
943
+ @placed.each_with_index do |sec, i|
944
+ sec.index = i + 1
945
+ @section_index[sec.name] = sec.index
946
+ @vaddr[sec.name] = sec.vaddr
947
+ end
948
+ # The regular linker symbol/string tables are non-allocatable metadata
949
+ # appended after the loadable image. Keep their indices out of
950
+ # @section_index: dynamic symbols must continue to name only mapped
951
+ # sections.
952
+ @symtab_index = @placed.size + 1
953
+ @strtab_index = @symtab_index + 1
954
+ @shstrtab_index = @strtab_index + 1
955
+ end
956
+
957
+ # The executable .plt (one stub per imported function), or none when there
958
+ # are no external calls.
959
+ def plt_sections
960
+ return [] unless plt?
961
+
962
+ [placed_generated(".plt", SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, 16, 0, @plt_order.size * PLT_ENTSIZE)]
963
+ end
964
+
965
+ # The synthesized read-only dynamic sections, in the order the loader
966
+ # expects to find them addressed from .dynamic: the SysV hash, the dynamic
967
+ # symbol table, and its string table, followed by .rela.dyn (RELATIVE /
968
+ # GLOB_DAT / symbolic 64) and .rela.plt (JUMP_SLOT) when present.
969
+ def dynamic_ro_sections
970
+ secs = [
971
+ placed_generated(".hash", SHT_HASH, SHF_ALLOC, 8, 4, hash_bytes.bytesize),
972
+ placed_generated(".dynsym", SHT_DYNSYM, SHF_ALLOC, 8, SYM_ENTSIZE, dynsym_size),
973
+ placed_generated(".dynstr", SHT_STRTAB, SHF_ALLOC, 1, 0, dynstr_bytes.bytesize)
974
+ ]
975
+ secs << placed_generated(".rela.dyn", SHT_RELA, SHF_ALLOC, 8, RELA_ENTSIZE, rela_dyn_size) if rela_dyn?
976
+ secs << placed_generated(".rela.plt", SHT_RELA, SHF_ALLOC, 8, RELA_ENTSIZE, rela_plt_size) if plt?
977
+ secs
978
+ end
979
+
980
+ # The synthesized writable dynamic sections: the data GOT (only when a data
981
+ # GOT slot is needed), the .got.plt (only when there are external calls),
982
+ # and the .dynamic array.
983
+ def writable_dynamic_sections
984
+ secs = []
985
+ secs << placed_generated(".got", SHT_PROGBITS, SHF_ALLOC | SHF_WRITE, 8, 8, @got_order.size * 8) unless @got_order.empty?
986
+ secs << placed_generated(".got.plt", SHT_PROGBITS, SHF_ALLOC | SHF_WRITE, 8, 8, gotplt_size) if plt?
987
+ secs << placed_generated(".dynamic", SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, 8, DYN_ENTSIZE, dynamic_size)
988
+ secs
989
+ end
990
+
991
+ def placed_generated(name, type, flags, addralign, entsize, size)
992
+ Placed.new(name: name, type: type, flags: flags, addralign: addralign,
993
+ entsize: entsize, size: size, data: nil)
994
+ end
995
+
996
+ def plt? = !@plt_order.empty?
997
+ def bind_now? = !@import_order.empty?
998
+
999
+ def gotplt_size = (GOTPLT_RESERVED + @plt_order.size) * 8
1000
+ def rela_plt_count = @plt_order.size
1001
+ def rela_plt_size = rela_plt_count * RELA_ENTSIZE
1002
+
1003
+ # A .rela.dyn is present when any internal address must be rebased (a GOT
1004
+ # slot or an R_X86_64_64 initializer) or any external data reference bound
1005
+ # (a GLOB_DAT slot or a symbolic R_X86_64_64).
1006
+ def rela_dyn? = rela_dyn_count.positive?
1007
+ def rela_dyn_size = rela_dyn_count * RELA_ENTSIZE
1008
+ # The GLOB_DAT entries (external data GOT slots) and symbolic-64 entries
1009
+ # (external absolute-64 initializers) are always dynamic; the RELATIVE
1010
+ # entries only exist when internal absolute addresses are rebased at load
1011
+ # time (a shared object). A non-PIE executable writes those addresses
1012
+ # directly, so it emits no RELATIVE entry.
1013
+ def rela_dyn_count = external_got_count + @data64_dyn_count + relacount
1014
+ # The number of leading RELATIVE entries in .rela.dyn (DT_RELACOUNT): the
1015
+ # internal GOT slots and internal absolute-64 initializers, which precede
1016
+ # the GLOB_DAT and symbolic entries. None when internal addresses are not
1017
+ # rebased (a non-PIE executable).
1018
+ def relacount = rebase_internal? ? (internal_got_count + @data64_count) : 0
1019
+ def internal_got_count = @got_order.count { |sym| !external_import?(sym) }
1020
+ def external_got_count = @got_order.count { |sym| external_import?(sym) }
1021
+
1022
+ def dynsym_size = (@exports.size + @import_order.size + 1) * SYM_ENTSIZE
1023
+ # The dynamic array's size depends only on which tags are present, not on
1024
+ # the addresses filled in later, so it can be computed during sizing.
1025
+ def dynamic_size = dynamic_entry_count * DYN_ENTSIZE
1026
+ def dynamic_entry_count
1027
+ @used_deps.size + (@soname ? 1 : 0) + 5 +
1028
+ (init_array? ? 2 : 0) + (fini_array? ? 2 : 0) +
1029
+ (rela_dyn? ? 4 : 0) + (plt? ? 4 : 0) + (bind_now? ? 2 : 0) + 1
1030
+ end
1031
+
1032
+ # --- relocation application --------------------------------------------
1033
+
1034
+ # Patches every allocatable target section against the assigned addresses,
1035
+ # then builds the GOT, the .plt and their dynamic relocation entries. The
1036
+ # relative entries are ordered internal GOT slots (in slot order) then the
1037
+ # absolute-64 initializers (in relocation order), for a deterministic
1038
+ # .rela.dyn.
1039
+ def apply_relocations
1040
+ @section_data = {}
1041
+ @placed.each { |sec| @section_data[sec.name] = sec.data if sec.data }
1042
+ @rela_data = [] # internal R_X86_64_64 -> [offset, addend] RELATIVE
1043
+ @rela_data_sym = [] # external R_X86_64_64 -> [offset, dynindex, addend]
1044
+
1045
+ allocatable_relocation_sections.each do |rs|
1046
+ base = @vaddr[rs.target.name]
1047
+ buf = @section_data[rs.target.name]
1048
+ rs.relocations.each { |reloc| apply_relocation(reloc, base, buf) }
1049
+ end
1050
+
1051
+ build_got
1052
+ build_plt if plt?
1053
+ end
1054
+
1055
+ def apply_relocation(reloc, base, buf)
1056
+ return apply_relocation_aarch64(reloc, base, buf) if aarch64?
1057
+
1058
+ sym = reloc.symbol
1059
+ a = reloc.addend
1060
+ p = base + reloc.offset
1061
+ external = external_import?(sym)
1062
+ case reloc.type
1063
+ when R_X86_64_PLT32
1064
+ target = external ? plt_stub_addr(sym) : symbol_address(sym)
1065
+ patch32(buf, reloc.offset, target + a - p)
1066
+ when R_X86_64_PC32
1067
+ patch32(buf, reloc.offset, symbol_address(sym) + a - p)
1068
+ when R_X86_64_32, R_X86_64_32S
1069
+ patch32(buf, reloc.offset, symbol_address(sym) + a)
1070
+ when *GOT_RELOC_TYPES
1071
+ slot = got_addr + @got_index[got_key(sym)] * 8
1072
+ patch32(buf, reloc.offset, slot + a - p)
1073
+ when R_X86_64_64
1074
+ if external
1075
+ patch64(buf, reloc.offset, 0)
1076
+ @rela_data_sym << [p, import_dynindex(sym), a]
1077
+ else
1078
+ value = symbol_address(sym) + a
1079
+ patch64(buf, reloc.offset, value)
1080
+ # A shared object rebases this absolute initializer at load time; a
1081
+ # non-PIE executable's address is already final, so no RELATIVE.
1082
+ @rela_data << [p, value] if rebase_internal?
1083
+ end
1084
+ end
1085
+ end
1086
+
1087
+ # The aarch64 apply, the counterpart of #apply_relocation. Where x86_64
1088
+ # patches a plain 32/64-bit displacement, aarch64 patches an immediate
1089
+ # bit-field packed inside a fixed 32-bit instruction word, and its address
1090
+ # references span two instructions the reader delivered as two relocations:
1091
+ # a CALL26 patches a `bl` (routed through the .plt for an external target),
1092
+ # ADR_PREL_PG_HI21 the page distance of an `adrp`, ADD_ABS_LO12_NC the
1093
+ # within-page offset of the `add`, and the GOT pair the same split against a
1094
+ # GOT slot. ABS64 is the absolute-64 pointer slot: written with its final
1095
+ # value in a non-PIE executable (no RELATIVE) or bound symbolically when it
1096
+ # names an import.
1097
+ def apply_relocation_aarch64(reloc, base, buf)
1098
+ sym = reloc.symbol
1099
+ a = reloc.addend
1100
+ p = base + reloc.offset
1101
+ external = external_import?(sym)
1102
+ case reloc.type
1103
+ when R_AARCH64_CALL26
1104
+ target = external ? plt_stub_addr(sym) : symbol_address(sym)
1105
+ patch_call26(buf, reloc.offset, target + a - p)
1106
+ when R_AARCH64_ADR_PREL_PG_HI21
1107
+ patch_adrp(buf, reloc.offset, page(symbol_address(sym) + a) - page(p))
1108
+ when R_AARCH64_ADD_ABS_LO12_NC
1109
+ patch_add_lo12(buf, reloc.offset, (symbol_address(sym) + a) & 0xFFF)
1110
+ when R_AARCH64_ADR_GOT_PAGE
1111
+ patch_adrp(buf, reloc.offset, page(got_slot_addr(sym)) - page(p))
1112
+ when R_AARCH64_LD64_GOT_LO12_NC
1113
+ patch_ld64_lo12(buf, reloc.offset, got_slot_addr(sym) & 0xFFF)
1114
+ when R_AARCH64_ABS64
1115
+ if external
1116
+ patch64(buf, reloc.offset, 0)
1117
+ @rela_data_sym << [p, import_dynindex(sym), a]
1118
+ else
1119
+ value = symbol_address(sym) + a
1120
+ patch64(buf, reloc.offset, value)
1121
+ @rela_data << [p, value] if rebase_internal?
1122
+ end
1123
+ end
1124
+ end
1125
+
1126
+ def got_slot_addr(sym) = got_addr + @got_index[got_key(sym)] * 8
1127
+
1128
+ # The 4 KiB page a virtual address sits in — the unit `adrp` operates on,
1129
+ # independent of the (larger) load-segment alignment.
1130
+ def page(addr) = addr & ~0xFFF
1131
+
1132
+ def read_insn(buf, offset) = buf[offset, 4].unpack1("L<")
1133
+ def write_insn(buf, offset, word) = (buf[offset, 4] = [word & 0xFFFFFFFF].pack("L<"))
1134
+
1135
+ # Patches a `bl`/`b`'s 26-bit signed word offset (ARM DDI 0487, the
1136
+ # unconditional-branch-immediate form): the byte displacement is a multiple
1137
+ # of four, so imm26 = disp >> 2 occupies bits [25:0].
1138
+ def patch_call26(buf, offset, disp)
1139
+ imm = (disp >> 2) & 0x03FFFFFF
1140
+ write_insn(buf, offset, (read_insn(buf, offset) & ~0x03FFFFFF) | imm)
1141
+ end
1142
+
1143
+ # Patches an `adrp`'s 21-bit signed page immediate. The page distance is a
1144
+ # multiple of 4 KiB, so imm21 = page_delta >> 12 splits into immlo (bits
1145
+ # [1:0], encoded at instruction bits [30:29]) and immhi (bits [20:2],
1146
+ # encoded at bits [23:5]).
1147
+ def patch_adrp(buf, offset, page_delta)
1148
+ imm = (page_delta >> 12) & 0x1FFFFF
1149
+ immlo = imm & 0x3
1150
+ immhi = (imm >> 2) & 0x7FFFF
1151
+ word = read_insn(buf, offset) & ~((0x3 << 29) | (0x7FFFF << 5))
1152
+ write_insn(buf, offset, word | (immlo << 29) | (immhi << 5))
1153
+ end
1154
+
1155
+ # Patches an `add` (immediate) 12-bit unsigned field at bits [21:10] with the
1156
+ # symbol's within-page byte offset.
1157
+ def patch_add_lo12(buf, offset, value)
1158
+ imm = value & 0xFFF
1159
+ write_insn(buf, offset, (read_insn(buf, offset) & ~(0xFFF << 10)) | (imm << 10))
1160
+ end
1161
+
1162
+ # Patches a 64-bit `ldr`'s 12-bit unsigned-offset field at bits [21:10]. The
1163
+ # field is the byte offset scaled by the access size (8), so the GOT slot's
1164
+ # 8-byte-aligned low offset encodes exactly as value >> 3.
1165
+ def patch_ld64_lo12(buf, offset, value)
1166
+ imm = (value >> 3) & 0xFFF
1167
+ write_insn(buf, offset, (read_insn(buf, offset) & ~(0xFFF << 10)) | (imm << 10))
1168
+ end
1169
+
1170
+ # Fills the data GOT: an internal slot holds its symbol's link-time address
1171
+ # and is rebased by an R_X86_64_RELATIVE; an external slot is left zero and
1172
+ # bound by an R_X86_64_GLOB_DAT naming the imported symbol.
1173
+ def build_got
1174
+ @got_bytes = +"".b
1175
+ @rela_relative_got = []
1176
+ @rela_glob_dat = []
1177
+ @got_order.each_with_index do |sym, i|
1178
+ slot = got_addr + i * 8
1179
+ if external_import?(sym)
1180
+ @got_bytes << [0].pack("Q<")
1181
+ @rela_glob_dat << [slot, import_dynindex(sym)]
1182
+ else
1183
+ address = symbol_address(sym)
1184
+ @got_bytes << [address].pack("Q<")
1185
+ # The slot already holds its final address; a shared object still
1186
+ # rebases it with a RELATIVE, a non-PIE executable does not.
1187
+ @rela_relative_got << [slot, address] if rebase_internal?
1188
+ end
1189
+ end
1190
+ end
1191
+
1192
+ # Builds the .plt stubs, the .got.plt and its JUMP_SLOT relocations. Each
1193
+ # stub is `jmp *slot(%rip)` (FF 25 + a PC-relative disp32 to its .got.plt
1194
+ # slot) padded with single-byte NOPs to the 16-byte entry; under BIND_NOW
1195
+ # the loader has already stored the resolved target in the slot, so the
1196
+ # first call jumps straight there. The .got.plt reserves three leading slots
1197
+ # ([0] = &_DYNAMIC, [1]/[2] zero) before the per-function slots.
1198
+ def build_plt
1199
+ return build_plt_aarch64 if aarch64?
1200
+
1201
+ @plt_bytes = +"".b
1202
+ @gotplt_bytes = +[@vaddr[".dynamic"], 0, 0].pack("Q<Q<Q<")
1203
+ @rela_plt = []
1204
+ @plt_order.each_with_index do |sym, i|
1205
+ stub = plt_stub_addr(sym)
1206
+ slot = gotplt_slot_addr(i)
1207
+ disp = slot - (stub + 6)
1208
+ @plt_bytes << [0xFF, 0x25].pack("C2") << [disp].pack("l<")
1209
+ @plt_bytes << ([0x90] * (PLT_ENTSIZE - 6)).pack("C*")
1210
+ @gotplt_bytes << [0].pack("Q<")
1211
+ @rela_plt << [slot, import_dynindex(sym)]
1212
+ end
1213
+ end
1214
+
1215
+ # The aarch64 .plt, the counterpart of #build_plt. Each 16-byte stub loads
1216
+ # its function's resolved address from the .got.plt slot and branches there;
1217
+ # under BIND_NOW the loader has already stored the JUMP_SLOT target, so the
1218
+ # first call reaches the callee directly. The four-instruction sequence
1219
+ # (ARM DDI 0487 encodings) is: `adrp x16, page(slot)`; `ldr x17, [x16,
1220
+ # #lo12]`; `add x16, x16, #lo12`; `br x17`. The .got.plt reserves three
1221
+ # leading slots ([0] = &_DYNAMIC, [1]/[2] zero) before the per-function ones.
1222
+ def build_plt_aarch64
1223
+ @plt_bytes = +"".b
1224
+ @gotplt_bytes = +[@vaddr[".dynamic"], 0, 0].pack("Q<Q<Q<")
1225
+ @rela_plt = []
1226
+ @plt_order.each_with_index do |sym, i|
1227
+ stub = plt_stub_addr(sym)
1228
+ slot = gotplt_slot_addr(i)
1229
+ @plt_bytes << aarch64_plt_stub(stub, slot)
1230
+ @gotplt_bytes << [0].pack("Q<")
1231
+ @rela_plt << [slot, import_dynindex(sym)]
1232
+ end
1233
+ end
1234
+
1235
+ # The four instruction words of one aarch64 .plt stub reaching `slot` from
1236
+ # its own address `stub` (see #build_plt_aarch64).
1237
+ def aarch64_plt_stub(stub, slot)
1238
+ lo12 = slot & 0xFFF
1239
+ adrp = aarch64_adrp(16, page(slot) - page(stub))
1240
+ ldr = aarch64_ldr64(17, 16, lo12)
1241
+ add = aarch64_add(16, 16, lo12)
1242
+ br = 0xD61F0000 | (17 << 5)
1243
+ [adrp, ldr, add, br].pack("L<4")
1244
+ end
1245
+
1246
+ # `adrp Xd, #page_delta`: base opcode 0x90000000 with the 21-bit page
1247
+ # immediate split into immlo (bits [30:29]) and immhi (bits [23:5]).
1248
+ def aarch64_adrp(rd, page_delta)
1249
+ imm = (page_delta >> 12) & 0x1FFFFF
1250
+ 0x90000000 | ((imm & 0x3) << 29) | (((imm >> 2) & 0x7FFFF) << 5) | rd
1251
+ end
1252
+
1253
+ # `ldr Xt, [Xn, #byteoff]` (64-bit unsigned offset): base 0xF9400000 with the
1254
+ # scaled (byteoff / 8) 12-bit immediate at bits [21:10].
1255
+ def aarch64_ldr64(rt, rn, byteoff)
1256
+ 0xF9400000 | (((byteoff >> 3) & 0xFFF) << 10) | (rn << 5) | rt
1257
+ end
1258
+
1259
+ # `add Xd, Xn, #imm12`: base 0x91000000 with the 12-bit immediate at bits
1260
+ # [21:10].
1261
+ def aarch64_add(rd, rn, imm12)
1262
+ 0x91000000 | ((imm12 & 0xFFF) << 10) | (rn << 5) | rd
1263
+ end
1264
+
1265
+ def got_addr = @vaddr[".got"]
1266
+ def plt_stub_addr(sym) = @vaddr[".plt"] + @plt_index[sym.name] * PLT_ENTSIZE
1267
+ def gotplt_slot_addr(i) = @vaddr[".got.plt"] + (GOTPLT_RESERVED + i) * 8
1268
+
1269
+ # The .dynsym index of an imported symbol: after the null entry and the
1270
+ # exported symbols.
1271
+ def import_dynindex(sym)
1272
+ @exports.size + 1 + @import_index[sym.name]
1273
+ end
1274
+
1275
+ # The load-time virtual address of a relocation's symbol: a section
1276
+ # reference resolves to the section's base, an absolute symbol to its
1277
+ # value, and any other defined symbol to its section base plus its offset.
1278
+ def symbol_address(sym)
1279
+ return 0 unless sym
1280
+ return @vaddr[sym.section.name] if sym.type == :section && sym.section
1281
+ return sym.value if sym.absolute?
1282
+
1283
+ @vaddr[sym.section.name] + sym.value
1284
+ end
1285
+
1286
+ # --- dynamic table contents --------------------------------------------
1287
+
1288
+ # Builds .dynstr and the name -> offset map, memoized so the sizing pass and
1289
+ # the emit share one table. It holds every exported and imported symbol name,
1290
+ # every recorded DT_NEEDED name and this object's DT_SONAME.
1291
+ def dynstr
1292
+ @dynstr ||= begin
1293
+ buf = +"\0".b
1294
+ offsets = {}
1295
+ dynstr_names.each do |name|
1296
+ next if name.nil? || name.empty? || offsets.key?(name)
1297
+
1298
+ offsets[name] = buf.bytesize
1299
+ buf << name.b << "\0".b
1300
+ end
1301
+ [buf, offsets]
1302
+ end
1303
+ end
1304
+
1305
+ def dynstr_names
1306
+ @exports.map(&:name) + @import_order.map(&:name) + @used_deps.map(&:name) + [@soname]
1307
+ end
1308
+
1309
+ def dynstr_bytes = dynstr[0]
1310
+ def dynstr_offset(name) = dynstr[1].fetch(name)
1311
+
1312
+ # The .dynsym payload: the reserved null entry, then one entry per exported
1313
+ # symbol carrying its load address, size, binding/type and defining section,
1314
+ # then one UND entry per imported symbol (st_shndx = SHN_UNDEF, value 0).
1315
+ def dynsym_bytes
1316
+ _, name_offsets = dynstr
1317
+ buf = +("\0".b * SYM_ENTSIZE)
1318
+ @exports.each do |sym|
1319
+ info = (STB.fetch(sym.bind, 1) << 4) | STT.fetch(sym.type, 0)
1320
+ buf << sym_entry(name: name_offsets.fetch(sym.name), info: info,
1321
+ other: STV.fetch(sym.visibility, 0),
1322
+ shndx: @section_index.fetch(sym.section.name),
1323
+ value: @vaddr[sym.section.name] + sym.value, size: sym.size)
1324
+ end
1325
+ @import_order.each do |sym|
1326
+ info = (STB.fetch(sym.bind, 1) << 4) | STT.fetch(sym.type, 0)
1327
+ buf << sym_entry(name: name_offsets.fetch(sym.name), info: info,
1328
+ other: STV.fetch(sym.visibility, 0),
1329
+ shndx: SHN_UNDEF, value: 0, size: 0)
1330
+ end
1331
+ buf
1332
+ end
1333
+
1334
+ # The SysV (.hash) table: nbucket, nchain, then the bucket heads and the
1335
+ # collision chain. Every dynamic symbol (exports and imports) occupies a
1336
+ # chain slot so nchain equals the .dynsym count, but only the exported
1337
+ # (defined) symbols are hashed into the buckets — an import is never looked
1338
+ # up here — in symbol-index order for a deterministic layout.
1339
+ def hash_bytes
1340
+ nsyms = @exports.size + @import_order.size + 1
1341
+ nbucket = bucket_count(@exports.size)
1342
+ buckets = Array.new(nbucket, SHN_UNDEF)
1343
+ chain = Array.new(nsyms, SHN_UNDEF)
1344
+ @exports.each_with_index do |sym, i|
1345
+ idx = i + 1
1346
+ b = elf_hash(sym.name) % nbucket
1347
+ chain[idx] = buckets[b]
1348
+ buckets[b] = idx
1349
+ end
1350
+ [nbucket, nsyms].pack("L<L<") + buckets.pack("L<*") + chain.pack("L<*")
1351
+ end
1352
+
1353
+ # Derives the hash bucket count deterministically from the export count via
1354
+ # a fixed growth ladder, keeping average chain length near one without
1355
+ # depending on anything but the input's size (N4).
1356
+ def bucket_count(n)
1357
+ ladder = [1, 3, 7, 17, 37, 67, 127, 251, 509, 1021, 2039]
1358
+ ladder.reverse_each { |b| return b if b <= n }
1359
+ 1
1360
+ end
1361
+
1362
+ # The ELF gABI symbol hash (name -> unsigned 32-bit-ish value) glibc uses to
1363
+ # locate a symbol in the SysV hash table.
1364
+ def elf_hash(name)
1365
+ h = 0
1366
+ name.each_byte do |c|
1367
+ h = (h << 4) + c
1368
+ g = h & 0xF0000000
1369
+ h ^= g >> 24 if g != 0
1370
+ h &= ~g & 0xFFFFFFFF
1371
+ end
1372
+ h
1373
+ end
1374
+
1375
+ # The .rela.dyn payload: the RELATIVE entries (internal GOT slots then data
1376
+ # initializers) first, so DT_RELACOUNT can cover them, then the GLOB_DAT
1377
+ # entries for external data GOT slots, then the symbolic R_X86_64_64 data
1378
+ # initializers pointing at imported data.
1379
+ def rela_dyn_bytes
1380
+ buf = +"".b
1381
+ (@rela_relative_got + @rela_data).each do |offset, addend|
1382
+ buf << rela_entry(offset, reloc_relative, 0, addend)
1383
+ end
1384
+ @rela_glob_dat.each do |offset, dynindex|
1385
+ buf << rela_entry(offset, reloc_glob_dat, dynindex, 0)
1386
+ end
1387
+ @rela_data_sym.each do |offset, dynindex, addend|
1388
+ buf << rela_entry(offset, reloc_abs64, dynindex, addend)
1389
+ end
1390
+ buf
1391
+ end
1392
+
1393
+ # The .rela.plt payload: one JUMP_SLOT per external function, naming its
1394
+ # imported symbol; the loader stores the resolved address into the slot.
1395
+ def rela_plt_bytes
1396
+ buf = +"".b
1397
+ @rela_plt.each { |offset, dynindex| buf << rela_entry(offset, reloc_jump_slot, dynindex, 0) }
1398
+ buf
1399
+ end
1400
+
1401
+ def rela_entry(offset, type, sym_index, addend)
1402
+ [offset, (sym_index << 32) | type].pack("Q<Q<") + [addend].pack("q<")
1403
+ end
1404
+
1405
+ # The .dynamic array: the DT_NEEDED dependencies and DT_SONAME first, then
1406
+ # the initializer/finalizer arrays (only when non-empty), then
1407
+ # pointers/sizes for the hash and symbol/string tables, the .rela.dyn table
1408
+ # (with DT_RELACOUNT), the .plt relocation table (DT_PLTGOT/PLTRELSZ/
1409
+ # PLTREL/JMPREL), the BIND_NOW flags, and the DT_NULL terminator.
1410
+ def dynamic_entries
1411
+ entries = []
1412
+ @used_deps.each { |dep| entries << [DT_NEEDED, dynstr_offset(dep.name)] }
1413
+ entries << [DT_SONAME, dynstr_offset(@soname)] if @soname
1414
+ # The initializer/finalizer arrays, sized in bytes, in gcc's position:
1415
+ # after the dependency names and ahead of the table pointers.
1416
+ entries += [[DT_INIT_ARRAY, init_array_addr], [DT_INIT_ARRAYSZ, init_array_size]] if init_array?
1417
+ entries += [[DT_FINI_ARRAY, fini_array_addr], [DT_FINI_ARRAYSZ, fini_array_size]] if fini_array?
1418
+ entries += [
1419
+ [DT_HASH, @vaddr[".hash"]],
1420
+ [DT_STRTAB, @vaddr[".dynstr"]],
1421
+ [DT_SYMTAB, @vaddr[".dynsym"]],
1422
+ [DT_STRSZ, dynstr_bytes.bytesize],
1423
+ [DT_SYMENT, SYM_ENTSIZE]
1424
+ ]
1425
+ if rela_dyn?
1426
+ entries += [
1427
+ [DT_RELA, @vaddr[".rela.dyn"]],
1428
+ [DT_RELASZ, rela_dyn_size],
1429
+ [DT_RELAENT, RELA_ENTSIZE],
1430
+ [DT_RELACOUNT, relacount]
1431
+ ]
1432
+ end
1433
+ if plt?
1434
+ entries += [
1435
+ [DT_PLTGOT, @vaddr[".got.plt"]],
1436
+ [DT_PLTRELSZ, rela_plt_size],
1437
+ [DT_PLTREL, DT_RELA],
1438
+ [DT_JMPREL, @vaddr[".rela.plt"]]
1439
+ ]
1440
+ end
1441
+ entries += [[DT_FLAGS, DF_BIND_NOW], [DT_FLAGS_1, DF_1_NOW]] if bind_now?
1442
+ entries << [DT_NULL, 0]
1443
+ entries
1444
+ end
1445
+
1446
+ def dynamic_bytes
1447
+ buf = +"".b
1448
+ dynamic_entries.each { |tag, val| buf << [tag].pack("q<") << [val].pack("Q<") }
1449
+ buf
1450
+ end
1451
+
1452
+ # Resolves the sh_link/sh_info cross-references and materializes the byte
1453
+ # payload of every synthesized section once addresses are known. Input
1454
+ # sections carry their patched bytes; NOBITS carries none.
1455
+ def build_dynamic_contents
1456
+ dynsym = named(".dynsym")
1457
+ named(".hash").link = dynsym.index
1458
+ dynsym.link = named(".dynstr").index
1459
+ dynsym.info = 1 # first non-local dynamic symbol (only the null entry is local)
1460
+ named(".dynamic").link = named(".dynstr").index
1461
+ named(".rela.dyn")&.link = dynsym.index
1462
+ rela_plt = named(".rela.plt")
1463
+ return unless rela_plt
1464
+
1465
+ rela_plt.link = dynsym.index
1466
+ rela_plt.info = named(".got.plt").index # the section the JUMP_SLOTs modify
1467
+ end
1468
+
1469
+ def named(name)
1470
+ @placed.find { |sec| sec.name == name }
1471
+ end
1472
+
1473
+ # The bytes to emit for a placed section: patched input bytes, or the
1474
+ # freshly built payload of a synthesized one.
1475
+ def section_bytes(sec)
1476
+ case sec.name
1477
+ when ".hash" then hash_bytes
1478
+ when ".dynsym" then dynsym_bytes
1479
+ when ".dynstr" then dynstr_bytes
1480
+ when ".rela.dyn" then rela_dyn_bytes
1481
+ when ".rela.plt" then rela_plt_bytes
1482
+ when ".plt" then @plt_bytes
1483
+ when ".got" then @got_bytes
1484
+ when ".got.plt" then @gotplt_bytes
1485
+ when ".dynamic" then dynamic_bytes
1486
+ else @section_data[sec.name]
1487
+ end
1488
+ end
1489
+
1490
+ # --- regular symbol table ----------------------------------------------
1491
+
1492
+ # Builds the non-allocatable symbol and string tables that describe the
1493
+ # final image to ordinary ELF tools. The dynamic tables above are the
1494
+ # loader-facing interface; `.symtab` is deliberately separate and may
1495
+ # contain local/hidden definitions that must not be exported through
1496
+ # `.dynsym`.
1497
+ def build_metadata_sections
1498
+ return @metadata_sections if @metadata_sections
1499
+
1500
+ records = regular_symbol_records
1501
+ strtab, offsets = regular_strtab(records)
1502
+ symtab = regular_symtab(records, offsets)
1503
+ cursor = align([@file_end, @mem_end].max, 8)
1504
+ symtab_section = Placed.new(
1505
+ name: ".symtab", type: SHT_SYMTAB, flags: 0, addralign: 8,
1506
+ entsize: SYM_ENTSIZE, size: symtab.bytesize, data: symtab,
1507
+ vaddr: 0, offset: cursor, link: @strtab_index,
1508
+ info: @symtab_first_global, index: @symtab_index
1509
+ )
1510
+ cursor += symtab.bytesize
1511
+ strtab_section = Placed.new(
1512
+ name: ".strtab", type: SHT_STRTAB, flags: 0, addralign: 1,
1513
+ entsize: 0, size: strtab.bytesize, data: strtab,
1514
+ vaddr: 0, offset: cursor, link: 0, info: 0, index: @strtab_index
1515
+ )
1516
+ @metadata_sections = [symtab_section, strtab_section]
1517
+ end
1518
+
1519
+ def regular_symbol_records
1520
+ candidates = @reader.symbols.drop(1).filter_map do |sym|
1521
+ shndx = regular_symbol_shndx(sym)
1522
+ next if shndx.nil?
1523
+
1524
+ value = sym.section ? @vaddr.fetch(sym.section.name) + sym.value : sym.value
1525
+ bind = STB.fetch(sym.bind, sym.bind)
1526
+ # GNU ld localizes hidden/internal definitions in the regular symbol
1527
+ # table at final-link time. Keep the same convention so `nm` renders
1528
+ # them as lowercase private symbols while default/protected definitions
1529
+ # remain externally visible uppercase entries.
1530
+ bind = STB[:local] if [STV[:hidden], STV[:internal]].include?(STV.fetch(sym.visibility, sym.visibility))
1531
+ {
1532
+ name: sym.name,
1533
+ bind: bind,
1534
+ type: STT.fetch(sym.type, sym.type),
1535
+ visibility: STV.fetch(sym.visibility, sym.visibility),
1536
+ shndx: shndx,
1537
+ value: value,
1538
+ size: sym.size
1539
+ }
1540
+ end
1541
+ locals, globals = candidates.partition { |sym| sym[:bind] == STB[:local] }
1542
+ @symtab_first_global = locals.length + 1 # the null symbol is entry zero
1543
+ [null_symbol_record] + locals + globals
1544
+ end
1545
+
1546
+ def regular_symbol_shndx(sym)
1547
+ return @section_index[sym.section.name] if sym.section && @section_index.key?(sym.section.name)
1548
+ return sym.shndx if sym.undefined? || sym.absolute? || sym.common?
1549
+
1550
+ nil
1551
+ end
1552
+
1553
+ def null_symbol_record
1554
+ { name: nil, bind: 0, type: 0, visibility: 0, shndx: SHN_UNDEF, value: 0, size: 0 }
1555
+ end
1556
+
1557
+ def regular_strtab(records)
1558
+ buf = +"\0".b
1559
+ offsets = {}
1560
+ records.each do |record|
1561
+ name = record[:name]
1562
+ next if name.nil? || name.empty? || offsets.key?(name)
1563
+
1564
+ offsets[name] = buf.bytesize
1565
+ buf << name.b << "\0".b
1566
+ end
1567
+ [buf, offsets]
1568
+ end
1569
+
1570
+ def regular_symtab(records, offsets)
1571
+ records.map do |record|
1572
+ name = record[:name]
1573
+ sym_entry(name: name && !name.empty? ? offsets.fetch(name) : 0,
1574
+ info: (record[:bind] << 4) | record[:type],
1575
+ other: record[:visibility], shndx: record[:shndx],
1576
+ value: record[:value], size: record[:size])
1577
+ end.join
1578
+ end
1579
+
1580
+ # --- assembly ----------------------------------------------------------
1581
+
1582
+ # Concatenates the ELF header, program headers, the placed sections' file
1583
+ # bytes, regular symbol/string tables, .shstrtab and the section header
1584
+ # table into the final image. The regular tables are not loaded; they are
1585
+ # retained so ordinary ELF tooling such as `nm` can inspect a linked `.so`.
1586
+ def assemble
1587
+ metadata = build_metadata_sections
1588
+ shstrtab, name_offsets = build_shstrtab
1589
+ shstrtab_offset = metadata.last.offset + metadata.last.size
1590
+ shoff = align(shstrtab_offset + shstrtab.bytesize, 8)
1591
+
1592
+ out = +"".b
1593
+ out << build_ehdr(shoff)
1594
+ out << build_phdrs
1595
+ @placed.each do |sec|
1596
+ next if sec.type == SHT_NOBITS
1597
+
1598
+ pad_to(out, sec.offset)
1599
+ out << section_bytes(sec)
1600
+ end
1601
+ metadata.each do |sec|
1602
+ pad_to(out, sec.offset)
1603
+ out << sec.data
1604
+ end
1605
+ pad_to(out, shstrtab_offset)
1606
+ out << shstrtab
1607
+ pad_to(out, shoff)
1608
+ out << build_null_shdr(name_offsets)
1609
+ @placed.each { |sec| out << build_shdr(sec, name_offsets) }
1610
+ metadata.each { |sec| out << build_shdr(sec, name_offsets) }
1611
+ out << build_shstrtab_shdr(shstrtab_offset, shstrtab.bytesize, name_offsets)
1612
+ out
1613
+ end
1614
+
1615
+ # The program header count is fixed: three PT_LOAD segments (r-x, r--,
1616
+ # rw-), PT_DYNAMIC, and PT_GNU_STACK marking a non-executable stack.
1617
+ def phnum = 5
1618
+
1619
+ def build_phdrs
1620
+ # The r-x segment is the one exception to segment_extent's "span from
1621
+ # the first section's file offset" rule: it maps from file offset 0 /
1622
+ # vaddr 0, ahead of every section, so the ELF header and program header
1623
+ # table it carries are covered too. p_filesz must therefore be the
1624
+ # *absolute* length up to the last file-backed byte (last.offset +
1625
+ # last.size), not that length minus the leading sections' own offset —
1626
+ # matching ExecutableLinker's rx_filesz, which faces the same layout.
1627
+ rx_last = @rx.reject { |s| s.type == SHT_NOBITS }.last
1628
+ rx_filesz = rx_last.offset + rx_last.size
1629
+ ro = segment_extent(@ro, base: @ro.first.vaddr)
1630
+ dynamic = named(".dynamic")
1631
+ [
1632
+ phdr(PT_LOAD, PF_R | PF_X, 0, 0, rx_filesz, rx_filesz, seg_align),
1633
+ phdr(PT_LOAD, PF_R, ro[:offset], ro[:offset], ro[:filesz], ro[:filesz], seg_align),
1634
+ phdr(PT_LOAD, PF_R | PF_W, @rw_start, @rw_start,
1635
+ @file_end - @rw_start, @mem_end - @rw_start, seg_align),
1636
+ phdr(PT_DYNAMIC, PF_R | PF_W, dynamic.offset, dynamic.vaddr, dynamic.size, dynamic.size, 8),
1637
+ phdr(PT_GNU_STACK, PF_R | PF_W, 0, 0, 0, 0, 0x10)
1638
+ ].join
1639
+ end
1640
+
1641
+ # The file/memory extent of a segment given its placed sections: the file
1642
+ # offset of its first section and the span from that offset to the end of
1643
+ # its last file-backed byte.
1644
+ def segment_extent(sections, base:)
1645
+ last = sections.reject { |s| s.type == SHT_NOBITS }.last
1646
+ finish = last ? last.offset + last.size : base
1647
+ { offset: sections.first.offset, filesz: finish - sections.first.offset }
1648
+ end
1649
+
1650
+ def phdr(type, flags, offset, vaddr, filesz, memsz, align)
1651
+ [type].pack("L<") + [flags].pack("L<") + [offset].pack("Q<") +
1652
+ [vaddr].pack("Q<") + [0].pack("Q<") + [filesz].pack("Q<") +
1653
+ [memsz].pack("Q<") + [align].pack("Q<")
1654
+ end
1655
+
1656
+ def build_ehdr(shoff)
1657
+ e_ident = [0x7F, 0x45, 0x4C, 0x46, ELFCLASS64, ELFDATA2LSB, EV_CURRENT,
1658
+ 0, 0, 0, 0, 0, 0, 0, 0, 0].pack("C16")
1659
+ e_ident +
1660
+ [e_type].pack("S<") + # ET_DYN for a .so, ET_EXEC for an executable
1661
+ [e_machine_id].pack("S<") +
1662
+ [EV_CURRENT].pack("L<") +
1663
+ [e_entry].pack("Q<") + # 0 for a shared object, _start's vaddr for an executable
1664
+ [EHDR_SIZE].pack("Q<") + # e_phoff (program headers follow the header)
1665
+ [shoff].pack("Q<") + # e_shoff
1666
+ [0].pack("L<") + # e_flags
1667
+ [EHDR_SIZE].pack("S<") + # e_ehsize
1668
+ [PHDR_SIZE].pack("S<") + # e_phentsize
1669
+ [phnum].pack("S<") + # e_phnum
1670
+ [SHDR_SIZE].pack("S<") + # e_shentsize
1671
+ [@placed.size + @metadata_sections.size + 2].pack("S<") + # NULL + sections + .shstrtab
1672
+ [@shstrtab_index].pack("S<") # e_shstrndx
1673
+ end
1674
+
1675
+ def build_shstrtab
1676
+ buf = +"\0".b
1677
+ offsets = {}
1678
+ (@placed.map(&:name) + @metadata_sections.map(&:name) + [".shstrtab"]).each do |name|
1679
+ next if offsets.key?(name)
1680
+
1681
+ offsets[name] = buf.bytesize
1682
+ buf << name.b << "\0".b
1683
+ end
1684
+ [buf, offsets]
1685
+ end
1686
+
1687
+ def build_null_shdr(name_offsets)
1688
+ shdr(name: 0, type: SHT_NULL, flags: 0, addr: 0, offset: 0, size: 0,
1689
+ link: 0, info: 0, addralign: 0, entsize: 0)
1690
+ end
1691
+
1692
+ def build_shdr(sec, name_offsets)
1693
+ shdr(name: name_offsets[sec.name], type: sec.type, flags: sec.flags,
1694
+ addr: sec.vaddr, offset: sec.offset, size: sec.size,
1695
+ link: sec.link || 0, info: sec.info || 0,
1696
+ addralign: sec.addralign, entsize: sec.entsize)
1697
+ end
1698
+
1699
+ def build_shstrtab_shdr(offset, size, name_offsets)
1700
+ shdr(name: name_offsets[".shstrtab"], type: SHT_STRTAB, flags: 0, addr: 0,
1701
+ offset: offset, size: size, link: 0, info: 0, addralign: 1, entsize: 0)
1702
+ end
1703
+
1704
+ def shdr(name:, type:, flags:, addr:, offset:, size:, link:, info:, addralign:, entsize:)
1705
+ [name].pack("L<") + [type].pack("L<") + [flags].pack("Q<") + [addr].pack("Q<") +
1706
+ [offset].pack("Q<") + [size].pack("Q<") + [link].pack("L<") + [info].pack("L<") +
1707
+ [addralign].pack("Q<") + [entsize].pack("Q<")
1708
+ end
1709
+
1710
+ def sym_entry(name:, info:, other:, shndx:, value:, size:)
1711
+ [name].pack("L<") + [info].pack("C") + [other].pack("C") +
1712
+ [shndx].pack("S<") + [value].pack("Q<") + [size].pack("Q<")
1713
+ end
1714
+
1715
+ def patch32(buf, offset, value)
1716
+ buf[offset, 4] = [value & 0xFFFFFFFF].pack("L<")
1717
+ end
1718
+
1719
+ def patch64(buf, offset, value)
1720
+ buf[offset, 8] = [value & 0xFFFFFFFFFFFFFFFF].pack("Q<")
1721
+ end
1722
+
1723
+ def align(value, alignment)
1724
+ alignment <= 1 ? value : (value + alignment - 1) / alignment * alignment
1725
+ end
1726
+
1727
+ def pad_to(buffer, target)
1728
+ buffer << ("\0" * (target - buffer.bytesize)).b if buffer.bytesize < target
1729
+ end
1730
+ end
1731
+ end
1732
+ end