diff --git a/lib/caotral/binary/elf.rb b/lib/caotral/binary/elf.rb index 172b522..47fae99 100644 --- a/lib/caotral/binary/elf.rb +++ b/lib/caotral/binary/elf.rb @@ -9,6 +9,8 @@ require_relative "elf/section/strtab" require_relative "elf/section/symtab" require_relative "elf/section_header" +require_relative "elf/symtab_methods" + require_relative "elf/reader" module Caotral @@ -29,6 +31,14 @@ def select_by_name(section_name) = @sections.select { |s| section_name == s.sect def index(section_name) = @sections.index { |s| section_name == s.section_name } def select_by_names(section_names) = @sections.select { |section| section_names.any? { |name| name === section.section_name.to_s } } def without_sections(names) = @sections.reject { |s| names.any? { |name| name === s.section_name.to_s } } + def entry_symbol + symtab = find_by_name(".symtab") + raise Caotral::Binary::ELF::Error, "missing .symtab section!!!" unless symtab + raise Caotral::Binary::ELF::Error, "invalid .symtab section!!!" unless symtab.respond_to?(:find_defined_symbol) + symtab.find_defined_symbol("_start") || symtab.find_defined_symbol("main") + end + + def entry_start? = entry_symbol&.name_string == "_start" end end end diff --git a/lib/caotral/binary/elf/reader.rb b/lib/caotral/binary/elf/reader.rb index ed1f8a9..f78337b 100644 --- a/lib/caotral/binary/elf/reader.rb +++ b/lib/caotral/binary/elf/reader.rb @@ -6,6 +6,7 @@ class ELF class Reader attr_reader :context def self.read!(input:, debug: false, linker_options: []) = new(input:, debug:, linker_options:).read + SymbolTable = Caotral::Binary::ELF::SymbolTable def initialize(input:, debug: false, linker_options: []) @input = decision(input) @@ -99,7 +100,9 @@ def read size = sym_bin[16, 8].unpack1("Q<") name_string = @context.sections[section.header.link]&.body&.lookup(name).to_s - Caotral::Binary::ELF::Section::Symtab.new.set!(name:, info:, other:, shndx:, value:, size:, name_string:) + symtab = Caotral::Binary::ELF::Section::Symtab.new + symtab.set!(name:, info:, other:, shndx:, value:, size:, name_string:) + symtab end when :rel, :rela rela = type == :rela @@ -126,6 +129,7 @@ def read when :progbits body_bin end + section.extend(SymbolTable) if [:symtab, :dynsym].include?(type) end strtab = @context.find_by_name(".strtab") diff --git a/lib/caotral/binary/elf/symtab_methods.rb b/lib/caotral/binary/elf/symtab_methods.rb new file mode 100644 index 0000000..8c3357e --- /dev/null +++ b/lib/caotral/binary/elf/symtab_methods.rb @@ -0,0 +1,9 @@ +module Caotral + module Binary + class ELF + module SymbolTable + def find_defined_symbol(symbol_name) = body.find { |sym| sym.name_string == symbol_name && sym.shndx != 0 } + end + end + end +end diff --git a/lib/caotral/linker.rb b/lib/caotral/linker.rb index 60c4c08..3368711 100644 --- a/lib/caotral/linker.rb +++ b/lib/caotral/linker.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require_relative "binary/elf/reader" + +require_relative "binary/elf" require_relative "linker/builder" require_relative "linker/layout" require_relative "linker/finalizer" diff --git a/lib/caotral/linker/builder.rb b/lib/caotral/linker/builder.rb index 8354ba2..259294f 100644 --- a/lib/caotral/linker/builder.rb +++ b/lib/caotral/linker/builder.rb @@ -5,6 +5,8 @@ module Caotral class Linker class Builder include Caotral::Binary::ELF::Utils + NULL_BIN = "\x00".b.freeze + NOP_BIN = "\x90".b.freeze REL_TYPES = Caotral::Binary::ELF::Section::Rel::TYPES DYNAMIC_TAGS = Caotral::Binary::ELF::Section::Dynamic::TAG_TYPES SYMTAB_BIND = { locals: 0, globals: 1, weaks: 2, }.freeze @@ -15,6 +17,7 @@ class Builder SHT = Caotral::Binary::ELF::SectionHeader::SHT SHF = Caotral::Binary::ELF::SectionHeader::SHF SectionDynamic = Caotral::Binary::ELF::Section::Dynamic + SymbolTableMethods = Caotral::Binary::ELF::SymbolTable UNSUPPORTED_REL_TYPES = [ REL_TYPES[:AMD64_GOTPCREL], REL_TYPES[:AMD64_GOTPCRELX], @@ -99,6 +102,7 @@ def build section_name: ".symtab", header: Caotral::Binary::ELF::SectionHeader.new ) + symtab_section.extend(SymbolTableMethods) shstrtab_section = Caotral::Binary::ELF::Section.new( body: Caotral::Binary::ELF::Section::Strtab.new("\0".b), section_name: ".shstrtab", @@ -135,35 +139,55 @@ def build got_plt_offsets = {} text_offset = 0 rodata_offset = 0 + data_offset = 0 bss_offset = 0 + got_plt_offset = 24 bss_present = false + text_addralign = 1 + rodata_addralign = 1 + data_addralign = 1 bss_addralign = 1 - data_offset = 0 - got_plt_offset = 24 sym_by_elf = Hash.new { |h, k| h[k] = [] } dynstr, dynsym = build_shared_dynamic_sections if dynamic? - start_len = 0 if @elf_objs.any? { |elf| elf.find_by_name(".symtab").body.find { |sym| sym.name_string == "_start" } } + start_len = 0 if @executable && @elf_objs.any? { |elf| elf.entry_start? } @elf_objs.each do |elf_obj| text = elf_obj.find_by_name(".text") unless text.nil? + alignment = [text.header.addralign, 1].max + aligned_offset = align_up(start_len + text_offset, alignment) - start_len + padding_size = aligned_offset - text_offset + + text_section.body << (NOP_BIN * padding_size) + text_offsets[elf_obj.object_id] = aligned_offset text_section.body << text.body - text_offsets[elf_obj.object_id] = text_offset - size = text.body.bytesize - text_offset += size + text_offset = aligned_offset + text.build.bytesize + text_addralign = [text_addralign, alignment].max end rodata = elf_obj.find_by_name(".rodata") unless rodata.nil? + alignment = [rodata.header.addralign, 1].max + aligned_offset = align_up(rodata_offset, alignment) + padding_size = aligned_offset - rodata_offset + + rodata_section.body << (NULL_BIN * padding_size) + rodata_offsets[elf_obj.object_id] = aligned_offset rodata_section.body << rodata.body - rodata_offsets[elf_obj.object_id] = rodata_offset - rodata_offset += rodata.body.bytesize + rodata_offset = aligned_offset + rodata.build.bytesize + rodata_addralign = [rodata_addralign, alignment].max end data = elf_obj.find_by_name(".data") unless data.nil? + alignment = [data.header.addralign, 1].max + aligned_offset = align_up(data_offset, alignment) + padding_size = aligned_offset - data_offset + + data_section.body << (NULL_BIN * padding_size) + data_offsets[elf_obj.object_id] = aligned_offset data_section.body << data.body - data_offsets[elf_obj.object_id] = data_offset - data_offset += data.body.bytesize + data_offset = aligned_offset + data.build.bytesize + data_addralign = [data_addralign, alignment].max end bss = elf_obj.find_by_name(".bss") unless bss.nil? @@ -257,8 +281,13 @@ def build elsif UNSUPPORTED_REL_TYPES.include?(rel.type) raise Caotral::Binary::ELF::Error, "unsupported relocation type: #{rel.type_name}" end - offset = rel.offset + text_offsets.fetch(elf_obj.object_id, 0) - offset += start_len if section.section_name.to_s.start_with?(".rela.text", ".rel.text") + + offset = case section.section_name.to_s + when ".rela.text", ".rel.text" + rel.offset + start_len + text_offsets.fetch(elf_obj.object_id, 0) + when ".rela.data", ".rel.data" + rel.offset + data_offsets.fetch(elf_obj.object_id, 0) + end addend = rel.addend? ? rel.addend : nil new_rel = Caotral::Binary::ELF::Section::Rel.new(addend: rel.addend?) info = (sym << 32) | rel.type @@ -273,8 +302,8 @@ def build strtab_section.body.names = strtab_names.to_a.sort.join("\0") + "\0" sections << null_section - _start = symtab_section.body.find { |sym| sym.name_string == "_start" } - entry_sym = _start || symtab_section.body.find { |sym| sym.name_string == "main" } + _start = symtab_section.find_defined_symbol("_start") + entry_sym = _start || symtab_section.find_defined_symbol("main") raise Caotral::Binary::ELF::Error, "main or _start function not found" if @executable && entry_sym.nil? if _start.nil? @@ -289,12 +318,14 @@ def build addr: vaddr, offset: exec_text_offset, size: text_section.body.bytesize, - addralign: 16 + addralign: text_addralign ) sections << text_section strtab_section.header.set!(type: 3, flags: 0, addralign: 1, entsize: 0) + rodata_section.header.set!(addralign: rodata_addralign) sections << rodata_section + data_section.header.set!(addralign: data_addralign) sections << data_section if dynamic? sections << plt_section @@ -497,6 +528,8 @@ def build_shared_dynamic_sections header: Caotral::Binary::ELF::SectionHeader.new ) + dynsym_section.extend(SymbolTableMethods) + dynsym_section.header.set!(info: 1, type: SHT[:dynsym], flags: SHF[:ALLOC], addralign: 8, entsize: 24) [dynstr_section, dynsym_section,] diff --git a/lib/caotral/linker/layout.rb b/lib/caotral/linker/layout.rb index 546b22d..3d2c2ec 100644 --- a/lib/caotral/linker/layout.rb +++ b/lib/caotral/linker/layout.rb @@ -27,7 +27,6 @@ def apply! private def classify_load_sections! - allocated_sections = @elf.sections.select { |s| s.header.allocated? } read_only_sections = allocated_sections.select { |s| !s.header.execinstr? && !s.header.writable? } executable_sections = allocated_sections.select { |s| s.header.execinstr? && !s.header.writable? } writable_sections = allocated_sections.select { |s| !s.header.execinstr? && s.header.writable? } @@ -64,7 +63,14 @@ def build_program_header(type:, flags: 0) def layout_sections! @file_offset = header_end text = @elf.find_by_name(".text") - @load_bias = text ? text.header.addr - text.header.offset : 0 + entry_offset = 0 + @load_bias = 0 + if text + entry_offset = @elf.header.entry - text.header.addr + requested_bias = text.header.addr - text.header.offset + max_alignment = allocated_sections.map { |s| [s.header.addralign, 1].max }.max + @load_bias = align_up(requested_bias, max_alignment) + end @load_sections.each do |flags, sections| next if sections.empty? @@ -90,6 +96,8 @@ def layout_sections! addr = @load_bias + section.header.offset section.header.set!(addr:) end + @elf.header.set!(entry: text.header.addr + entry_offset) if @executable && text + @elf end def layout_section_headers! shstrtab = @elf.find_by_name(".shstrtab") @@ -184,6 +192,7 @@ def header_end raise Error, "must have ELF header" unless Caotral::Binary::ELF::Header === @elf.header @elf.header.build.bytesize + @elf.program_headers.sum { |ph| ph.build.bytesize } end + def allocated_sections = @elf.sections.select { |s| s.header.allocated? } end end end diff --git a/sample/assembler/aligned_data_relocation.s b/sample/assembler/aligned_data_relocation.s new file mode 100644 index 0000000..58c9510 --- /dev/null +++ b/sample/assembler/aligned_data_relocation.s @@ -0,0 +1,7 @@ +.data +.balign 32 +.globl aligned_data_reference +.type aligned_data_reference, @object +aligned_data_reference: + .long prefix_data +.size aligned_data_reference, .-aligned_data_reference diff --git a/sample/assembler/aligned_sections.s b/sample/assembler/aligned_sections.s new file mode 100644 index 0000000..b5719fb --- /dev/null +++ b/sample/assembler/aligned_sections.s @@ -0,0 +1,25 @@ +.text +.balign 32 +.globl aligned_function +.type aligned_function, @function +aligned_function: + mov aligned_rodata(%rip), %eax + add aligned_data(%rip), %eax + ret +.size aligned_function, .-aligned_function + +.section .rodata +.balign 32 +.globl aligned_rodata +.type aligned_rodata, @object +aligned_rodata: + .long 40 +.size aligned_rodata, .-aligned_rodata + +.data +.balign 32 +.globl aligned_data +.type aligned_data, @object +aligned_data: + .long 2 +.size aligned_data, .-aligned_data diff --git a/sample/assembler/alignment_prefix.s b/sample/assembler/alignment_prefix.s new file mode 100644 index 0000000..004fa9c --- /dev/null +++ b/sample/assembler/alignment_prefix.s @@ -0,0 +1,21 @@ +.text +.globl main +.type main, @function +main: + call aligned_function + ret +.size main, .-main + +.section .rodata +.globl prefix_rodata +.type prefix_rodata, @object +prefix_rodata: + .byte 0x11 +.size prefix_rodata, .-prefix_rodata + +.data +.globl prefix_data +.type prefix_data, @object +prefix_data: + .byte 0x22 +.size prefix_data, .-prefix_data diff --git a/sample/assembler/undefined_start.s b/sample/assembler/undefined_start.s new file mode 100644 index 0000000..bbd183b --- /dev/null +++ b/sample/assembler/undefined_start.s @@ -0,0 +1,12 @@ +.text +.globl main +.type main, @function +main: + mov $42, %eax + ret +.size main, .-main + +.weak _start + +.data + .long _start diff --git a/sig/caotral/binary/elf.rbs b/sig/caotral/binary/elf.rbs index e77b5d5..bba29a7 100644 --- a/sig/caotral/binary/elf.rbs +++ b/sig/caotral/binary/elf.rbs @@ -13,4 +13,6 @@ class Caotral::Binary::ELF def index: (String | Symbol) -> Integer? def select_by_names: (Array[String | Regexp]) -> Array[Caotral::Binary::ELF::Section] def without_sections: (Array[String | Regexp]) -> Array[Caotral::Binary::ELF::Section] + def entry_symbol: () -> Caotral::Binary::ELF::Section::Symtab? + def entry_start?: () -> bool end diff --git a/sig/caotral/binary/elf/symtab_methods.rbs b/sig/caotral/binary/elf/symtab_methods.rbs new file mode 100644 index 0000000..2d0927a --- /dev/null +++ b/sig/caotral/binary/elf/symtab_methods.rbs @@ -0,0 +1,3 @@ +module Caotral::Binary::ELF::SymbolTable + def find_defined_symbol: (String) -> Caotral::Binary::ELF::Section::Symtab? +end diff --git a/test/caotral/linker/builder_test.rb b/test/caotral/linker/builder_test.rb new file mode 100644 index 0000000..3c62bf6 --- /dev/null +++ b/test/caotral/linker/builder_test.rb @@ -0,0 +1,106 @@ +require_relative "../../test_suite" + +class Caotral::Linker::BuilderTest < Test::Unit::TestCase + attr_reader :inputs, :output, :elf_objs + def setup + @inputs = [] + @elf_objs = [] + end + def teardown = inputs.each { |i| File.delete(i) if File.exist?(i) } + + def test_build_alignment + [ + ["sample/assembler/alignment_prefix.s", "alignment_prefix.o"], + ["sample/assembler/aligned_sections.s", "aligned_sections.o"], + ].each do |(path, input)| + IO.popen(["as", "-o", input, path]).close + @elf_objs << Caotral::Binary::ELF::Reader.read!(input:) + @inputs << input + end + + builder = Caotral::Linker::Builder.new(elf_objs:) + elf = builder.build + text = elf.find_by_name(".text") + rodata = elf.find_by_name(".rodata") + data = elf.find_by_name(".data") + + assert_equal(32, text.header.addralign) + assert_equal(32, rodata.header.addralign) + assert_equal(32, data.header.addralign) + + assert_equal([0x90] * 9, text.body.byteslice(23, 9).bytes) + assert_equal([0x00] * 31, rodata.body.byteslice(1, 31).bytes) + assert_equal([0x00] * 31, data.body.byteslice(1, 31).bytes) + + symtab = elf.find_by_name(".symtab") + aligned_function = symtab.find_defined_symbol("aligned_function") + aligned_rodata = symtab.find_defined_symbol("aligned_rodata") + aligned_data = symtab.find_defined_symbol("aligned_data") + + assert_equal(32, aligned_function.value) + assert_equal(32, aligned_rodata.value) + assert_equal(32, aligned_data.value) + end + + def test_align_first_text_contribution_after_synthetic_start + [ + ["sample/assembler/aligned_sections.s", "aligned_sections_first.o"], + ["sample/assembler/alignment_prefix.s", "alignment_prefix_second.o"], + ].each do |(path, input)| + IO.popen(["as", "-o", input, path]).close + @elf_objs << Caotral::Binary::ELF::Reader.read!(input:) + @inputs << input + end + + builder = Caotral::Linker::Builder.new(elf_objs:) + elf = builder.build + text = elf.find_by_name(".text") + symtab = elf.find_by_name(".symtab") + aligned_function = symtab.find_defined_symbol("aligned_function") + + assert_equal([0x90] * 15, text.body.byteslice(17, 15).bytes) + assert_equal(32, aligned_function.value) + end + + def test_align_data_relocation_offset + [ + ["sample/assembler/alignment_prefix.s", "alignment_prefix_relocation.o"], + ["sample/assembler/aligned_data_relocation.s", "aligned_data_relocation.o"], + ].each do |(path, input)| + IO.popen(["as", "-o", input, path]).close + @elf_objs << Caotral::Binary::ELF::Reader.read!(input:) + @inputs << input + end + + builder = Caotral::Linker::Builder.new(elf_objs:, executable: false) + elf = builder.build + symtab = elf.find_by_name(".symtab") + symbol = symtab.find_defined_symbol("aligned_data_reference") + relocation = elf.find_by_name(".rela.data").body.first + + assert_equal(32, symbol.value) + assert_equal(symbol.value, relocation.offset) + end + + def test_undefined_start_uses_synthetic_start + input = "undefined_start.o" + IO.popen(["as", "-o", input, "sample/assembler/undefined_start.s"]).close + input_elf = Caotral::Binary::ELF::Reader.read!(input:) + @elf_objs << input_elf + @inputs << input + + input_start = input_elf.find_by_name(".symtab").body.find do |symbol| + symbol.name_string == "_start" + end + + assert_equal(0, input_start.shndx) + assert_false(input_elf.entry_start?) + + elf = Caotral::Linker::Builder.new(elf_objs:).build + text = elf.find_by_name(".text") + main = elf.find_by_name(".symtab").find_defined_symbol("main") + + assert_equal(17, main.value) + assert_equal(main.value - 5, text.body.byteslice(1, 4).unpack1("l<")) + end +end diff --git a/test/caotral/linker/integration/alignment_test.rb b/test/caotral/linker/integration/alignment_test.rb new file mode 100644 index 0000000..9dbca21 --- /dev/null +++ b/test/caotral/linker/integration/alignment_test.rb @@ -0,0 +1,47 @@ +require_relative "../../../test_suite" + +class Caotral::Linker::AlignmentTest < Test::Unit::TestCase + include TestProcessHelper + + def setup + @inputs = ["alignment_prefix_integration.o", "aligned_sections_integration.o"] + @output = "alignment_integration" + end + + def teardown + [*@inputs, @output].each do |path| + File.delete(path) if File.exist?(path) + end + end + + def test_aligned_contributions_execute + fixtures = [ + "sample/assembler/alignment_prefix.s", + "sample/assembler/aligned_sections.s", + ] + @inputs.zip(fixtures).each do |input, fixture| + IO.popen(["as", "-o", input, fixture]).close + end + + Caotral::Linker.link!(inputs: @inputs, output: @output, linker: "self") + + elf = Caotral::Binary::ELF::Reader.read!(input: @output) + symtab = elf.find_by_name(".symtab") + { + ".text" => "aligned_function", + ".rodata" => "aligned_rodata", + ".data" => "aligned_data", + }.each do |section_name, symbol_name| + section = elf.find_by_name(section_name) + symbol = symtab.find_defined_symbol(symbol_name) + + assert_equal(32, section.header.addralign) + assert_equal(0, (section.header.addr + symbol.value) % 32) + end + + _stdout, _stderr, status = Open3.capture3("./#{@output}") + exit_code, handle_code = check_process(status.to_i) + assert_equal(42, exit_code) + assert_equal(0, handle_code) + end +end diff --git a/test/caotral/linker/integration/bss_test.rb b/test/caotral/linker/integration/bss_test.rb index e47003d..df81fcd 100644 --- a/test/caotral/linker/integration/bss_test.rb +++ b/test/caotral/linker/integration/bss_test.rb @@ -63,7 +63,7 @@ def test_initialize_zero_pie_bss reader = Caotral::Binary::ELF::Reader.new(input: output, debug: false) elf = reader.read bss = elf.find_by_name(".bss") - dynamic_symbol = elf.find_by_name(".dynsym").body.find { |sym| sym.name_string == "value" } + dynamic_symbol = elf.find_by_name(".dynsym").find_defined_symbol("value") assert_equal(elf.index(".bss"), dynamic_symbol.shndx) assert_equal(:nobits, bss.header.type) @@ -92,7 +92,7 @@ def test_multiple_bss reader = Caotral::Binary::ELF::Reader.new(input: output, debug: false) elf = reader.read bss = elf.find_by_name(".bss") - aligned_symbol = elf.find_by_name(".symtab").body.find { |sym| sym.name_string == "aligned_value" } + aligned_symbol = elf.find_by_name(".symtab").find_defined_symbol("aligned_value") assert_equal(elf.index(".bss"), aligned_symbol.shndx) assert_equal(32, bss.header.size) @@ -120,7 +120,7 @@ def test_zero_size_bss reader = Caotral::Binary::ELF::Reader.new(input: output, debug: false) elf = reader.read bss = elf.find_by_name(".bss") - symbol = elf.find_by_name(".symtab").body.find { |sym| sym.name_string == "value" } + symbol = elf.find_by_name(".symtab").find_defined_symbol("value") assert_not_nil(bss) assert_equal(elf.index(".bss"), symbol.shndx) diff --git a/test/caotral/linker/layout_test.rb b/test/caotral/linker/layout_test.rb index 7c4e863..4689f6b 100644 --- a/test/caotral/linker/layout_test.rb +++ b/test/caotral/linker/layout_test.rb @@ -119,6 +119,20 @@ def test_layout_sections assert_equal(text.header.offset, xph.offset) end + def test_layout_sections_with_huge_text_alignment + text = build_dummy_section(name: ".text", body: "abc".b, addralign: 0x800000, addr: 0x400000, flags: flags(:text)) + elf.sections << text + elf.sections << shstrtab + elf.header.set!(entry: 0x400002) + layout = Caotral::Linker::Layout.new(elf:, shared: false, executable: true, pie: false) + entry_offset = elf.header.entry - text.header.addr + layout.apply! + text = elf.find_by_name(".text") + + assert_equal(0, text.header.addr % text.header.addralign) + assert_equal(entry_offset, elf.header.entry - text.header.addr) + end + private def build_dummy_section(name:, body: "".b, addralign: 1, offset: 0, flags: 0, addr: 0) header = Caotral::Binary::ELF::SectionHeader.new