From a64865dca58b0a904af9ad78b1f5f3135ad35601 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Fri, 21 Aug 2026 00:42:31 +0900 Subject: [PATCH 1/4] add archive reader --- lib/caotral/binary/archive.rb | 14 ++++++++++++ lib/caotral/binary/archive/header.rb | 13 +++++++++++ lib/caotral/binary/archive/member.rb | 20 ++++++++++++++++ lib/caotral/binary/archive/reader.rb | 32 ++++++++++++++++++++++++++ lib/caotral/binary/elf.rb | 3 +++ test/caotral/binary/archive_test.rb | 34 ++++++++++++++++++++++++++++ test/test_suite.rb | 15 ++++++++++-- 7 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 lib/caotral/binary/archive.rb create mode 100644 lib/caotral/binary/archive/header.rb create mode 100644 lib/caotral/binary/archive/member.rb create mode 100644 lib/caotral/binary/archive/reader.rb create mode 100644 test/caotral/binary/archive_test.rb diff --git a/lib/caotral/binary/archive.rb b/lib/caotral/binary/archive.rb new file mode 100644 index 00000000..138f651a --- /dev/null +++ b/lib/caotral/binary/archive.rb @@ -0,0 +1,14 @@ +require_relative "archive/header" +require_relative "archive/member" + +module Caotral + module Binary + class Archive + attr_reader :members + + def initialize + @members = [] + 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..63c94e29 --- /dev/null +++ b/lib/caotral/binary/archive/header.rb @@ -0,0 +1,13 @@ +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 + 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..d479f2bc --- /dev/null +++ b/lib/caotral/binary/archive/member.rb @@ -0,0 +1,20 @@ +require_relative "header" + +module Caotral + module Binary + class Archive + class Member + attr_reader :body, :header + + def initialize(bin) + @header = Caotral::Binary::Archive::Header.new(bin.read(60)) + size = @header.size + @body_bin = bin.read(@header.size) + bin.read(1) if size.odd? + end + + def read! = @body = "" + 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..44118602 --- /dev/null +++ b/lib/caotral/binary/archive/reader.rb @@ -0,0 +1,32 @@ +require_relative "../archive" + +module Caotral + module Binary + class Archive + class Reader + attr_reader :archive + + def initialize(path) + input = decision(path) + @bin = StringIO.new(input.read) + @archive = Caotral::Binary::Archive.new + end + + def read! + _magic = @bin.read(8) + archive.members << Caotral::Binary::Archive::Member.new(@bin) until @bin.eof? + end + + private + def decision(path) + case path + when String, Pathname + File.open(File.expand_path(path.to_s), "rb") + else + raise ArgumentError, "wrong input type" + end + end + 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/test/caotral/binary/archive_test.rb b/test/caotral/binary/archive_test.rb new file mode 100644 index 00000000..76538fd5 --- /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 BuildFixtureHelper + + attr_reader :output, :inputs + + def teardown + @inputs.each { |name| File.delete(name) } + File.delete(output) + end + + def test_archive + @output = "archive.a" + @inputs = [] + [ + "sample/C/multi-file-link-a.c", + "sample/C/add.c" + ].each do |input| + output = File.basename(input, ".c") + ".s" + compile_fixture(output:, input:, options: ["-fno-pie"]) + input = output + asm = output + output = File.basename(input, ".s") + ".o" + assemble_fixture(output:, input:) + File.delete(asm) + @inputs << output + end + + create_archive(output:, inputs:) + archive = Caotral::Binary::Archive::Reader.new(output) + archive.read! + end +end diff --git a/test/test_suite.rb b/test/test_suite.rb index 0a526c66..35d47fa7 100644 --- a/test/test_suite.rb +++ b/test/test_suite.rb @@ -1,10 +1,21 @@ require "caotral" require "test/unit" -require "pathname" require "open3" +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) + def assemble_fixture(output:, input:) = fixture_command("as", "-o", output, input) + def create_archive(output:, inputs:) = fixture_command("ar", "rcsD", output, *inputs) +end + module TestProcessHelper - private def check_process(status) exit_code = status >> 8 handle_code = status & 0x7f From 0d9ae2d5400c791eb3c783746761c837256c3e35 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Sat, 22 Aug 2026 01:19:05 +0900 Subject: [PATCH 2/4] add archive reader support --- lib/caotral/binary/archive.rb | 9 +++++- lib/caotral/binary/archive/error.rb | 9 ++++++ lib/caotral/binary/archive/header.rb | 3 ++ lib/caotral/binary/archive/member.rb | 20 +++++++------ lib/caotral/binary/archive/name_table.rb | 33 ++++++++++++++++++++++ lib/caotral/binary/archive/reader.rb | 24 +++++++++++++++- lib/caotral/binary/archive/symbol_table.rb | 31 ++++++++++++++++++++ lib/caotral/binary/elf/reader.rb | 2 ++ test/caotral/binary/archive_test.rb | 31 ++++++++++++++------ 9 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 lib/caotral/binary/archive/error.rb create mode 100644 lib/caotral/binary/archive/name_table.rb create mode 100644 lib/caotral/binary/archive/symbol_table.rb diff --git a/lib/caotral/binary/archive.rb b/lib/caotral/binary/archive.rb index 138f651a..ebea80fb 100644 --- a/lib/caotral/binary/archive.rb +++ b/lib/caotral/binary/archive.rb @@ -1,13 +1,20 @@ +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_reader :members + attr_accessor :name_table + attr_reader :members, :symbol_tables def initialize @members = [] + @symbol_tables = [] + @name_table = nil 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 index 63c94e29..dbc26403 100644 --- a/lib/caotral/binary/archive/header.rb +++ b/lib/caotral/binary/archive/header.rb @@ -7,6 +7,9 @@ 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 diff --git a/lib/caotral/binary/archive/member.rb b/lib/caotral/binary/archive/member.rb index d479f2bc..4f7c0b30 100644 --- a/lib/caotral/binary/archive/member.rb +++ b/lib/caotral/binary/archive/member.rb @@ -1,19 +1,21 @@ -require_relative "header" - module Caotral module Binary class Archive class Member - attr_reader :body, :header + attr_reader :elf, :offset, :name - def initialize(bin) - @header = Caotral::Binary::Archive::Header.new(bin.read(60)) - size = @header.size - @body_bin = bin.read(@header.size) - bin.read(1) if size.odd? + def initialize(name:, body:, offset:) + @name = name + @body = StringIO.new(body) + @offset = offset end - def read! = @body = "" + def read! + reader = Caotral::Binary::ELF::Reader.new(input: @body) + reader.read + @elf = reader.context + self + 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 index 44118602..58b658e0 100644 --- a/lib/caotral/binary/archive/reader.rb +++ b/lib/caotral/binary/archive/reader.rb @@ -14,7 +14,29 @@ def initialize(path) def read! _magic = @bin.read(8) - archive.members << Caotral::Binary::Archive::Member.new(@bin) until @bin.eof? + 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:).read! + end + @bin.read(1) if size.odd? + end end private diff --git a/lib/caotral/binary/archive/symbol_table.rb b/lib/caotral/binary/archive/symbol_table.rb new file mode 100644 index 00000000..e19a5721 --- /dev/null +++ b/lib/caotral/binary/archive/symbol_table.rb @@ -0,0 +1,31 @@ +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 + end + end + end +end 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/test/caotral/binary/archive_test.rb b/test/caotral/binary/archive_test.rb index 76538fd5..5d05c3a2 100644 --- a/test/caotral/binary/archive_test.rb +++ b/test/caotral/binary/archive_test.rb @@ -5,30 +5,43 @@ class Caotral::Binary::ArchiveTest < Test::Unit::TestCase attr_reader :output, :inputs - def teardown - @inputs.each { |name| File.delete(name) } - File.delete(output) - end + def teardown = @garbage_box.each { |name| File.delete(name) if File.exist?(name) } def test_archive @output = "archive.a" + @garbage_box = [@output] @inputs = [] [ "sample/C/multi-file-link-a.c", "sample/C/add.c" ].each do |input| output = File.basename(input, ".c") + ".s" + @garbage_box << output compile_fixture(output:, input:, options: ["-fno-pie"]) input = output - asm = output output = File.basename(input, ".s") + ".o" + @garbage_box << output assemble_fixture(output:, input:) - File.delete(asm) @inputs << output end create_archive(output:, inputs:) - archive = Caotral::Binary::Archive::Reader.new(output) - archive.read! - end + 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 From d9e7a1696f2ea157283677a0a64438c6be3d8d7c Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Sun, 23 Aug 2026 23:59:45 +0900 Subject: [PATCH 3/4] support archive inputs in self linker --- lib/caotral/binary/archive.rb | 3 + lib/caotral/binary/archive/member.rb | 4 +- lib/caotral/binary/archive/reader.rb | 9 ++- lib/caotral/binary/archive/symbol_table.rb | 3 + lib/caotral/linker.rb | 15 +++-- lib/caotral/linker/builder.rb | 64 ++++++++++++------- sample/C/archive-recursive.c | 5 ++ test/caotral/binary/archive_test.rb | 23 ++----- .../linker/integration/archive_test.rb | 48 ++++++++++++++ test/caotral/linker/writer_test.rb | 4 +- test/test_suite.rb | 43 ++++++++++++- 11 files changed, 167 insertions(+), 54 deletions(-) create mode 100644 sample/C/archive-recursive.c create mode 100644 test/caotral/linker/integration/archive_test.rb diff --git a/lib/caotral/binary/archive.rb b/lib/caotral/binary/archive.rb index ebea80fb..548c9305 100644 --- a/lib/caotral/binary/archive.rb +++ b/lib/caotral/binary/archive.rb @@ -16,6 +16,9 @@ def initialize @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/member.rb b/lib/caotral/binary/archive/member.rb index 4f7c0b30..49890375 100644 --- a/lib/caotral/binary/archive/member.rb +++ b/lib/caotral/binary/archive/member.rb @@ -10,10 +10,12 @@ def initialize(name:, body:, offset:) @offset = offset end - def read! + def read + return self if @elf reader = Caotral::Binary::ELF::Reader.new(input: @body) reader.read @elf = reader.context + @body = nil self end end diff --git a/lib/caotral/binary/archive/reader.rb b/lib/caotral/binary/archive/reader.rb index 58b658e0..bc583ff0 100644 --- a/lib/caotral/binary/archive/reader.rb +++ b/lib/caotral/binary/archive/reader.rb @@ -10,6 +10,8 @@ def initialize(path) input = decision(path) @bin = StringIO.new(input.read) @archive = Caotral::Binary::Archive.new + ensure + input.close end def read! @@ -33,10 +35,13 @@ def read! raw_name.delete_suffix("/") end - archive.members << Caotral::Binary::Archive::Member.new(name:, body:, offset:).read! + archive.members << Caotral::Binary::Archive::Member.new(name:, body:, offset:) end @bin.read(1) if size.odd? end + @archive + ensure + @bin.close end private @@ -44,6 +49,8 @@ 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 diff --git a/lib/caotral/binary/archive/symbol_table.rb b/lib/caotral/binary/archive/symbol_table.rb index e19a5721..15305cb2 100644 --- a/lib/caotral/binary/archive/symbol_table.rb +++ b/lib/caotral/binary/archive/symbol_table.rb @@ -25,6 +25,9 @@ def parse! @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 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 index 5d05c3a2..f97cd360 100644 --- a/test/caotral/binary/archive_test.rb +++ b/test/caotral/binary/archive_test.rb @@ -1,31 +1,18 @@ require_relative "../../test_suite" class Caotral::Binary::ArchiveTest < Test::Unit::TestCase + include CommonSetupHelper include BuildFixtureHelper - attr_reader :output, :inputs - - def teardown = @garbage_box.each { |name| File.delete(name) if File.exist?(name) } - def test_archive @output = "archive.a" - @garbage_box = [@output] + @generated = [@output] @inputs = [] - [ + inputs = [ "sample/C/multi-file-link-a.c", "sample/C/add.c" - ].each do |input| - output = File.basename(input, ".c") + ".s" - @garbage_box << output - compile_fixture(output:, input:, options: ["-fno-pie"]) - input = output - output = File.basename(input, ".s") + ".o" - @garbage_box << output - assemble_fixture(output:, input:) - @inputs << output - end - - create_archive(output:, inputs:) + ] + create_archive(output: @output, inputs:) reader = Caotral::Binary::Archive::Reader.new(output) reader.read! archive = reader.archive 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 35d47fa7..862a2c2d 100644 --- a/test/test_suite.rb +++ b/test/test_suite.rb @@ -2,6 +2,13 @@ require "test/unit" 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) @@ -10,9 +17,39 @@ def fixture_command(*command) raise "command failed: #{command.join(" ")}\n#{e}" end - def compile_fixture(output:, input:, options: []) = fixture_command("gcc", *options, "-S", "-o", output, input) - def assemble_fixture(output:, input:) = fixture_command("as", "-o", output, input) - def create_archive(output:, inputs:) = fixture_command("ar", "rcsD", output, *inputs) + 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 From 185a9317ec1d596d8572c863b1c7650ae3a1f2b7 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Mon, 24 Aug 2026 17:48:42 +0900 Subject: [PATCH 4/4] validate archive inputs --- lib/caotral/binary/archive/reader.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/caotral/binary/archive/reader.rb b/lib/caotral/binary/archive/reader.rb index bc583ff0..eb703d6a 100644 --- a/lib/caotral/binary/archive/reader.rb +++ b/lib/caotral/binary/archive/reader.rb @@ -8,14 +8,17 @@ class Reader def initialize(path) input = decision(path) - @bin = StringIO.new(input.read) - @archive = Caotral::Binary::Archive.new - ensure - input.close + begin + @bin = StringIO.new(input.read) + @archive = Caotral::Binary::Archive.new + ensure + input.close + end end def read! - _magic = @bin.read(8) + magic = @bin.read(8) + raise Caotral::Binary::Archive::Error, "unsupported archive type" unless magic == "!\n" nt = nil until @bin.eof? offset = @bin.pos