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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +56 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +52 -0
- data/README.md +208 -0
- data/data/README.md +117 -0
- data/data/r10_corpus_scan.json +4082 -0
- data/data/r10_manual_classification.json +5342 -0
- data/data/r10_verification_m4a.json +1531 -0
- data/data/r10_verification_m4b.json +1818 -0
- data/data/r10_verification_m4c.json +1489 -0
- data/data/r10_verification_m4d.json +318 -0
- data/data/r10_verification_rbs.json +192 -0
- data/data/verified_gems.json +397 -0
- data/exe/rmake +16 -0
- data/exe/rubycc +9 -0
- data/exe/rubycc-ar +114 -0
- data/exe/rubycc-doctor +14 -0
- data/exe/rubycc-pkgconf +8 -0
- data/include/float.h +87 -0
- data/include/iso646.h +18 -0
- data/include/libc/alloca.h +18 -0
- data/include/libc/arpa/inet.h +61 -0
- data/include/libc/assert.h +43 -0
- data/include/libc/dirent.h +72 -0
- data/include/libc/dlfcn.h +58 -0
- data/include/libc/features.h +205 -0
- data/include/libc/glibc/aarch64/ctype.h +108 -0
- data/include/libc/glibc/aarch64/endian.h +54 -0
- data/include/libc/glibc/aarch64/errno.h +154 -0
- data/include/libc/glibc/aarch64/fcntl.h +144 -0
- data/include/libc/glibc/aarch64/inttypes.h +182 -0
- data/include/libc/glibc/aarch64/limits.h +63 -0
- data/include/libc/glibc/aarch64/pthread.h +131 -0
- data/include/libc/glibc/aarch64/setjmp.h +84 -0
- data/include/libc/glibc/aarch64/stdint.h +174 -0
- data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
- data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
- data/include/libc/glibc/aarch64/sys/select.h +75 -0
- data/include/libc/glibc/aarch64/sys/stat.h +136 -0
- data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
- data/include/libc/glibc/aarch64/sys/time.h +74 -0
- data/include/libc/glibc/aarch64/sys/types.h +127 -0
- data/include/libc/glibc/aarch64/time.h +113 -0
- data/include/libc/glibc/x86_64/ctype.h +108 -0
- data/include/libc/glibc/x86_64/endian.h +54 -0
- data/include/libc/glibc/x86_64/errno.h +154 -0
- data/include/libc/glibc/x86_64/fcntl.h +134 -0
- data/include/libc/glibc/x86_64/inttypes.h +182 -0
- data/include/libc/glibc/x86_64/limits.h +61 -0
- data/include/libc/glibc/x86_64/pthread.h +111 -0
- data/include/libc/glibc/x86_64/setjmp.h +82 -0
- data/include/libc/glibc/x86_64/stdint.h +163 -0
- data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
- data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
- data/include/libc/glibc/x86_64/sys/select.h +75 -0
- data/include/libc/glibc/x86_64/sys/stat.h +130 -0
- data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
- data/include/libc/glibc/x86_64/sys/time.h +74 -0
- data/include/libc/glibc/x86_64/sys/types.h +123 -0
- data/include/libc/glibc/x86_64/time.h +113 -0
- data/include/libc/grp.h +50 -0
- data/include/libc/langinfo.h +126 -0
- data/include/libc/link.h +18 -0
- data/include/libc/locale.h +83 -0
- data/include/libc/math.h +191 -0
- data/include/libc/netinet/in.h +126 -0
- data/include/libc/netinet/tcp.h +44 -0
- data/include/libc/poll.h +46 -0
- data/include/libc/pwd.h +58 -0
- data/include/libc/regex.h +53 -0
- data/include/libc/sched.h +35 -0
- data/include/libc/signal.h +204 -0
- data/include/libc/stdio.h +157 -0
- data/include/libc/stdlib.h +92 -0
- data/include/libc/string.h +74 -0
- data/include/libc/strings.h +25 -0
- data/include/libc/sys/cdefs.h +126 -0
- data/include/libc/sys/inotify.h +111 -0
- data/include/libc/sys/ioctl.h +36 -0
- data/include/libc/sys/mman.h +65 -0
- data/include/libc/sys/param.h +41 -0
- data/include/libc/sys/resource.h +109 -0
- data/include/libc/sys/socket.h +167 -0
- data/include/libc/sys/statfs.h +82 -0
- data/include/libc/sys/timerfd.h +55 -0
- data/include/libc/sys/uio.h +40 -0
- data/include/libc/sys/un.h +25 -0
- data/include/libc/sys/utsname.h +35 -0
- data/include/libc/sys/wait.h +135 -0
- data/include/libc/termios.h +179 -0
- data/include/libc/unistd.h +194 -0
- data/include/stdalign.h +16 -0
- data/include/stdarg.h +31 -0
- data/include/stdatomic.h +158 -0
- data/include/stdbool.h +15 -0
- data/include/stdckdint.h +28 -0
- data/include/stddef.h +60 -0
- data/include/stdnoreturn.h +18 -0
- data/include/x86intrin.h +16 -0
- data/lib/rubycc/backend/aarch64.rb +1724 -0
- data/lib/rubycc/backend/x86_64.rb +1369 -0
- data/lib/rubycc/compile_error.rb +36 -0
- data/lib/rubycc/compiler.rb +305 -0
- data/lib/rubycc/doctor/builder.rb +151 -0
- data/lib/rubycc/doctor/cli.rb +190 -0
- data/lib/rubycc/doctor/fetcher.rb +84 -0
- data/lib/rubycc/doctor/gemfile.rb +115 -0
- data/lib/rubycc/doctor/verified_gems.rb +114 -0
- data/lib/rubycc/doctor.rb +16 -0
- data/lib/rubycc/driver.rb +463 -0
- data/lib/rubycc/front/ast.rb +528 -0
- data/lib/rubycc/front/constant_evaluator.rb +631 -0
- data/lib/rubycc/front/initializer_resolver.rb +592 -0
- data/lib/rubycc/front/lexeme_reader.rb +460 -0
- data/lib/rubycc/front/lexer.rb +232 -0
- data/lib/rubycc/front/parser.rb +4122 -0
- data/lib/rubycc/front/token.rb +49 -0
- data/lib/rubycc/ir/call_convention.rb +486 -0
- data/lib/rubycc/ir/generator.rb +6036 -0
- data/lib/rubycc/ir/ir.rb +417 -0
- data/lib/rubycc/link/compat_runtime.rb +89 -0
- data/lib/rubycc/link/errors.rb +13 -0
- data/lib/rubycc/link/executable_linker.rb +405 -0
- data/lib/rubycc/link/library_resolver.rb +437 -0
- data/lib/rubycc/link/partial_linker.rb +546 -0
- data/lib/rubycc/link/shared_linker.rb +1732 -0
- data/lib/rubycc/mkmf_shim.rb +83 -0
- data/lib/rubycc/objfile/ar_archive.rb +437 -0
- data/lib/rubycc/objfile/elf_reader.rb +646 -0
- data/lib/rubycc/objfile/elf_writer.rb +891 -0
- data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
- data/lib/rubycc/pkgconf/cli.rb +139 -0
- data/lib/rubycc/pkgconf/errors.rb +39 -0
- data/lib/rubycc/pkgconf/model.rb +28 -0
- data/lib/rubycc/pkgconf/parser.rb +109 -0
- data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
- data/lib/rubycc/pkgconf/resolver.rb +70 -0
- data/lib/rubycc/pkgconf/search_path.rb +38 -0
- data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
- data/lib/rubycc/preprocess/constant_expression.rb +219 -0
- data/lib/rubycc/preprocess/glibc_version.rb +124 -0
- data/lib/rubycc/preprocess/pp_token.rb +62 -0
- data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
- data/lib/rubycc/preprocess/scanner.rb +290 -0
- data/lib/rubycc/preprocess/token_converter.rb +157 -0
- data/lib/rubycc/rmake/cli.rb +143 -0
- data/lib/rubycc/rmake/errors.rb +71 -0
- data/lib/rubycc/rmake/executor.rb +818 -0
- data/lib/rubycc/rmake/expander.rb +251 -0
- data/lib/rubycc/rmake/makefile.rb +352 -0
- data/lib/rubycc/rmake/model.rb +137 -0
- data/lib/rubycc/rmake/parser.rb +226 -0
- data/lib/rubycc/rmake/rmake.rb +15 -0
- data/lib/rubycc/type.rb +1236 -0
- data/lib/rubycc/version.rb +5 -0
- data/lib/rubycc.rb +36 -0
- data/lib/rubygems_plugin.rb +102 -0
- 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
|