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,646 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module ObjFile
5
+ # Raised for any input that is not a well-formed ELF64 object this reader
6
+ # accepts: a truncated file, a bad magic, a non-64-bit class, a non-x86_64
7
+ # machine, or an internally inconsistent structure (a section or table
8
+ # pointing past the end of the file). The message names the specific defect
9
+ # so the linker (L3) can surface it to the user.
10
+ class ELFFormatError < Rubycc::Error; end
11
+
12
+ # Reads an ELF64 little-endian x86_64 object for the linker. Two shapes are
13
+ # supported: a relocatable object (ET_REL, a compiler-emitted `.o`) is fully
14
+ # parsed — sections with their raw bytes, the `.symtab`, and every `.rela.*`
15
+ # relocation table; a shared object (ET_DYN, a `.so`) is read for its
16
+ # dynamic-linking interface — the `.dynsym` exported/imported symbols and the
17
+ # `.dynamic` array's DT_SONAME / DT_NEEDED strings.
18
+ #
19
+ # This is the counterpart of ELFWriter and is designed to round-trip
20
+ # everything that writer emits (see test/test_elf_reader.rb): the parsed
21
+ # Section/Symbol/Relocation value objects carry exactly the fields the writer
22
+ # sets, resolved back from their on-disk encoding (section names via
23
+ # `.shstrtab`, symbol names via the linked string table, relocation targets
24
+ # via `r_info`'s symbol index and the RELA section's `sh_info`).
25
+ #
26
+ # The model is load-then-query: `read`/`read_file` parse the whole image up
27
+ # front into arrays and name-indexed lookups so the linker can address any
28
+ # section, symbol, or relocation randomly rather than through a callback
29
+ # stream. Nothing here writes or mutates an object.
30
+ #
31
+ # For a `.so` the dynamic tables are located strictly through the section
32
+ # header table (`.dynsym` / `.dynstr` / `.dynamic` by `sh_type`, sized by
33
+ # `sh_size` / `sh_entsize`). The allocated dynamic sections survive `strip`,
34
+ # so a section-header-less `.so` is rare enough that the PT_DYNAMIC program
35
+ # header fallback (deriving the symbol count from DT_HASH / DT_GNU_HASH) is
36
+ # deliberately not built here — YAGNI until a real input demands it.
37
+ class ELFReader
38
+ # ELF identification (e_ident) and header field encodings.
39
+ ELFMAG = "\x7FELF".b
40
+ EI_CLASS = 4
41
+ EI_DATA = 5
42
+ EI_VERSION = 6
43
+ ELFCLASS64 = 2
44
+ ELFDATA2LSB = 1
45
+ EV_CURRENT = 1
46
+ EHDR_SIZE = 64
47
+ SHDR_SIZE = 64
48
+
49
+ # e_type values the reader consumes; others are reported numerically and
50
+ # rejected by #read. ET_EXEC is read back like a shared object — through the
51
+ # allocated .dynsym / .dynamic tables located by the section header table —
52
+ # so the executable writer's output can be inspected the same way.
53
+ ET_REL = 1
54
+ ET_EXEC = 2
55
+ ET_DYN = 3
56
+
57
+ # The machines this reader understands. Both are little-endian ELF64, so
58
+ # the whole header/section/symbol layer is shared; only the relocation
59
+ # type names differ (see RELOC_TYPES).
60
+ EM_X86_64 = 62
61
+ EM_AARCH64 = 183
62
+
63
+ SUPPORTED_MACHINES = [EM_X86_64, EM_AARCH64].freeze
64
+
65
+ # Section header types.
66
+ SHT_NULL = 0
67
+ SHT_PROGBITS = 1
68
+ SHT_SYMTAB = 2
69
+ SHT_STRTAB = 3
70
+ SHT_RELA = 4
71
+ SHT_DYNAMIC = 6
72
+ SHT_NOBITS = 8
73
+ SHT_DYNSYM = 11
74
+ # The version-definition table (.gnu.version_d): the symbol versions this
75
+ # object defines. Its type lives in the gABI's OS-specific range
76
+ # (SHT_LOOS = 0x60000000 upwards), which is why the number looks nothing
77
+ # like the ones above.
78
+ SHT_GNU_VERDEF = 0x6FFFFFFD
79
+
80
+ # Reserved section indices that a symbol's st_shndx may carry instead of a
81
+ # real section number: an undefined (imported) symbol, an absolute value,
82
+ # and a not-yet-allocated COMMON block.
83
+ SHN_UNDEF = 0
84
+ SHN_LORESERVE = 0xFF00
85
+ SHN_ABS = 0xFFF1
86
+ SHN_COMMON = 0xFFF2
87
+ SHN_XINDEX = 0xFFFF
88
+
89
+ SYM_ENTSIZE = 24
90
+ RELA_ENTSIZE = 24
91
+ DYN_ENTSIZE = 16
92
+
93
+ # An Elf64_Verdef record and the Elf64_Verdaux records behind it. Neither
94
+ # is an array element: each carries its own byte distance to the next one
95
+ # (vd_next / vda_next), so these sizes bound a single record rather than
96
+ # stride a table (see #parse_version_definitions).
97
+ VERDEF_SIZE = 20
98
+ VERDAUX_SIZE = 8
99
+
100
+ # The only vd_version (VER_DEF_CURRENT) this layout is defined for; a
101
+ # record announcing another revision is not the structure decoded below.
102
+ VER_DEF_CURRENT = 1
103
+
104
+ # vd_flags bits: the definition that names the object itself rather than a
105
+ # version its symbols can bind to, and one whose version holds no symbols.
106
+ VER_FLG_BASE = 0x1
107
+ VER_FLG_WEAK = 0x2
108
+
109
+ # Symbol binding (st_info >> 4) and type (st_info & 0xF), mapped to symbols
110
+ # for readable queries; an unrecognized value passes through as its integer.
111
+ SYM_BINDINGS = { 0 => :local, 1 => :global, 2 => :weak }.freeze
112
+ SYM_TYPES = {
113
+ 0 => :notype, 1 => :object, 2 => :func, 3 => :section, 4 => :file,
114
+ 6 => :tls, 10 => :ifunc
115
+ }.freeze
116
+ # Symbol visibility (st_other & 0x3).
117
+ SYM_VISIBILITIES = { 0 => :default, 1 => :internal, 2 => :hidden, 3 => :protected }.freeze
118
+
119
+ # Relocation types (r_info & 0xFFFFFFFF), keyed by e_machine because the
120
+ # numbering is per-architecture. The ones the toolchain emits are named;
121
+ # any other type is preserved numerically (its name is nil) rather than
122
+ # rejected, so an unfamiliar object is still fully readable.
123
+ # The x86_64 GOTPCREL family addresses a symbol's Global Offset Table
124
+ # slot: 9 is the plain PC-relative GOT reference, and 41/42 its
125
+ # "relaxable" forms the psABI defines for a `mov` that a linker may
126
+ # rewrite in place to a `lea` (42 is the REX-prefixed form a 64-bit
127
+ # `mov rax, sym@GOTPCREL(%rip)` uses).
128
+ #
129
+ # On aarch64 the picture is different because that machine forms an
130
+ # address in two instructions: CALL26 is the 26-bit branch immediate of a
131
+ # `bl`/`b` to a named symbol, ABS64 an absolute 64-bit pointer slot, and
132
+ # the remaining four come in pairs. ADR_PREL_PG_HI21 carries the 21-bit
133
+ # page distance of an `adrp` and ADD_ABS_LO12_NC the 12-bit within-page
134
+ # offset of the `add` behind it; ADR_GOT_PAGE and LD64_GOT_LO12_NC are the
135
+ # same split applied to a symbol's Global Offset Table slot, the second
136
+ # patching the scaled immediate of the `ldr` that reads it.
137
+ RELOC_TYPES = {
138
+ EM_X86_64 => {
139
+ 1 => :R_X86_64_64,
140
+ 2 => :R_X86_64_PC32,
141
+ 4 => :R_X86_64_PLT32,
142
+ 9 => :R_X86_64_GOTPCREL,
143
+ 10 => :R_X86_64_32,
144
+ 11 => :R_X86_64_32S,
145
+ 41 => :R_X86_64_GOTPCRELX,
146
+ 42 => :R_X86_64_REX_GOTPCRELX
147
+ }.freeze,
148
+ EM_AARCH64 => {
149
+ 257 => :R_AARCH64_ABS64,
150
+ 275 => :R_AARCH64_ADR_PREL_PG_HI21,
151
+ 277 => :R_AARCH64_ADD_ABS_LO12_NC,
152
+ 283 => :R_AARCH64_CALL26,
153
+ 311 => :R_AARCH64_ADR_GOT_PAGE,
154
+ 312 => :R_AARCH64_LD64_GOT_LO12_NC
155
+ }.freeze
156
+ }.freeze
157
+
158
+ # Dynamic array tags (.dynamic) this reader interprets: the terminator, the
159
+ # DT_NEEDED shared-library dependencies, and the DT_SONAME of the object
160
+ # itself. Every tag's raw value is still exposed; only these drive behavior.
161
+ DT_NULL = 0
162
+ DT_NEEDED = 1
163
+ DT_SONAME = 14
164
+
165
+ # A parsed section header plus (for non-NOBITS sections) its raw file bytes.
166
+ # `index` is the section's position in the header table — the value other
167
+ # sections' sh_link/sh_info and symbols' st_shndx refer to. `data` is nil
168
+ # for SHT_NULL and SHT_NOBITS (.bss), which occupy no file bytes while
169
+ # still reporting their in-memory `size`.
170
+ Section = Struct.new(
171
+ :index, :name, :type, :flags, :addr, :offset, :size,
172
+ :link, :info, :addralign, :entsize, :data, keyword_init: true
173
+ ) do
174
+ def nobits? = type == SHT_NOBITS
175
+ end
176
+
177
+ # A parsed symbol table entry. `bind`/`type`/`visibility` are the decoded
178
+ # symbolic forms; `shndx` is the raw st_shndx and `section` the Section it
179
+ # names (nil for an undefined/absolute/common symbol). The predicates
180
+ # distinguish the reserved st_shndx markers the writer emits.
181
+ Symbol = Struct.new(
182
+ :index, :name, :value, :size, :bind, :type, :visibility, :shndx, :section,
183
+ keyword_init: true
184
+ ) do
185
+ def undefined? = shndx == SHN_UNDEF
186
+ def absolute? = shndx == SHN_ABS
187
+ def common? = shndx == SHN_COMMON
188
+ # Defined here means backed by a real section in this object (neither
189
+ # imported nor a reserved marker) — what "does this .so export printf?"
190
+ # asks of a dynamic symbol.
191
+ def defined? = !undefined? && shndx < SHN_LORESERVE
192
+ end
193
+
194
+ # A single RELA entry. `symbol` is the resolved Symbol (from the RELA
195
+ # section's linked symbol table); `type` is the numeric relocation type and
196
+ # `type_name` its RELOC_TYPES symbol (nil when unknown). `addend` is signed.
197
+ Relocation = Struct.new(
198
+ :offset, :type, :type_name, :symbol, :addend, keyword_init: true
199
+ )
200
+
201
+ # One version this object defines (an Elf64_Verdef with its Elf64_Verdaux
202
+ # chain resolved). `name` is the version's own name — the first aux record
203
+ # — and `parents` the names of the versions it inherits from, in the order
204
+ # the chain lists them. `index` is vd_ndx, the number a .gnu.version entry
205
+ # carries to bind a symbol to this definition, and `flags` the raw
206
+ # vd_flags, of which VER_FLG_BASE marks the file's own pseudo-version
207
+ # (whose `name` is the SONAME, not a version a symbol can carry).
208
+ VersionDefinition = Struct.new(:index, :flags, :name, :parents, keyword_init: true) do
209
+ def base? = (flags & VER_FLG_BASE) != 0
210
+ def weak? = (flags & VER_FLG_WEAK) != 0
211
+ end
212
+
213
+ # One SHT_RELA table: the relocations it holds and the Section they patch
214
+ # (its sh_info target, e.g. .text for .rela.text). Grouping by target is
215
+ # what the linker walks when applying fixups section by section.
216
+ RelocationSection = Struct.new(:section, :target, :relocations, keyword_init: true)
217
+
218
+ class << self
219
+ # Parses an in-memory ELF image (an ASCII-8BIT String) and returns a
220
+ # ready-to-query ELFReader.
221
+ def read(bytes)
222
+ new(bytes).tap(&:parse!)
223
+ end
224
+
225
+ # Reads a file from disk and parses it. Convenience over read(File.binread).
226
+ def read_file(path)
227
+ read(File.binread(path))
228
+ end
229
+ end
230
+
231
+ attr_reader :type, :machine, :entry, :sections, :symbols,
232
+ :relocation_sections, :dynamic_symbols, :dynamic_entries,
233
+ :soname, :needed, :version_definitions
234
+
235
+ def initialize(bytes)
236
+ @data = bytes.b
237
+ @sections = []
238
+ @symbols = []
239
+ @relocation_sections = []
240
+ @dynamic_symbols = []
241
+ @dynamic_entries = []
242
+ @needed = []
243
+ @soname = nil
244
+ @version_definitions = []
245
+ end
246
+
247
+ def parse!
248
+ parse_header
249
+ parse_sections
250
+ @symbols = symbol_table_by_index[symtab_section&.index] || []
251
+ parse_relocations
252
+ parse_dynamic_symbols
253
+ parse_dynamic
254
+ parse_version_definitions
255
+ self
256
+ end
257
+
258
+ def relocatable? = @type == ET_REL
259
+ def executable? = @type == ET_EXEC
260
+ def shared_object? = @type == ET_DYN
261
+
262
+ # The first section with the given name, or nil. Section names are not
263
+ # unique in ELF, but the ones this reader is asked about (.text, .symtab,
264
+ # .rela.text, .dynsym, ...) are, so first-match is the useful lookup.
265
+ def section(name)
266
+ @sections.find { |sec| sec.name == name }
267
+ end
268
+
269
+ # The first symbol with the given name in .symtab, or nil.
270
+ def symbol(name)
271
+ @symbols.find { |sym| sym.name == name }
272
+ end
273
+
274
+ # The first dynamic symbol with the given name in .dynsym, or nil — the
275
+ # export query for a shared object ("is printf here and defined?").
276
+ def dynamic_symbol(name)
277
+ @dynamic_symbols.find { |sym| sym.name == name }
278
+ end
279
+
280
+ # The relocations that patch the named section (e.g. "relocations_for('.text')"
281
+ # yields the .rela.text entries), or an empty array.
282
+ def relocations_for(target_name)
283
+ rs = @relocation_sections.find { |r| r.target&.name == target_name }
284
+ rs ? rs.relocations : []
285
+ end
286
+
287
+ private
288
+
289
+ # --- header ------------------------------------------------------------
290
+
291
+ def parse_header
292
+ raise ELFFormatError, "file is too short to hold an ELF header" if @data.bytesize < EHDR_SIZE
293
+ raise ELFFormatError, "bad ELF magic" unless @data.byteslice(0, 4) == ELFMAG
294
+
295
+ cls = byte(EI_CLASS)
296
+ raise ELFFormatError, "unsupported ELF class #{cls} (only ELFCLASS64 is supported)" unless cls == ELFCLASS64
297
+
298
+ data_enc = byte(EI_DATA)
299
+ raise ELFFormatError, "unsupported ELF data encoding #{data_enc} (only little-endian is supported)" \
300
+ unless data_enc == ELFDATA2LSB
301
+
302
+ @type = u16(16)
303
+ @machine = u16(18)
304
+ raise ELFFormatError, "unsupported machine #{@machine} (only EM_X86_64 and EM_AARCH64 are supported)" \
305
+ unless SUPPORTED_MACHINES.include?(@machine)
306
+ unless [ET_REL, ET_EXEC, ET_DYN].include?(@type)
307
+ raise ELFFormatError, "unsupported ELF type #{@type} (only ET_REL, ET_EXEC and ET_DYN are supported)"
308
+ end
309
+
310
+ @entry = u64(24)
311
+ @shoff = u64(40)
312
+ @shentsize = u16(58)
313
+ @shnum = u16(60)
314
+ @shstrndx = u16(62)
315
+ end
316
+
317
+ # --- sections ----------------------------------------------------------
318
+
319
+ def parse_sections
320
+ return if @shoff.zero?
321
+
322
+ raise ELFFormatError, "unexpected section header size #{@shentsize}" unless @shentsize == SHDR_SIZE
323
+
324
+ # The zeroth section header doubles as an escape hatch for the two 16-bit
325
+ # header fields that can overflow: when e_shnum is 0 the real count lives
326
+ # in its sh_size, and when e_shstrndx is SHN_XINDEX the real index lives
327
+ # in its sh_link. Resolve both before reading the rest.
328
+ count = @shnum
329
+ strndx = @shstrndx
330
+ if count.zero? || strndx == SHN_XINDEX
331
+ first = read_section_header(0)
332
+ count = first.size if count.zero?
333
+ strndx = first.link if strndx == SHN_XINDEX
334
+ end
335
+
336
+ # The whole section-header table must lie within the file. e_shnum is a
337
+ # bounded 16-bit field, but the e_shnum==0 escape hatch above takes the
338
+ # count from the zeroth header's 64-bit sh_size, which a hostile object
339
+ # could set enormous to make the map below try to allocate a giant array
340
+ # (or spin reading headers) before any per-entry bound trips. Rejecting a
341
+ # count whose table cannot physically fit stops that up front; a genuine
342
+ # object's table always fits, since the file holds it.
343
+ if count.positive? && @shoff + count * SHDR_SIZE > @data.bytesize
344
+ raise ELFFormatError, "section header table of #{count} entries extends past end of file"
345
+ end
346
+
347
+ raw = (0...count).map { |i| read_section_header(i) }
348
+ shstr = raw[strndx] or raise ELFFormatError, "section-name string table index #{strndx} is out of range"
349
+ strtab = section_bytes(shstr)
350
+
351
+ @sections = raw.map do |sec|
352
+ sec.name = read_string(strtab, sec.name_offset)
353
+ sec.data = sec.type == SHT_NOBITS || sec.type == SHT_NULL ? nil : section_bytes(sec)
354
+ Section.new(
355
+ index: sec.index, name: sec.name, type: sec.type, flags: sec.flags,
356
+ addr: sec.addr, offset: sec.offset, size: sec.size, link: sec.link,
357
+ info: sec.info, addralign: sec.addralign, entsize: sec.entsize, data: sec.data
358
+ )
359
+ end
360
+ end
361
+
362
+ # A raw section header, before its name is resolved. name_offset is the
363
+ # sh_name byte offset into .shstrtab.
364
+ RawSection = Struct.new(
365
+ :index, :name_offset, :name, :type, :flags, :addr, :offset, :size,
366
+ :link, :info, :addralign, :entsize, :data, keyword_init: true
367
+ )
368
+
369
+ def read_section_header(index)
370
+ base = @shoff + index * SHDR_SIZE
371
+ require_range(base, SHDR_SIZE, "section header ##{index}")
372
+ RawSection.new(
373
+ index: index,
374
+ name_offset: u32(base),
375
+ type: u32(base + 4),
376
+ flags: u64(base + 8),
377
+ addr: u64(base + 16),
378
+ offset: u64(base + 24),
379
+ size: u64(base + 32),
380
+ link: u32(base + 40),
381
+ info: u32(base + 44),
382
+ addralign: u64(base + 48),
383
+ entsize: u64(base + 56)
384
+ )
385
+ end
386
+
387
+ # The file bytes a section occupies. A NOBITS section holds none; every
388
+ # other section must lie wholly within the file.
389
+ def section_bytes(sec)
390
+ return "".b if sec.type == SHT_NOBITS || sec.size.zero?
391
+
392
+ require_range(sec.offset, sec.size, "section #{sec.name || sec.index} contents")
393
+ @data.byteslice(sec.offset, sec.size)
394
+ end
395
+
396
+ # --- symbols -----------------------------------------------------------
397
+
398
+ def symtab_section
399
+ @sections.find { |sec| sec.type == SHT_SYMTAB }
400
+ end
401
+
402
+ # Parses every symbol table (.symtab and .dynsym) into arrays keyed by the
403
+ # table's own section index, so a relocation can resolve r_info's symbol
404
+ # through whichever table its RELA section links to. Computed lazily and
405
+ # memoized because both #symbols and the relocation/dynamic passes need it.
406
+ def symbol_table_by_index
407
+ @symbol_table_by_index ||= begin
408
+ tables = {}
409
+ @sections.each do |sec|
410
+ next unless sec.type == SHT_SYMTAB || sec.type == SHT_DYNSYM
411
+
412
+ tables[sec.index] = parse_symbols(sec)
413
+ end
414
+ tables
415
+ end
416
+ end
417
+
418
+ # Decodes one symbol table section into Symbol value objects. Names resolve
419
+ # through the string table named by sh_link.
420
+ def parse_symbols(sec)
421
+ strtab = string_table_for(sec)
422
+ entsize = sec.entsize.zero? ? SYM_ENTSIZE : sec.entsize
423
+ # `count` cannot run away: parse_sections already validated that this
424
+ # section's [offset, size) span lies within the file (section_bytes), so
425
+ # size — and thus size/entsize — is bounded by the file's own length. The
426
+ # per-entry require_range below is a second, exact guard. (The same holds
427
+ # for parse_rela and parse_dynamic, whose sizes were validated alike.)
428
+ count = sec.size / entsize
429
+ (0...count).map do |i|
430
+ base = sec.offset + i * entsize
431
+ require_range(base, SYM_ENTSIZE, "symbol ##{i} in #{sec.name}")
432
+ st_name = u32(base)
433
+ st_info = byte_at(base + 4)
434
+ st_other = byte_at(base + 5)
435
+ shndx = u16(base + 6)
436
+ Symbol.new(
437
+ index: i,
438
+ name: read_string(strtab, st_name),
439
+ value: u64(base + 8),
440
+ size: u64(base + 16),
441
+ bind: SYM_BINDINGS.fetch(st_info >> 4, st_info >> 4),
442
+ type: SYM_TYPES.fetch(st_info & 0xF, st_info & 0xF),
443
+ visibility: SYM_VISIBILITIES.fetch(st_other & 0x3, st_other & 0x3),
444
+ shndx: shndx,
445
+ section: real_section_index?(shndx) ? @sections[shndx] : nil
446
+ )
447
+ end
448
+ end
449
+
450
+ # A st_shndx that names a real entry in the section header table, as
451
+ # opposed to SHN_UNDEF or a reserved high value (SHN_ABS/SHN_COMMON/...).
452
+ def real_section_index?(shndx)
453
+ shndx > SHN_UNDEF && shndx < SHN_LORESERVE && shndx < @sections.size
454
+ end
455
+
456
+ # The string table (as bytes) a symbol- or dynamic-section's sh_link points
457
+ # at; empty when the link is absent so name lookups yield "".
458
+ def string_table_for(sec)
459
+ link = @sections[sec.link]
460
+ link ? link.data || "".b : "".b
461
+ end
462
+
463
+ # --- relocations -------------------------------------------------------
464
+
465
+ def parse_relocations
466
+ @relocation_sections = @sections.select { |sec| sec.type == SHT_RELA }.map do |sec|
467
+ RelocationSection.new(
468
+ section: sec,
469
+ target: @sections[sec.info],
470
+ relocations: parse_rela(sec)
471
+ )
472
+ end
473
+ end
474
+
475
+ # The relocation-type name table for this object's machine; an object of a
476
+ # machine with no table still reads, its types simply staying unnamed.
477
+ def reloc_type_names
478
+ @reloc_type_names ||= RELOC_TYPES.fetch(@machine, {})
479
+ end
480
+
481
+ def parse_rela(sec)
482
+ symbols = symbol_table_by_index[sec.link] || []
483
+ entsize = sec.entsize.zero? ? RELA_ENTSIZE : sec.entsize
484
+ count = sec.size / entsize
485
+ (0...count).map do |i|
486
+ base = sec.offset + i * entsize
487
+ require_range(base, RELA_ENTSIZE, "relocation ##{i} in #{sec.name}")
488
+ r_info = u64(base + 8)
489
+ sym_index = r_info >> 32
490
+ type = r_info & 0xFFFFFFFF
491
+ Relocation.new(
492
+ offset: u64(base),
493
+ type: type,
494
+ type_name: reloc_type_names[type],
495
+ symbol: symbols[sym_index],
496
+ addend: s64(base + 16)
497
+ )
498
+ end
499
+ end
500
+
501
+ # --- dynamic (.so) -----------------------------------------------------
502
+
503
+ def parse_dynamic_symbols
504
+ dynsym = @sections.find { |sec| sec.type == SHT_DYNSYM }
505
+ @dynamic_symbols = dynsym ? symbol_table_by_index[dynsym.index] : []
506
+ end
507
+
508
+ # Reads the .dynamic array (tag/value pairs) into DynamicEntry records,
509
+ # then resolves the string-valued tags (DT_SONAME, DT_NEEDED) through
510
+ # .dynstr — the section named by .dynamic's own sh_link.
511
+ def parse_dynamic
512
+ dyn = @sections.find { |sec| sec.type == SHT_DYNAMIC }
513
+ return unless dyn
514
+
515
+ strtab = string_table_for(dyn)
516
+ entsize = dyn.entsize.zero? ? DYN_ENTSIZE : dyn.entsize
517
+ count = dyn.size / entsize
518
+ (0...count).each do |i|
519
+ base = dyn.offset + i * entsize
520
+ require_range(base, DYN_ENTSIZE, "dynamic entry ##{i}")
521
+ tag = s64(base)
522
+ val = u64(base + 8)
523
+ break if tag == DT_NULL
524
+
525
+ @dynamic_entries << DynamicEntry.new(tag: tag, value: val)
526
+ case tag
527
+ when DT_SONAME then @soname = read_string(strtab, val)
528
+ when DT_NEEDED then @needed << read_string(strtab, val)
529
+ end
530
+ end
531
+ end
532
+
533
+ # One .dynamic array element: its raw d_tag and d_un value. String tags'
534
+ # values are byte offsets into .dynstr (resolved into #soname / #needed).
535
+ DynamicEntry = Struct.new(:tag, :value, keyword_init: true)
536
+
537
+ # --- symbol versions (.gnu.version_d) ----------------------------------
538
+
539
+ # Reads the version-definition table into VersionDefinition records. It is
540
+ # the answer to "which versions does this shared object define" — the
541
+ # question a glibc image answers with its GLIBC_2.<n> names, which is how
542
+ # the preprocessor measures the C library it is compiling against
543
+ # (Preprocess::GlibcVersion) instead of naming a version in a header.
544
+ #
545
+ # The table is a linked list, not an array: every Elf64_Verdef holds
546
+ # vd_next, the byte distance from itself to the next record (zero ends the
547
+ # list), and vd_aux, the distance from itself to its first Elf64_Verdaux,
548
+ # which chains on in the same way through vda_next. The records therefore
549
+ # need not be adjacent and cannot be strided over, so the walk follows the
550
+ # links. sh_info states how many definitions there are, which bounds the
551
+ # walk independently of the links: a self-referential or otherwise corrupt
552
+ # chain stops after that many steps rather than spinning. Names resolve
553
+ # through the string table sh_link names (.dynstr for this allocated
554
+ # table), exactly like a symbol table's.
555
+ def parse_version_definitions
556
+ sec = @sections.find { |s| s.type == SHT_GNU_VERDEF }
557
+ return unless sec
558
+
559
+ strtab = string_table_for(sec)
560
+ offset = sec.offset
561
+ sec.info.times do
562
+ require_range(offset, VERDEF_SIZE, "version definition in #{sec.name}")
563
+ version = u16(offset)
564
+ unless version == VER_DEF_CURRENT
565
+ raise ELFFormatError, "unsupported version definition revision #{version} in #{sec.name}"
566
+ end
567
+
568
+ # The aux chain is the definition's own name followed by the names of
569
+ # the versions it inherits from; vd_cnt counts both together.
570
+ names = read_verdaux_names(offset + u32(offset + 12), u16(offset + 6), strtab, sec)
571
+ @version_definitions << VersionDefinition.new(
572
+ index: u16(offset + 4), flags: u16(offset + 2),
573
+ name: names.first, parents: names.drop(1)
574
+ )
575
+
576
+ vd_next = u32(offset + 16)
577
+ break if vd_next.zero?
578
+
579
+ offset += vd_next
580
+ end
581
+ end
582
+
583
+ # Walks one definition's Elf64_Verdaux chain from `offset`, returning at
584
+ # most `count` names in chain order. Like the Verdef list the chain is
585
+ # bounded by both its own zero terminator and the stated count.
586
+ def read_verdaux_names(offset, count, strtab, sec)
587
+ names = []
588
+ count.times do
589
+ require_range(offset, VERDAUX_SIZE, "version definition name in #{sec.name}")
590
+ names << read_string(strtab, u32(offset))
591
+ vda_next = u32(offset + 4)
592
+ break if vda_next.zero?
593
+
594
+ offset += vda_next
595
+ end
596
+ names
597
+ end
598
+
599
+ # --- primitive reads (all bounds-checked against the image) -------------
600
+
601
+ def byte(offset) = @data.getbyte(offset)
602
+ def byte_at(offset)
603
+ require_range(offset, 1, "byte")
604
+ @data.getbyte(offset)
605
+ end
606
+
607
+ def u16(offset)
608
+ require_range(offset, 2, "u16")
609
+ @data.byteslice(offset, 2).unpack1("S<")
610
+ end
611
+
612
+ def u32(offset)
613
+ require_range(offset, 4, "u32")
614
+ @data.byteslice(offset, 4).unpack1("L<")
615
+ end
616
+
617
+ def u64(offset)
618
+ require_range(offset, 8, "u64")
619
+ @data.byteslice(offset, 8).unpack1("Q<")
620
+ end
621
+
622
+ def s64(offset)
623
+ require_range(offset, 8, "s64")
624
+ @data.byteslice(offset, 8).unpack1("q<")
625
+ end
626
+
627
+ # A NUL-terminated string starting at `offset` within a string table's
628
+ # bytes; an out-of-range offset yields "" rather than raising, since a
629
+ # zero sh_name legitimately points at the leading NUL (the empty name).
630
+ def read_string(strtab, offset)
631
+ return "" if offset >= strtab.bytesize
632
+
633
+ stop = strtab.index("\0".b, offset) || strtab.bytesize
634
+ strtab.byteslice(offset...stop).force_encoding(Encoding::UTF_8)
635
+ end
636
+
637
+ # Guards every structural read: the whole [offset, length) span must lie
638
+ # within the image, or the file is truncated / self-inconsistent.
639
+ def require_range(offset, length, what)
640
+ if offset.negative? || length.negative? || offset + length > @data.bytesize
641
+ raise ELFFormatError, "#{what} at offset #{offset} (#{length} bytes) extends past end of file"
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end