Class: Rubycc::Compiler

Inherits:
Object
  • Object
show all
Defined in:
lib/rubycc/compiler.rb

Overview

Orchestrates every compilation stage: source -> tokens -> AST -> IR -> machine code -> ELF relocatable object.

Constant Summary collapse

TARGETS =

The backend dispatch table: a normalized target name selects the code generator that lowers IR to that machine's instructions, the ELF machine description (e_machine value + relocation-type table) the object writer emits under, and the one ABI trait the machine-independent front end has to know about — whether plain char is signed. That last entry is what makes the front end target-aware at all: the signedness of plain char is implementation-defined (6.2.5p15) and each ABI pins it, signed under the x86-64 System V psABI and unsigned under AAPCS64, so it has to reach type resolution rather than being decided once for the whole compiler. The arch_macros entry is the same idea one stage earlier: the CPU-identifying predefined macros a translation unit (and the libc headers it includes) dispatches on. unnamed_bitfields_align is a third: the ABIs disagree on whether an unnamed bit-field's type raises its aggregate's alignment, so sizeof and Alignof of such a struct are target-dependent (see StructType#define). libc_arch is a fourth: it names the bundled libc-and-arch header layer the preprocessor puts on its default search path, so a cross compile reads the target's ABI headers (struct stat, nlink_t, WCHAR* and kin) rather than the host's. Apart from those four every entry shares the orchestration logic below unchanged.

Which C library those headers describe is deliberately not in this table: the machine and the libc are independent axes (either arch runs either libc), so it is #compile's own libc keyword instead.

