Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/caotral/binary/elf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ def initialize
end
def each(&block) = @sections.each(&block)
def [](idx) = @sections[idx]
def find_by_name(section_name) = @sections.find { section_name == it.section_name }
def select_by_name(section_name) = @sections.select { section_name == it.section_name }
def index(section_name) = @sections.index { section_name == it.section_name }
def find_by_name(section_name) = @sections.find { |s| section_name == s.section_name }
def select_by_name(section_name) = @sections.select { |s| section_name == s.section_name }
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 } }
end
end
end
1 change: 1 addition & 0 deletions lib/caotral/binary/elf/section/symtab.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def set!(name: nil, info: nil, other: nil, shndx: nil, value: nil, size: nil)

def name_offset = @name.pack("C*").unpack1("L<")
def value = @value.pack("C*").unpack1("Q<")
def info = @info.pack("C*").unpack1("C")

private def bytes = [@name, @info, @other, @shndx, @value, @size]
end
Expand Down
1 change: 1 addition & 0 deletions lib/caotral/binary/elf/section_header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def size = @size.pack("C*").unpack1("Q<")
def type = SHT_BY_VALUE[@type.pack("C*").unpack1("L<")]
def info = @info.pack("C*").unpack1("L<")
def addr = @addr.pack("C*").unpack1("Q<")
def link = @link.pack("C*").unpack1("L<")

private def bytes = [@name, @type, @flags, @addr, @offset, @size, @link, @info, @addralign, @entsize]
end
Expand Down
3 changes: 3 additions & 0 deletions lib/caotral/linker.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true
require_relative "binary/elf/reader"
require_relative "linker/builder"
require_relative "linker/writer"

