diff --git a/lib/caotral/binary/archive.rb b/lib/caotral/binary/archive.rb new file mode 100644 index 00000000..548c9305 --- /dev/null +++ b/lib/caotral/binary/archive.rb @@ -0,0 +1,24 @@ +require_relative "archive/error" + +require_relative "archive/header" +require_relative "archive/member" +require_relative "archive/name_table" +require_relative "archive/symbol_table" + +module Caotral + module Binary + class Archive + attr_accessor :name_table + attr_reader :members, :symbol_tables + + def initialize + @members = [] + @symbol_tables = [] + @name_table = nil + end + + def symbol_offset(name) = @symbol_tables.map { |symbol_table| symbol_table.offset_of(name) }&.compact&.first + def find_member_by_offset(offset) = @members.find { |member| member.offset == offset } + end + end +end diff --git a/lib/caotral/binary/archive/error.rb b/lib/caotral/binary/archive/error.rb new file mode 100644 index 00000000..031c74cc --- /dev/null +++ b/lib/caotral/binary/archive/error.rb @@ -0,0 +1,9 @@ +module Caotral + module Binary + class Archive + class Error < StandardError + end + end + end +end + diff --git a/lib/caotral/binary/archive/header.rb b/lib/caotral/binary/archive/header.rb new file mode 100644 index 00000000..dbc26403 --- /dev/null +++ b/lib/caotral/binary/archive/header.rb @@ -0,0 +1,16 @@ +module Caotral + module Binary + class Archive + class Header + attr_reader :name, :size, :fmsg + def initialize(str) + @name, size, @fmsg = str.unpack("A16x12x6x6x8A10a2") + @size = Integer(size, 10) + end + + def symbol_table? = @name == "/" + def name_table? = @name == "//" + end + end + end +end diff --git a/lib/caotral/binary/archive/member.rb b/lib/caotral/binary/archive/member.rb new file mode 100644 index 00000000..49890375 --- /dev/null +++ b/lib/caotral/binary/archive/member.rb @@ -0,0 +1,24 @@ +module Caotral + module Binary + class Archive + class Member + attr_reader :elf, :offset, :name + + def initialize(name:, body:, offset:) + @name = name + @body = StringIO.new(body) + @offset = offset + end + + def read + return self if @elf + reader = Caotral::Binary::ELF::Reader.new(input: @body) + reader.read + @elf = reader.context + @body = nil + self + end + end + end + end +end diff --git a/lib/caotral/binary/archive/name_table.rb b/lib/caotral/binary/archive/name_table.rb new file mode 100644 index 00000000..c48952b7 --- /dev/null +++ b/lib/caotral/binary/archive/name_table.rb @@ -0,0 +1,33 @@ +module Caotral + module Binary + class Archive + class NameTable + attr_reader :names + + def initialize(body:) + @body = StringIO.new(body) + @names = [] + @name_offsets = {} + end + + def parse! + until @body.eof? + offset = @body.pos + entry = @body.gets("/\n") + break if entry == "\n" + name = entry.delete_suffix("/\n") + @names << name + @name_offsets[offset] = name + end + self + end + + def resolve(offset) + @name_offsets.fetch(offset) do + raise Caotral::Binary::Archive::Error, "invalid name offset: #{offset}" + end + end + end + end + end +end diff --git a/lib/caotral/binary/archive/reader.rb b/lib/caotral/binary/archive/reader.rb new file mode 100644 index 00000000..eb703d6a --- /dev/null +++ b/lib/caotral/binary/archive/reader.rb @@ -0,0 +1,64 @@ +require_relative "../archive" + +module Caotral + module Binary + class Archive + class Reader + attr_reader :archive + + def initialize(path) + input = decision(path) + begin + @bin = StringIO.new(input.read) + @archive = Caotral::Binary::Archive.new + ensure + input.close + end + end + + def read! + magic = @bin.read(8) + raise Caotral::Binary::Archive::Error, "unsupported archive type" unless magic == "!\n" + nt = nil + until @bin.eof? + offset = @bin.pos + header = Caotral::Binary::Archive::Header.new(@bin.read(60)) + size = header.size + body = @bin.read(size) + raw_name = header.name + case header.name + when "/" + archive.symbol_tables << Caotral::Binary::Archive::SymbolTable.new(body:).parse! + when "//" + nt = archive.name_table = Caotral::Binary::Archive::NameTable.new(body:).parse! + else + name = if /\A\/(?\d+)\z/ =~ raw_name + nt.resolve(Integer(idx, 10)) + else + raw_name.delete_suffix("/") + end + + archive.members << Caotral::Binary::Archive::Member.new(name:, body:, offset:) + end + @bin.read(1) if size.odd? + end + @archive + ensure + @bin.close + end + + private + def decision(path) + case path + when String, Pathname + File.open(File.expand_path(path.to_s), "rb") + when StringIO + path + else + raise ArgumentError, "wrong input type" + end + end + end + end + end +end diff --git a/lib/caotral/binary/archive/symbol_table.rb b/lib/caotral/binary/archive/symbol_table.rb new file mode 100644 index 00000000..15305cb2 --- /dev/null +++ b/lib/caotral/binary/archive/symbol_table.rb @@ -0,0 +1,34 @@ +module Caotral + module Binary + class Archive + class SymbolTable + attr_reader :symbols + + Symbol = Data.define(:name, :offset) + def initialize(body:) + @body = StringIO.new(body) + @symbols = [] + end + + def parse! + count = @body.read(4).unpack1("N") + + offsets = count.times.map { @body.read(4).unpack1("N") } + names = count.times.map do + entry = @body.gets("\0") + unless entry&.end_with?("\0") + raise Caotral::Binary::Archive::Error, "invalid symbol table" + end + + entry.delete_suffix("\0") + end + @symbols = names.zip(offsets).map { |name, offset| Symbol.new(name:, offset:) } + self + end + + def names = @symbols.map(&:name) + def offset_of(name) = @symbols.find { |symbol| symbol.name == name }&.offset + end + end + end +end diff --git a/lib/caotral/binary/elf.rb b/lib/caotral/binary/elf.rb index 47fae990..aacf43da 100644 --- a/lib/caotral/binary/elf.rb +++ b/lib/caotral/binary/elf.rb @@ -11,6 +11,9 @@ require_relative "elf/section_header" require_relative "elf/symtab_methods" +require_relative "archive" + +require_relative "archive/reader" require_relative "elf/reader" module Caotral diff --git a/lib/caotral/binary/elf/reader.rb b/lib/caotral/binary/elf/reader.rb index f78337bc..35c44bef 100644 --- a/lib/caotral/binary/elf/reader.rb +++ b/lib/caotral/binary/elf/reader.rb @@ -209,6 +209,8 @@ def decision(input) case input when String, Pathname File.open(File.expand_path(input.to_s), "rb") + when StringIO + input else raise ArgumentError, "wrong input type" end diff --git a/lib/caotral/linker.rb b/lib/caotral/linker.rb index 33687110..48f0acf6 100644 --- a/lib/caotral/linker.rb +++ b/lib/caotral/linker.rb @@ -8,14 +8,14 @@ module Caotral class Linker - def self.link!(inputs:, output: "a.out", linker: "mold", debug: false, shared: false, executable: true, pie: false, needed: []) - new(inputs:, output:, linker:, debug:, shared:, executable:, pie:, needed:).link + def self.link!(inputs:, output: "a.out", linker: "mold", debug: false, shared: false, executable: true, pie: false, needed: [], archives: []) + new(inputs:, output:, linker:, debug:, shared:, executable:, pie:, needed:, archives:).link end - def initialize(inputs:, output: "a.out", linker: "mold", linker_options: [], executable: true, shared: false, pie: false, debug: false, needed: []) + def initialize(inputs:, output: "a.out", linker: "mold", linker_options: [], executable: true, shared: false, pie: false, debug: false, needed: [], archives: []) @inputs, @output, @linker = inputs, output, linker @options = linker_options - @needed = needed + @needed, @archives = needed, archives @executable = shared ? false : executable @debug, @shared, @pie = debug, shared, pie end @@ -54,12 +54,13 @@ def link_command(inputs: @inputs, output: @output) def libpath = @libpath ||= File.dirname(Dir.glob("/usr/lib*/**/crti.o").last) def gcc_libpath = @gcc_libpath ||= File.dirname(Dir.glob("/usr/lib/gcc/x86_64-*/*/crtbegin.o").last) - def to_elf(inputs: @inputs, output: @output, debug: @debug, shared: @shared, executable: @executable, pie: @pie, needed: @needed) + def to_elf(inputs: @inputs, output: @output, debug: @debug, shared: @shared, executable: @executable, pie: @pie, needed: @needed, archives: @archives) raise Caotral::Binary::ELF::Error, "disallow both mode: shared and PIE" if shared && pie elf_objs = inputs.map { |input| Caotral::Binary::ELF::Reader.new(input:, debug:).read } - builder = Caotral::Linker::Builder.new(elf_objs:, debug:, shared:, executable:, pie:, needed:) - builder.resolve_symbols + builder = Caotral::Linker::Builder.new(elf_objs:, debug:, shared:, executable:, pie:, needed:, archives:) + builder.resolve_symbols! + builder.extract_archive_members! unless archives.empty? elf = builder.build metadata = builder.linker_metadata Caotral::Linker::Layout.new(elf:, shared:, executable:, pie:).apply! diff --git a/lib/caotral/linker/builder.rb b/lib/caotral/linker/builder.rb index 259294f0..3c7ef8b1 100644 --- a/lib/caotral/linker/builder.rb +++ b/lib/caotral/linker/builder.rb @@ -31,8 +31,9 @@ class Builder attr_reader :symbols, :linker_metadata - def initialize(elf_objs:, executable: true, debug: false, shared: false, pie: false, needed: []) + def initialize(elf_objs:, executable: true, debug: false, shared: false, pie: false, needed: [], archives: []) @elf_objs = elf_objs + @archives = archives.map { |archive| Caotral::Binary::Archive::Reader.new(archive).read! } @executable, @debug, @shared, @pie, @needed = executable, debug, shared, pie, needed @symbols = { locals: Set.new, @@ -486,33 +487,52 @@ def build elf end - def resolve_symbols - @elf_objs.each do |elf_obj| - elf_obj.find_by_name(".symtab").body.each do |symtab| - name = symtab.name_string - next if name.empty? - info = symtab.info - bind = BIND_BY_VALUE.fetch(info >> 4) - if bind == :globals - if symtab.shndx == 0 - @symbols[bind][:undefined] << name unless @symbols[bind][:defined].include?(name) - else - if @symbols[bind][:defined].keys.include?(name) - raise Caotral::Binary::ELF::Error,"cannot add into globals: #{name}" - end - @symbols[bind][:undefined].delete(name) - @symbols[bind][:defined][name] = symtab.shndx - end - else - @symbols[bind] << name - end + def resolve_symbols! = @elf_objs.each { |elf_obj| resolve_symbol_for(elf_obj) } + def extract_archive_members! + resolved = Set[] + loop do + elfs = [] + @symbols[:globals][:undefined].each do |symbol| + archive = implement_symbol_archive(symbol) + next if archive.nil? + offset = archive.symbol_offset(symbol) + member = archive.find_member_by_offset(offset) + next if member.nil? + next unless resolved.add?(member) + elfs << member.read.elf end + break if elfs.empty? + elfs.each { |elf| resolve_symbol_for(elf) } + @elf_objs += elfs end - @symbols end private + def implement_symbol_archive(symbol) = @archives.find { |archive| archive.symbol_tables.any? { |symbol_table| symbol_table.names.include?(symbol) } } + + def resolve_symbol_for(elf_obj) + elf_obj.find_by_name(".symtab").body.each do |symtab| + name = symtab.name_string + next if name.empty? + info = symtab.info + bind = BIND_BY_VALUE.fetch(info >> 4) + if bind == :globals + if symtab.shndx == 0 + @symbols[bind][:undefined] << name unless @symbols[bind][:defined].include?(name) + else + if @symbols[bind][:defined].keys.include?(name) + raise Caotral::Binary::ELF::Error,"cannot add into globals: #{name}" + end + @symbols[bind][:undefined].delete(name) + @symbols[bind][:defined][name] = symtab.shndx + end + else + @symbols[bind] << name + end + end + end + def build_shared_dynamic_sections dynstr_section = Caotral::Binary::ELF::Section.new( body: Caotral::Binary::ELF::Section::Strtab.new("\0".b), diff --git a/sample/C/archive-recursive.c b/sample/C/archive-recursive.c new file mode 100644 index 00000000..e704b6c0 --- /dev/null +++ b/sample/C/archive-recursive.c @@ -0,0 +1,5 @@ +int add(int x, int y); + +int foo(void) { + return add(40, 2); +} diff --git a/test/caotral/binary/archive_test.rb b/test/caotral/binary/archive_test.rb new file mode 100644 index 00000000..f97cd360 --- /dev/null +++ b/test/caotral/binary/archive_test.rb @@ -0,0 +1,34 @@ +require_relative "../../test_suite" + +class Caotral::Binary::ArchiveTest < Test::Unit::TestCase + include CommonSetupHelper + include BuildFixtureHelper + + def test_archive + @output = "archive.a" + @generated = [@output] + @inputs = [] + inputs = [ + "sample/C/multi-file-link-a.c", + "sample/C/add.c" + ] + create_archive(output: @output, inputs:) + reader = Caotral::Binary::Archive::Reader.new(output) + reader.read! + archive = reader.archive + mul = archive.members.first + add = archive.members.last + symbol_tables = archive.symbol_tables + name_table = archive.name_table + assert_equal(2, archive.members.size) + assert_equal("multi-file-link-a.o", mul.name) + assert_equal("add.o", add.name) + + assert_equal(1, symbol_tables.size) + assert_equal("foo", symbol_tables.first.symbols.first.name) + assert_equal(170, symbol_tables.first.symbols.first.offset) + assert_not_nil(name_table) + assert_equal(["multi-file-link-a.o"], name_table.names) + assert_equal("multi-file-link-a.o", name_table.resolve(0)) + end +end diff --git a/test/caotral/linker/integration/archive_test.rb b/test/caotral/linker/integration/archive_test.rb new file mode 100644 index 00000000..c12725e9 --- /dev/null +++ b/test/caotral/linker/integration/archive_test.rb @@ -0,0 +1,48 @@ +require_relative "../../../test_suite" + +class Caotral::Linker::ArchiveTest < Test::Unit::TestCase + include CommonSetupHelper + include BuildFixtureHelper + include TestProcessHelper + + def test_integrate_archive_file + @output = "archive" + output = "archive.a" + @generated << @output + @generated << output + inputs = [ + "sample/C/multi-file-link-a.c", + "sample/C/add.c" + ] + create_archive(output:, inputs:, options: ["-fno-pie"]) + create_object(output: "multi-file-link-b.o", input: "sample/C/multi-file-link-b.c") + @inputs << "multi-file-link-b.o" + archives = [output] + Caotral::Linker.link!(output: @output, inputs: @inputs, archives:, linker: "self") + elf = Caotral::Binary::ELF::Reader.read!(input: @output) + IO.popen("./#{@output}").close + exit_code, handle_code = check_process($?.to_i) + assert_equal(42, exit_code) + assert_equal(0, handle_code) + end + + def test_extract_dependent_archive_member + @output = "recursive-archive" + output = "recursive-archive.a" + @generated << @output + @generated << output + inputs = [ + "sample/C/archive-recursive.c", + "sample/C/add.c" + ] + create_archive(output:, inputs:, options: ["-fno-pie"]) + create_object(output: "multi-file-link-b.o", input: "sample/C/multi-file-link-b.c") + @inputs << "multi-file-link-b.o" + archives = [output] + Caotral::Linker.link!(output: @output, inputs: @inputs, archives:, linker: "self") + IO.popen("./#{@output}").close + exit_code, handle_code = check_process($?.to_i) + assert_equal(42, exit_code) + assert_equal(0, handle_code) + end +end diff --git a/test/caotral/linker/writer_test.rb b/test/caotral/linker/writer_test.rb index 4a132405..d0b67d85 100644 --- a/test/caotral/linker/writer_test.rb +++ b/test/caotral/linker/writer_test.rb @@ -5,7 +5,7 @@ def setup Caotral.assemble(input: "sample/assembler/plus.s", assembler: "self", output: "plus.o") elf_obj = Caotral::Binary::ELF::Reader.read!(input: "plus.o", debug: false) builder = Caotral::Linker::Builder.new(elf_objs: [elf_obj], debug: false) - builder.resolve_symbols + builder.resolve_symbols! @elf_obj = builder.build metadata = builder.linker_metadata Caotral::Linker::Layout.new(elf: @elf_obj, shared: false, pie: false, executable: true).apply! @@ -40,7 +40,7 @@ def test_relocation_write_and_execute IO.popen("gcc -c -fno-pic -fno-pie -o relocatable.o sample/C/rel_text.c").close input_elf = Caotral::Binary::ELF::Reader.read!(input: "relocatable.o", debug: false) builder = Caotral::Linker::Builder.new(elf_objs: [input_elf], debug: false) - builder.resolve_symbols + builder.resolve_symbols! elf = builder.build metadata = builder.linker_metadata Caotral::Linker::Layout.new(elf:, shared: false, pie: false, executable: true).apply! diff --git a/test/test_suite.rb b/test/test_suite.rb index 0a526c66..862a2c2d 100644 --- a/test/test_suite.rb +++ b/test/test_suite.rb @@ -1,10 +1,58 @@ require "caotral" require "test/unit" -require "pathname" require "open3" +module CommonSetupHelper + attr_reader :generated, :inputs, :output + + def setup = (@generated, @inputs, @output = [], [], nil) + def teardown = @generated.each { |f| File.delete(f) if File.exist?(f) } +end + +module BuildFixtureHelper + def fixture_command(*command) + o, e, s = Open3.capture3(*command) + return o if s.success? + + raise "command failed: #{command.join(" ")}\n#{e}" + end + + def compile_fixture(output:, input:, options: []) + fixture_command("gcc", *options, "-S", "-o", output, input) + output + end + + def assemble_fixture(output:, input:) + fixture_command("as", "-o", output, input) + output + end + + def build_archive(output:, inputs:) + fixture_command("ar", "rcsD", output, *inputs) + output + end + + def create_archive(output:, inputs:, options: []) + inputs = inputs.map { |input| create_object(output: File.basename(input, ".c") + ".o", input:, options:) } + build_archive(output:, inputs:) + end + + def create_object(output:, input:, options: []) + compiler_output = compile_fixture(output: File.basename(input, ".c") + ".s", input:, options:) + @generated << compiler_output + input = compiler_output + assembler_output = assemble_fixture(output:, input:) + @generated << assembler_output + assembler_output + end + + def create_objects(output:, inputs:, options: [], execution_options: []) + inputs = inputs.map { |input| create_object(output:, input:, options:) } + fixture_command("gcc", *execution_options, "-o", output, *inputs) + end +end + module TestProcessHelper - private def check_process(status) exit_code = status >> 8 handle_code = status & 0x7f