{
  "x86_64" => { backend: Backend::X86_64, machine: ObjFile::ELFWriter::X86_64,
                char_signed: true,
                arch_macros: Preprocess::Preprocessor::X86_64_ARCH_MACROS,
                libc_arch: "x86_64",
                unnamed_bitfields_align: false,
                convention: IR::CallConvention::SYSTEM_V_AMD64 },
  "aarch64" => { backend: Backend::AArch64, machine: ObjFile::ELFWriter::AARCH64,
                 char_signed: false,
                 arch_macros: Preprocess::Preprocessor::AARCH64_ARCH_MACROS,
                 libc_arch: "aarch64",
                 unnamed_bitfields_align: true,
                 convention: IR::CallConvention::AAPCS64 }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.compile_file(input_path, output_path, include_paths: [], pic: false, defines: [], system_includes: true, target: "x86_64", libc: Preprocess::Preprocessor.host_libc, default_visibility: :default) ⇒ Object

Convenience: read input_path, compile it and write the object to output_path.



292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/rubycc/compiler.rb', line 292

def self.compile_file(input_path, output_path, include_paths: [], pic: false, defines: [],
                      system_includes: true, target: "x86_64",
                      libc: Preprocess::Preprocessor.host_libc,
                      default_visibility: :default)
  source = File.read(input_path)
  binary = new.compile(source, filename: input_path, include_paths: include_paths,
                               pic: pic, defines: defines, system_includes: system_includes,
                               target: target, libc: libc,
                               default_visibility: default_visibility)
  File.binwrite(output_path, binary)
  output_path
end

Instance Method Details

#compile(source, filename:, include_paths: [], pic: false, defines: [], system_includes: true, target: "x86_64", libc: Preprocess::Preprocessor.host_libc, default_visibility: :default) ⇒ Object

Compiles C source into an ELF64 relocatable object, returned as an ASCII-8BIT String. Raises Rubycc::CompileError on user errors. target names the machine to generate code for (see TARGETS); it defaults to x86_64 and an unknown value is a caller error. libc names the C library whose ABI the bundled headers are to describe ("glibc" or "musl"); it defaults to the host's own, so an unconfigured compile on a musl host reads the musl branches, and an unknown value is a caller error too (the preprocessor raises it).



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/rubycc/compiler.rb', line 62

def compile(source, filename:, include_paths: [], pic: false, defines: [], system_includes: true,
            target: "x86_64", libc: Preprocess::Preprocessor.host_libc,
            default_visibility: :default)
  entry = TARGETS.fetch(target) { raise ArgumentError, "unsupported target: #{target.inspect}" }
  # The target's plain-`char` type, threaded through every stage that builds
  # or reasons about one: the preprocessor (which predefines
  # __CHAR_UNSIGNED__ when it is unsigned), the parser (which resolves the
  # `char` type-specifier to it) and the generator (which types a string
  # literal's elements with it).
  plain_char = Type.plain_char(entry[:char_signed])
  tokens = Preprocess::Preprocessor.new(char_unsigned: plain_char.unsigned?,
                                        arch_macros: entry[:arch_macros],
                                        libc_arch: entry[:libc_arch],
                                        libc: libc)
                                   .run(source, filename: filename,
                                        include_paths: include_paths, defines: defines,
                                        system_includes: system_includes)
  program = Front::Parser.new(tokens, plain_char: plain_char,
                                      unnamed_bitfields_align: entry[:unnamed_bitfields_align],
                                      builtin_va_list: entry[:convention].va_list_type).parse
  ir_program = IR::Generator.new(plain_char: plain_char,
                                 convention: entry[:convention]).generate(program, pic: pic)

  backend = entry[:backend].new
  writer = ObjFile::ELFWriter.new(machine: entry[:machine])
  writer.add_file_symbol(File.basename(filename))

  # Lay out the translation unit's string pool as .rodata: each interned
  # string in id order, NUL-terminated. `string_offsets[id]` is the byte
  # offset of string `id` within the section.
  rodata = +"".b
  string_offsets = []
  ir_program.strings.each do |bytes|
    string_offsets << rodata.bytesize
    rodata << bytes << "\0".b
  end

  # Lay out the file-scope variables: initialized ones into .data (their
  # values packed little-endian at the declared width) and zero-initialized
  # ones into .bss (space reserved only). Each is placed at its type's
  # alignment and registered as a global STT_OBJECT symbol; the section
  # alignment is the widest member's.
  data = +"".b
  data_align = 1
  bss_size = 0
  bss_align = 1
  # Symbols a .data pointer slot resolves against (a "&global", a decayed
  # global array, or a function address in a function-pointer global). A
  # reference to a function defined elsewhere must be registered as an
  # undefined symbol, but only once the set of locally defined functions is
  # known (after the text is compiled), so the names are collected here.
  data_symbol_refs = []
  ir_program.globals.each do |global|
    # A `static` global gets an internal-linkage (STB_LOCAL) object symbol,
    # an ordinary one a global (STB_GLOBAL) symbol; both are laid out into
    # .data/.bss identically.
    internal = global.linkage == :internal
    if global.init.nil?
      bss_align = [bss_align, global.align].max
      bss_size = align_up(bss_size, global.align)
      add_object_symbol(writer, internal, global.name, :bss, bss_size, global.size,
                        visibility: ir_program.visibility.fetch(global.name, default_visibility))
      bss_size += global.size
    else
      data_align = [data_align, global.align].max
      offset = align_up(data.bytesize, global.align)
      data << ("\0".b * (offset - data.bytesize))
      add_object_symbol(writer, internal, global.name, :data, offset, global.size,
                        visibility: ir_program.visibility.fetch(global.name, default_visibility))
      data << global.init.bytes
      # Each pointer slot in the image is patched by a .data relocation,
      # its .text-relative offset (within the global) biased by where the
      # global itself landed in .data.
      global.init.relocations.each do |reloc|
        register_data_relocation(writer, reloc, offset, string_offsets)
        data_symbol_refs << reloc.symbol if reloc.kind == :symbol
      end
    end
  end
  writer.set_data(data, align: data_align) unless data.empty?
  writer.set_bss(bss_size, align: bss_align) if bss_size.positive?

  text = +"".b
  # A defined function's name is only ever tested for membership below
  # (never iterated in order), so a Set keeps that check O(1) instead of
  # the O(n) linear scan an Array would need per relocation.
  defined_names = Set.new
  relocations = []
  ir_program.functions.each do |ir_func|
    result = backend.compile(ir_func)
    # Align each function to 16 bytes with the target's NOP filler, keeping
    # the output deterministic and every entry point aligned.
    pad_to_alignment(text, 16, entry[:machine].text_padding)
    base = text.bytesize
    text << result.bytes
    result.symbols.each do |sym|
      # A `static` function is a file-local (STB_LOCAL) symbol; an ordinary
      # one is global. Either way it is a defined name a same-object
      # reference resolves against, never an undefined external.
      if ir_func.linkage == :internal
        writer.add_local_func(sym[:name], base + sym[:offset], sym[:size])
      else
        writer.add_global_func(
          sym[:name], base + sym[:offset], sym[:size],
          visibility: ir_program.visibility.fetch(ir_func.name, default_visibility)
        )
      end
      defined_names << sym[:name]
    end
    result.relocations.each do |reloc|
      relocations << reloc.merge(offset: base + reloc[:offset])
    end
  end

  # A function-pointer global that names a function defined elsewhere leaves
  # an undefined symbol for the linker; one that names a local function or
  # another global is already in the symbol table. Like `defined_names`,
  # this is only ever queried for membership, so it stays a Set.
  known_names = defined_names | ir_program.globals.map(&:name)
  data_symbol_refs.each do |symbol|
    writer.add_undefined_symbol(symbol) unless known_names.include?(symbol)
  end

  relocations.each do |reloc|
    case reloc[:kind]
    when :call, :func
      # A call, or a taken function address, whose target is not defined in
      # this translation unit becomes an undefined symbol for the linker to
      # resolve (e.g. libc's abs). The two are recorded as distinct kinds
      # because they only coincide on some targets: x86_64 resolves both with
      # the same PC-relative PLT32, while aarch64 needs a `bl`'s CALL26 for
      # one and an address-forming instruction pair for the other.
      writer.add_undefined_symbol(reloc[:symbol]) unless defined_names.include?(reloc[:symbol])
      if reloc[:kind] == :call
        writer.add_text_relocation(offset: reloc[:offset], symbol: reloc[:symbol])
      else
        writer.add_func_relocation(offset: reloc[:offset], symbol: reloc[:symbol])
      end
    when :string
      # A reference from .text into .rodata, resolved against the .rodata
      # section symbol and displaced by the string's byte offset within the
      # pool. The offset is passed unbiased: whatever a target's own field
      # placement demands on top of it belongs to its machine description,
      # not to this machine-independent layer.
      writer.add_rodata_relocation(offset: reloc[:offset],
                                   addend: string_offsets[reloc[:string_id]])
    when :global
      # A reference from .text addressing a file-scope variable, resolved
      # against that variable's own object symbol. A variable only declared
      # `extern` in this unit (referenced but never defined here) has no
      # local object symbol, so it becomes an undefined symbol for the
      # linker, just like an undefined call target.
      writer.add_undefined_symbol(reloc[:symbol]) unless known_names.include?(reloc[:symbol])
      writer.add_global_relocation(offset: reloc[:offset], symbol: reloc[:symbol])
    when :got
      # A PIC access (-fPIC) through the Global Offset Table: a load of the
      # address of a symbol this unit does not define — an extern file-scope
      # object, or an external function whose address is taken — from that
      # symbol's GOT slot. The symbol becomes an undefined symbol for the
      # linker to bind (unless another part of this unit defines it); which
      # relocation addresses the slot is the machine description's business.
      writer.add_undefined_symbol(reloc[:symbol]) unless known_names.include?(reloc[:symbol])
      writer.add_got_relocation(offset: reloc[:offset], symbol: reloc[:symbol])
    end
  end

  # A `__attribute__((constructor))` / `((destructor))` function becomes a
  # slot in this object's .init_array / .fini_array pointing at its own text
  # symbol. Registered after the text loop, so every function symbol the
  # slots resolve against is already in the symbol table.
  ir_program.array_entries.each do |entry|
    writer.add_array_entry(kind: entry.kind, priority: entry.priority, symbol: entry.symbol)
  end

  writer.set_rodata(rodata) unless rodata.empty?
  writer.add_text_section(text)
  writer.to_binary
end