module Caotral
Expand Down Expand Up @@ -48,6 +49,8 @@ def gcc_libpath = @gcc_libpath ||= File.dirname(Dir.glob("/usr/lib/gcc/x86_64-*/

def to_elf(input: @input, output: @output, debug: @debug)
elf_obj = Caotral::Binary::ELF::Reader.new(input:, debug:).read
builder = Caotral::Linker::Builder.new(elf_obj:)
builder.resolve_symbols
Caotral::Linker::Writer.new(elf_obj:, output:, debug:).write
end
end
Expand Down
30 changes: 30 additions & 0 deletions lib/caotral/linker/builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "set"
require "caotral/binary/elf"

module Caotral
class Linker
class Builder
SYMTAB_BIND = { locals: 0, globals: 1, weaks: 2, }.freeze
BIND_BY_VALUE = SYMTAB_BIND.invert.freeze
attr_reader :symbols

def initialize(elf_obj:)
@elf_obj = elf_obj
@symbols = { locals: Set.new, globals: Set.new, weaks: Set.new }
end
def resolve_symbols
@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 && @symbols[bind].include?(name)
raise Caotral::Binary::ELF::Error,"cannot add into globals: #{name}"
end
@symbols[bind] << name
end
@symbols
end
end
end
end
42 changes: 36 additions & 6 deletions lib/caotral/linker/writer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ module Caotral
class Linker
class Writer
include Caotral::Binary::ELF::Utils
ALLOW_SECTIONS = %w(.text .strtab .shstrtab).freeze
R_X86_64_PC32 = 2
R_X86_64_PLT32 = 4
ALLOW_RELOCATION_TYPES = [R_X86_64_PC32, R_X86_64_PLT32].freeze
Expand All @@ -15,6 +14,7 @@ def self.write!(elf_obj:, output:, entry: nil, debug: false)
end
def initialize(elf_obj:, output:, entry: nil, debug: false)
@elf_obj, @output, @entry, @debug = elf_obj, output, entry, debug
@write_sections = write_order_sections
end
def write
f = File.open(@output, "wb")
Expand Down Expand Up @@ -58,6 +58,7 @@ def write
end
target.body = bytes
end

header.set!(entry: @entry || base_addr + text_offset)
ph.set!(type:, offset: text_offset, vaddr:, paddr:, filesz:, memsz:, flags:, align:)
text_section.header.set!(size: text_section.body.bytesize, addr: vaddr, offset: text_offset)
Expand All @@ -70,17 +71,28 @@ def write
shstrtab_offset = f.pos
f.write(shstrtab.body.names)
shstrtab.header.set!(offset: shstrtab_offset, size: shstrtab.body.names.bytesize)
write_sections = @elf_obj.sections.select { ALLOW_SECTIONS.include?(it.section_name) || it.section_name.nil? }
shoffset = f.pos
shstrndx = write_sections.index { it.section_name == ".shstrtab" }
shnum = write_sections.size
shstrndx = write_section_index(".shstrtab")
strtabndx = write_section_index(".strtab")
symtabndx = write_section_index(".symtab")
shnum = @write_sections.size
@elf_obj.header.set!(shoffset:, shnum:, shstrndx:)
names = @elf_obj.find_by_name(".shstrtab").body

write_sections.each do |section|
@write_sections.each do |section|
header = section.header
lookup_name = section.section_name
name_offset = names.offset_of(lookup_name)
section.header.set!(name: name_offset) if name_offset
name, info, link, entsize = (name_offset.nil? ? 0 : name_offset), header.info, header.link, 0
if header.type == :symtab
info = section.body.size
link = strtabndx
Comment on lines +86 to +89

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Set symtab sh_info to local symbol count

In ELF, a symtab section’s sh_info must be the index of the first non-local symbol (i.e., the count of local symbols). Here info = section.body.size sets it to the total symbol count, so any global symbols are effectively marked as local to consumers that rely on sh_info. This shows up whenever the input has globals (e.g., the main symbol from the assembler or GCC-produced objects), and it can cause tools/linkers to misclassify or ignore globals.

Useful? React with 👍 / 👎.

entsize = header.entsize.nonzero? || 24
elsif [:rel, :rela].include?(header.type)
link = symtabndx
info = ref_index(section)
end
header.set!(name:, info:, link:, entsize:)
f.write(section.header.build)
end

Expand All @@ -90,6 +102,24 @@ def write
ensure
f.close if f
end

private
def write_order_sections
write_order = []
write_order << @elf_obj.sections.find { |s| s.section_name.nil? }
write_order << @elf_obj.find_by_name(".text")
write_order << @elf_obj.find_by_name(".symtab")
write_order << @elf_obj.find_by_name(".strtab")
write_order.concat(@elf_obj.select_by_names([/\.rel\./, /\.rela\./]))
write_order << @elf_obj.find_by_name(".shstrtab")
Comment on lines +110 to +114

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid emitting rel sections without target sections

The write order now includes every .rel.*/.rela.* section but only keeps .text, .symtab, .strtab, and .shstrtab. For objects that also have relocations for other sections (e.g., GCC adds .rela.eh_frame alongside .eh_frame), this emits relocation sections whose referenced target section is omitted from the output. That leaves sh_info pointing at a section index that doesn’t exist in the new section table, producing an invalid ELF that can confuse tooling. Either include the referenced sections or drop relocations whose targets aren’t being written.

Useful? React with 👍 / 👎.

write_order.compact
end
def write_section_index(section_name) = @write_sections.index { it.section_name == section_name }
def ref_index(section)
section_name = section.section_name
ref = @elf_obj.select_by_names(section_name.split(".").filter { |sn| !sn.empty? && sn != "rel" && sn != "rela" }.map { "." + it }).first
write_section_index(ref.section_name)
end
end
end
end
1 change: 1 addition & 0 deletions sig/caotral/binary/elf.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ class Caotral::Binary::ELF
def find_by_name: (String | Symbol) -> Caotral::Binary::ELF::Section?
def select_by_name: (String | Symbol) -> Array[Caotral::Binary::ELF::Section]
def index: (String | Symbol) -> Integer?
def select_by_names: (Array[String | Regexp]) -> Array[Caotral::Binary::ELF::Section]
end
1 change: 1 addition & 0 deletions sig/caotral/binary/elf/section/symtab.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ class Caotral::Binary::ELF::Section::Symtab
) -> self

def name_offset: () -> Integer
def info: () -> Integer
def value: () -> Integer
end
9 changes: 9 additions & 0 deletions sig/caotral/linker/builder.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Caotral::Linker::Builder
@elf_obj: Caotral::Binary::ELF
@symbols: Hash[Symbol, Set[String]]

attr_reader symbols: Hash[Symbol, Set[String]]

def initialize: (elf_obj: Caotral::Binary::ELF) -> void
def resolve_symbols: () -> Hash[Symbol, Set[String]]
end
2 changes: 1 addition & 1 deletion test/caotral/linker/writer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_write
written_output = Caotral::Linker::Writer.write!(elf_obj: @elf_obj, output: "write.o", debug: false)
read_written_elf = Caotral::Binary::ELF::Reader.read!(input: written_output, debug: false)
assert_equal @elf_obj.header.shoffset, read_written_elf.header.shoffset
assert_equal 4, read_written_elf.sections.size
assert_equal 5, read_written_elf.sections.size
assert_equal 0x401000, read_written_elf.header.entry
end

Expand Down