diff --git a/.gitignore b/.gitignore index 2144d3d..d2d627d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /coverage/ /doc/ /pkg/ +/AGENTS.md /spec/reports/ /tmp/ /Gemfile.lock @@ -13,3 +14,5 @@ /.gem_rbs_collection /node_modules /package*.json +/*.md +!/README.md diff --git a/Gemfile b/Gemfile index abd565b..79d82bb 100644 --- a/Gemfile +++ b/Gemfile @@ -7,5 +7,6 @@ gemspec gem "rake", "~> 13.0" gem "fiddle" +gem "irb" gem "steep" gem "test-unit" diff --git a/lib/caotral/linker.rb b/lib/caotral/linker.rb index 9a73dd2..7521491 100644 --- a/lib/caotral/linker.rb +++ b/lib/caotral/linker.rb @@ -1,39 +1,51 @@ # frozen_string_literal: true -class Caotral::Linker - def self.link!(input:, output: "a.out", linker: "mold", debug: false, shared: false) = new(input:, output:, linker:, debug:, shared:).link +require_relative "linker/reader" +require_relative "linker/writer" - def initialize(input:, output: "a.out", linker: "mold", linker_options: [], shared: false, debug: false) - @input, @output, @linker = input, output, linker - @options = linker_options - @debug, @shared = debug, shared - end +module Caotral + class Linker + def self.link!(input:, output: "a.out", linker: "mold", debug: false, shared: false) = new(input:, output:, linker:, debug:, shared:).link - def link(input: @input, output: @output, debug: @debug, shared: @shared) = IO.popen(link_command).close - - def link_command(input: @input, output: @output, debug: @debug, shared: @shared) - ld_path = [] - if @shared - ld_path << "--shared" - ld_path << "#{libpath}/crti.o" - ld_path << "#{gcc_libpath}/crtbeginS.o" - ld_path << "#{gcc_libpath}/crtendS.o" - else - ld_path << "-dynamic-linker" - ld_path << "/lib64/ld-linux-x86-64.so.2" - ld_path << "#{libpath}/crt1.o" - ld_path << "#{libpath}/crti.o" - ld_path << "#{gcc_libpath}/crtbegin.o" - # for not static compile - ld_path << "#{gcc_libpath}/crtend.o" + def initialize(input:, output: "a.out", linker: "mold", linker_options: [], shared: false, debug: false) + @input, @output, @linker = input, output, linker + @options = linker_options + @debug, @shared = debug, shared end - ld_path << "#{libpath}/libc.so" - ld_path << "#{libpath}/crtn.o" - cmd = [@linker, "-o", @output, "-m", "elf_x86_64", *@options, *ld_path, @input].join(' ') - puts cmd if @debug - cmd - end + def link(input: @input, output: @output, debug: @debug, shared: @shared) = IO.popen(link_command).close + + def link_command(input: @input, output: @output, debug: @debug, shared: @shared) + ld_path = [] + return to_elf(input:, output:, debug:) if @linker == "self" - 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) + if @shared + ld_path << "--shared" + ld_path << "#{libpath}/crti.o" + ld_path << "#{gcc_libpath}/crtbeginS.o" + ld_path << "#{gcc_libpath}/crtendS.o" + else + ld_path << "-dynamic-linker" + ld_path << "/lib64/ld-linux-x86-64.so.2" + ld_path << "#{libpath}/crt1.o" + ld_path << "#{libpath}/crti.o" + ld_path << "#{gcc_libpath}/crtbegin.o" + # for not static compile + ld_path << "#{gcc_libpath}/crtend.o" + end + + ld_path << "#{libpath}/libc.so" + ld_path << "#{libpath}/crtn.o" + cmd = [@linker, "-o", @output, "-m", "elf_x86_64", *@options, *ld_path, @input].join(' ') + puts cmd if @debug + cmd + end + + 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(input: @input, output: @output, debug: @debug) + elf_obj = Caotral::Linker::Reader.new(input:, debug:).read + Caotral::Linker::Writer.new(elf_obj:, output:, debug:).write + end + end end diff --git a/lib/caotral/linker/elf.rb b/lib/caotral/linker/elf.rb new file mode 100644 index 0000000..0635e21 --- /dev/null +++ b/lib/caotral/linker/elf.rb @@ -0,0 +1,14 @@ +require_relative "elf/header" +require_relative "elf/sections" + +module Caotral + class Linker + class ELF + attr_reader :sections, :header + def initialize + @sections = Caotral::Linker::ELF::Sections.new + @header = Caotral::Linker::ELF::Header.new + end + end + end +end diff --git a/lib/caotral/linker/elf/header.rb b/lib/caotral/linker/elf/header.rb new file mode 100644 index 0000000..5533506 --- /dev/null +++ b/lib/caotral/linker/elf/header.rb @@ -0,0 +1,97 @@ +module Caotral + class Linker + class ELF + class Header + include Caotral::Assembler::ELF::Utils + attr_reader :entry, :phoffset, :shoffset, :shnum, :shstrndx + IDENT = [0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00].freeze + IDENT_STR = IDENT.pack("C*").freeze + ELF_FILE_TYPE = { NONE: 0, REL: 1, EXEC: 2, DYN: 3, CORE: 4 }.freeze + + def initialize(endian: :little, type: :rel, arc: :amd64) + @ident = IDENT + @type = num2bytes(ELF_FILE_TYPE[elf(type)], 2) + @arch = arch(arc) + @version = num2bytes(1, 4) + @entry = num2bytes(0x00, 8) + @phoffset = num2bytes(0x00, 8) + @shoffset = num2bytes(0x00, 8) + @flags = num2bytes(0x00, 4) + @ehsize = num2bytes(0x40, 2) + @phsize = num2bytes(0x00, 2) + @phnum = num2bytes(0x00, 2) + @shentsize = num2bytes(0x40, 2) + @shnum = num2bytes(0x08, 2) + @shstrndx = num2bytes(0x07, 2) + end + + def build = bytes.flatten.pack("C*") + + def set!(type: nil, entry: nil, phoffset: nil, shoffset: nil, shnum: nil, shstrndx: nil, phsize: nil, phnum: nil, ehsize: nil) + @type = num2bytes(type, 2) if check(type, 2) + @entry = num2bytes(entry, 8) if check(entry, 8) + @phoffset = num2bytes(phoffset, 8) if check(phoffset, 8) + @phsize = num2bytes(phsize, 2) if check(phsize, 2) + @phnum = num2bytes(phnum, 2) if check(phnum, 2) + @ehsize = num2bytes(ehsize, 2) if check(ehsize, 2) + @shoffset = num2bytes(shoffset, 8) if check(shoffset, 8) + @shnum = num2bytes(shnum, 2) if check(shnum, 2) + @shstrndx = num2bytes(shstrndx, 2) if check(shstrndx, 2) + self + end + + def ehsize = get(:ehsize) + def phsize = get(:phsize) + def phnum = get(:phnum) + def shentsize = get(:shentsize) + def shnum = get(:shnum) + def shstrndx = get(:shstrndx) + + LONG_TYPES = %w[entry phoffset shoffset].freeze + INT_TYPES = %w[type version].freeze + SHORT_TYPES = %w[ehsize phsize phnum shentsize shnum shstrndx].freeze + CHAR_TYPES = %w[arch flags].freeze + private_constant :LONG_TYPES, :INT_TYPES, :SHORT_TYPES, :CHAR_TYPES + + private + def bytes = [ + @ident, @type, @arch, @version, @entry, @phoffset, + @shoffset, @flags, @ehsize, @phsize, @phnum, @shentsize, + @shnum, @shstrndx + ] + + def arch(machine) + case machine.to_s + in "amd64" | "x86_64" | "x64" + [0x3e, 0x00] + end + end + + def elf(type) + case type.to_s + in "relocatable" | "rel" + :REL + in "exe" | "ex" | "exec" + :EXEC + in "shared" | "share" | "dynamic" | "dyn" + :DYN + else + :NONE + end + end + + def get(type) + val = instance_variable_get(:"@#{type.to_s}").pack("C*") + case type.to_s.downcase + when *LONG_TYPES; val.unpack("Q<") + when *INT_TYPES; val.unpack("L<") + when *SHORT_TYPES; val.unpack("S<") + when *CHAR_TYPES; val.unpack("C<") + else + raise "not specified: #{type}" + end.first + end + end + end + end +end diff --git a/lib/caotral/linker/elf/program_header.rb b/lib/caotral/linker/elf/program_header.rb new file mode 100644 index 0000000..a5177ec --- /dev/null +++ b/lib/caotral/linker/elf/program_header.rb @@ -0,0 +1,32 @@ +module Caotral + class Linker + class ELF + class ProgramHeader + include Caotral::Assembler::ELF::Utils + def initialize + @type = num2bytes(0, 4) + @flags = num2bytes(0, 4) + @offset = num2bytes(0, 8) + @vaddr = num2bytes(0, 8) + @paddr = num2bytes(0, 8) + @filesz = num2bytes(0, 8) + @memsz = num2bytes(0, 8) + @align = num2bytes(0, 8) + end + def build = bytes.flatten.pack("C*") + def set!(type: nil, flags: nil, offset: nil, vaddr: nil, paddr: nil, filesz: nil, memsz: nil, align: nil) + @type = num2bytes(type, 4) if check(type, 4) + @flags = num2bytes(flags, 4) if check(flags, 4) + @offset = num2bytes(offset, 8) if check(offset, 8) + @vaddr = num2bytes(vaddr, 8) if check(vaddr, 8) + @paddr = num2bytes(paddr, 8) if check(paddr, 8) + @filesz = num2bytes(filesz, 8) if check(filesz, 8) + @memsz = num2bytes(memsz, 8) if check(memsz, 8) + @align = num2bytes(align, 8) if check(align, 8) + self + end + private def bytes = [@type, @flags, @offset, @vaddr, @paddr, @filesz, @memsz, @align] + end + end + end +end diff --git a/lib/caotral/linker/elf/section.rb b/lib/caotral/linker/elf/section.rb new file mode 100644 index 0000000..35249f7 --- /dev/null +++ b/lib/caotral/linker/elf/section.rb @@ -0,0 +1,15 @@ +class Caotral::Linker + class ELF + class Section + attr_accessor :header, :body, :section_name + def initialize(type:, section_name: nil, options: {}) + type_string = type.to_s.capitalize + type_string = type_string.upcase if type_string == "Bss" + @section_name = (section_name.nil? ? type_string : section_name).to_s.downcase + @header, @body = nil, nil + end + + def name = @section_name == "null" ? "" : "\0#{@section_name}" + end + end +end diff --git a/lib/caotral/linker/elf/section/rel.rb b/lib/caotral/linker/elf/section/rel.rb new file mode 100644 index 0000000..b6ecf62 --- /dev/null +++ b/lib/caotral/linker/elf/section/rel.rb @@ -0,0 +1,36 @@ +module Caotral + class Linker + class ELF + class Section + class Rel + include Caotral::Assembler::ELF::Utils + def initialize(addend: true) + @offset = num2bytes(0, 8) + @info = num2bytes(0, 8) + @addend = addend ? num2bytes(0, 8) : false + end + + def set!(offset: nil, info: nil, addend: nil) + @offset = num2bytes(offset, 8) if check(offset, 8) + @info = num2bytes(info, 8) if check(info, 8) + @addend = num2bytes(addend, 8) if check(addend, 8) + self + end + + def build = bytes.flatten.pack("C*") + def offset = @offset.pack("C*").unpack1("Q<") + def info = @info.pack("C*").unpack1("Q<") + def addend + raise "No addend field in this REL entry" unless addend? + @addend.pack("C*").unpack1("Q<") + end + def sym = @info.pack("C*").unpack1("Q<") >> 32 + def type = @info.pack("C*").unpack1("Q<") & 0xffffffff + def addend? = !!@addend + + private def bytes = addend? ? [@offset, @info, @addend] : [@offset, @info] + end + end + end + end +end diff --git a/lib/caotral/linker/elf/section/strtab.rb b/lib/caotral/linker/elf/section/strtab.rb new file mode 100644 index 0000000..310e35a --- /dev/null +++ b/lib/caotral/linker/elf/section/strtab.rb @@ -0,0 +1,27 @@ +module Caotral + class Linker + class ELF + class Section + class Strtab + include Caotral::Assembler::ELF::Utils + attr_reader :names + def initialize(names = "\0main\0", **opts) = @names = names + def build = @names.bytes.pack("C*") + def offset_of(name) + offset = 0 + @names.split("\0").each do |n| + return offset if n == name + offset += n.bytesize + 1 + end + nil + end + + def lookup(offset) + return "" if offset == 0 + @names.byteslice(offset..).split("\0", 2).first + end + end + end + end + end +end diff --git a/lib/caotral/linker/elf/section/symtab.rb b/lib/caotral/linker/elf/section/symtab.rb new file mode 100644 index 0000000..a331418 --- /dev/null +++ b/lib/caotral/linker/elf/section/symtab.rb @@ -0,0 +1,35 @@ +module Caotral + class Linker + class ELF + class Section + class Symtab + include Caotral::Assembler::ELF::Utils + attr_accessor :name_string + def initialize(**opts) + @entsize = [] + @name = num2bytes(0, 4) + @info = num2bytes(0, 1) + @other = num2bytes(0, 1) + @shndx = num2bytes(0, 2) + @value = num2bytes(0, 8) + @size = num2bytes(0, 8) + @name_string = "" + end + + def set!(name: nil, info: nil, other: nil, shndx: nil, value: nil, size: nil) + @name = num2bytes(name, 4) if check(name, 4) + @info = num2bytes(info, 1) if check(info, 1) + @other = num2bytes(other, 1) if check(other, 1) + @shndx = num2bytes(shndx, 2) if check(shndx, 2) + @value = num2bytes(value, 8) if check(value, 8) + @size = num2bytes(size, 8) if check(size, 8) + self + end + + def name_offset = @name.pack("C*").unpack1("L<") + def value = @value.pack("C*").unpack1("Q<") + end + end + end + end +end diff --git a/lib/caotral/linker/elf/section/text.rb b/lib/caotral/linker/elf/section/text.rb new file mode 100644 index 0000000..e3998f5 --- /dev/null +++ b/lib/caotral/linker/elf/section/text.rb @@ -0,0 +1,4 @@ +class Caotral::Linker::ELF::Section::Text + def initialize = @bytes = [] + def build = @bytes.flatten.pack("C*") +end diff --git a/lib/caotral/linker/elf/section_header.rb b/lib/caotral/linker/elf/section_header.rb new file mode 100644 index 0000000..d3fcb04 --- /dev/null +++ b/lib/caotral/linker/elf/section_header.rb @@ -0,0 +1,78 @@ +module Caotral + class Linker + class ELF + class SectionHeader + SHT = { + null: 0, + progbits: 1, + symtab: 2, + strtab: 3, + rela: 4, + hash: 5, + dynamic: 6, + note: 7, + nobits: 8, + rel: 9, + shlib: 10, + dynsym: 11, + }.freeze + SHT_BY_VALUE = SHT.invert.freeze + include Caotral::Assembler::ELF::Utils + def initialize + @name = nil + @type = nil + @flags = nil + @addr = nil + @offset = nil + @size = nil + @link = nil + @info = nil + @addralign = nil + @entsize = nil + end + + def build = bytes.flatten.pack("C*") + + def set!(name: nil, type: nil, flags: nil, addr: nil, + offset: nil, size: nil, link: nil, info: nil, + addralign: nil, entsize: nil) + @name = num2bytes(name, 4) if check(name, 4) + @type = num2bytes(type, 4) if check(type, 4) + @flags = num2bytes(flags, 8) if check(flags, 8) + @addr = num2bytes(addr, 8) if check(addr, 8) + @offset = num2bytes(offset, 8) if check(offset, 8) + @size = num2bytes(size, 8) if check(size, 8) + @link = num2bytes(link, 4) if check(link, 4) + @info = num2bytes(info, 4) if check(info, 4) + @addralign = num2bytes(addralign, 8) if check(addralign, 8) + @entsize = num2bytes(entsize, 8) if check(entsize, 8) + self + end + + def null! = set!(name: 0, type: 0, flags: 0, addr: 0, offset: 0, size: 0, link: 0, info: 0, addralign: 0, entsize: 0) + def name = get(:name) + def offset = get(:offset) + def entsize = get(:entsize) + def size = get(:size) + def type = SHT_BY_VALUE[@type.pack("C*").unpack("L<").first] + def info = get(:info) + def addr = get(:addr) + LONG_TYPES = %w[flags addr offset size addralign entsize].freeze + INT_TYPES = %w[name type link info].freeze + + private_constant :LONG_TYPES, :INT_TYPES + + private def bytes = [@name, @type, @flags, @addr, @offset, @size, @link, @info, @addralign, @entsize] + private def get(type) + val = instance_variable_get("@#{type.to_s}").pack("C*") + case type.to_s + when *INT_TYPES; val.unpack("L<") + when *LONG_TYPES; val.unpack("Q<") + else + raise "not specified: #{type}" + end.first + end + end + end + end +end diff --git a/lib/caotral/linker/elf/sections.rb b/lib/caotral/linker/elf/sections.rb new file mode 100644 index 0000000..ec8444f --- /dev/null +++ b/lib/caotral/linker/elf/sections.rb @@ -0,0 +1,37 @@ +class Caotral::Linker::ELF::Sections + include Enumerable + + def initialize = @sections = [] + def each(&block) = @sections.each(&block) + def add(section) = @sections << section + alias << add + def size = @sections.size + alias length size + def empty? = @sections.empty? + def count(&block) + return @sections.count(&block) if block_given? + @sections.size + end + + def [](index) + case index + when Integer + @sections[index] + when String, Symbol + @sections.find { it.section_name == prepend_dot(index) } + else + raise ArgumentError, "Invalid index type: #{index.class}" + end + end + + def index(name) + name = prepend_dot(name) + @sections.each_with_index do |section, idx| + return idx if section.section_name == name + end + end + private def prepend_dot(name) + str = name.to_s + str.start_with?(".") ? str : ".#{str}" + end +end diff --git a/lib/caotral/linker/reader.rb b/lib/caotral/linker/reader.rb new file mode 100644 index 0000000..1d6c7c1 --- /dev/null +++ b/lib/caotral/linker/reader.rb @@ -0,0 +1,131 @@ +require "stringio" +require_relative "elf" +require_relative "elf/section" +require_relative "elf/section_header" +require_relative "elf/section/strtab" +require_relative "elf/section/symtab" +require_relative "elf/section/rel" + +module Caotral + class Linker + class Reader + attr_reader :context + def self.read!(input:, debug: false, linker_options: []) = new(input:, debug:, linker_options:).read + + def initialize(input:, debug: false, linker_options: []) + @input = decision(input) + @bin = StringIO.new(@input.read) + @context = Caotral::Linker::ELF.new + end + + def read + header = @bin.read(0x40) + ident = header[0, 16] + raise "Not ELF file" unless ident == Caotral::Linker::ELF::Header::IDENT_STR + + entry = header[24, 8].unpack("Q<").first + phoffset = header[32, 8].unpack("Q<").first + shoffset = header[40, 8].unpack("Q<").first + shentsize = header[58, 2].unpack("S<").first + shnum = header[60, 2].unpack("S<").first + shstrndx = header[62, 2].unpack("S<").first + @context.header.set!(entry:, phoffset:, shoffset:, shnum:, shstrndx:) + + @bin.pos = shoffset + shnum.times do |i| + sh_entry = @bin.read(shentsize) + name = sh_entry[0, 4].unpack("L<").first + type_val = sh_entry[4, 4].unpack("L<").first + flags = sh_entry[8, 8].unpack("Q<").first + addr = sh_entry[16, 8].unpack("Q<").first + offset = sh_entry[24, 8].unpack("Q<").first + size = sh_entry[32, 8].unpack("Q<").first + link = sh_entry[40, 4].unpack("L<").first + info = sh_entry[44, 4].unpack("L<").first + addralign = sh_entry[48, 8].unpack("Q<").first + entsize = sh_entry[56, 8].unpack("Q<").first + type_sym = type(type_val) + section_header = Caotral::Linker::ELF::SectionHeader.new + section_header.set!(name:, type: type_val, flags:, addr:, offset:, size:, link:, info:, addralign:, entsize:) + section_name = i == shstrndx ? ".shstrtab" : nil + args = { type: type_sym, section_name: }.compact + section = Caotral::Linker::ELF::Section.new(**args) + section.header = section_header + @context.sections.add(section) + end + shstrtab = @context.sections[shstrndx].tap do |shstrtab| + @bin.pos = shstrtab.header.offset + names = @bin.read(shstrtab.header.size) + shstrtab.body = Caotral::Linker::ELF::Section::Strtab.new(names) + shstrtab + end + + names = shstrtab.body.names + + @context.sections.each_with_index do |section, i| + next if i == shstrndx || section.header.name == 0 + offset = section.header.name + section.section_name = names.byteslice(offset..).split("\0", 2).first + type = section.header.type + @bin.pos = section.header.offset + body_bin = @bin.read(section.header.size) + section.body = case type + when :strtab + Caotral::Linker::ELF::Section::Strtab.new(body_bin) + when :symtab + symtab_entsize = section.header.entsize + count = body_bin.bytesize / symtab_entsize + count.times.map do |i| + sym_bin = body_bin[i * symtab_entsize, symtab_entsize] + name = sym_bin[0, 4].unpack1("L<") + info = sym_bin[4, 1].unpack1("C") + other = sym_bin[5, 1].unpack1("C") + shndx = sym_bin[6, 2].unpack1("S<") + value = sym_bin[8, 8].unpack1("Q<") + size = sym_bin[16, 8].unpack1("Q<") + Caotral::Linker::ELF::Section::Symtab.new.set!(name:, info:, other:, shndx:, value:, size:) + end + when :rel, :rela + rela = type == :rela + rel_entsize = section.header.entsize + rel_entsize = rela ? 24 : 16 if rel_entsize == 0 + count = body_bin.bytesize / rel_entsize + count.times.map do |i| + rel_bin = body_bin.byteslice(i * rel_entsize, rel_entsize) + offset = rel_bin[0, 8].unpack1("Q<") + info = rel_bin[8, 8].unpack1("Q<") + addend = rela ? rel_bin[16, 8].unpack1("q<") : nil + Caotral::Linker::ELF::Section::Rel.new(addend: rela).set!(offset:, info:, addend:) + end + when :progbits + body_bin + end + end + + strtab = @context.sections[".strtab"] + @context.sections.select { it.header.type == :symtab }.each do |symtab| + symtab.body.each do |sym| + name_offset = sym.name_offset + sym.name_string = strtab.body.lookup(name_offset) + end + end + + @context + ensure + @input.close + end + + private + def decision(input) + case input + when String, Pathname + File.open(File.expand_path(input.to_s), "rb") + else + raise ArgumentError, "wrong input type" + end + end + + def type(num) = Caotral::Linker::ELF::SectionHeader::SHT_BY_VALUE.fetch(num, :unknown) + end + end +end diff --git a/lib/caotral/linker/writer.rb b/lib/caotral/linker/writer.rb new file mode 100644 index 0000000..8ecddcc --- /dev/null +++ b/lib/caotral/linker/writer.rb @@ -0,0 +1,96 @@ +require_relative "elf/program_header" + +module Caotral + class Linker + class Writer + include Caotral::Assembler::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 + RELOCATION_SECTION_NAMES = [".rela.text", ".rel.text"].freeze + attr_reader :elf_obj, :output, :entry, :debug + def self.write!(elf_obj:, output:, entry: nil, debug: false) + new(elf_obj:, output:, entry:, debug:).write + end + def initialize(elf_obj:, output:, entry: nil, debug: false) + @elf_obj, @output, @entry, @debug = elf_obj, output, entry, debug + end + def write + f = File.open(@output, "wb") + phoffset, phnum, phsize, ehsize = 64, 1, 56, 64 + header = @elf_obj.header.set!(type: 2, phoffset:, phnum:, phsize:, ehsize:) + ph = Caotral::Linker::ELF::ProgramHeader.new + text_section = @elf_obj.sections[".text"] + rel_sections = @elf_obj.sections.select { RELOCATION_SECTION_NAMES.include?(it.section_name) } + start_bytes = [0xe8, *[0] * 4, 0x48, 0x89, 0xc7, 0x48, 0xc7, 0xc0, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x05] + symtab = @elf_obj.sections[".symtab"] + symtab_body = symtab.body + old_text = text_section.body + main_sym = symtab_body.find { |sym| sym.name_string == "main" } + text_offset = 0x1000 + base_addr = 0x400000 + align = 0x1000 + vaddr = base_addr + text_offset + paddr = base_addr + text_offset + start_len = start_bytes.length + start_addr = base_addr + text_offset + main_offset = main_sym.value + start_len + start_bytes[1, 4] = num2bytes((main_offset - 5), 4) + start_bytestring = start_bytes.pack("C*") + text_section.body = start_bytestring + old_text + type, flags = 1, 5 + filesz = text_section.body.bytesize + memsz = filesz + + rel_sections.each do |rel| + target = @elf_obj.sections[rel.header.info] + bytes = target.body.dup + rel.body.each do |entry| + next unless ALLOW_RELOCATION_TYPES.include?(entry.type) + a = entry.type == R_X86_64_PC32 ? 4 : 0 + sym = symtab_body[entry.sym] + target_addr = target == text_section ? vaddr : target.header.addr + sym_addr = sym.value + target_addr + (target == text_section ? start_len : 0) + sym_offset = entry.offset + (target == text_section ? start_len : 0) + sym_addend = entry.addend? ? entry.addend : bytes[sym_offset, 4].unpack1("l<") + value = sym_addr + sym_addend - (target_addr + sym_offset + a) + bytes[sym_offset, 4] = [value].pack("l<") + 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) + f.write(@elf_obj.header.build) + f.write(ph.build) + gap = [text_offset - f.pos, 0].max + f.write("\0" * gap) + f.write(text_section.body) + shstrtab = @elf_obj.sections[".shstrtab"] + 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.name == "" } + shoffset = f.pos + shstrndx = write_sections.index { it.section_name == ".shstrtab" } + shnum = write_sections.size + @elf_obj.header.set!(shoffset:, shnum:, shstrndx:) + names = @elf_obj.sections[".shstrtab"].body + + write_sections.each do |section| + lookup_name = section.name.sub(/\A\0/, "") + name_offset = names.offset_of(lookup_name) + section.header.set!(name: name_offset) if name_offset + f.write(section.header.build) + end + + f.seek(0) + f.write(@elf_obj.header.build) + output + ensure + f.close if f + end + end + end +end diff --git a/sample/C/rel_text.c b/sample/C/rel_text.c new file mode 100644 index 0000000..b34aba5 --- /dev/null +++ b/sample/C/rel_text.c @@ -0,0 +1,12 @@ +extern int foo(void); +int foo(void) { return 0; } + +int main(void) { + asm volatile( + "jmp 1f\n" + ".long 0\n" + ".reloc .-4, R_X86_64_PC32, foo\n" + "1:\n" + ); + return foo(); +} diff --git a/sample/assembler/plus.s b/sample/assembler/plus.s index 5a725d2..d846f2a 100644 --- a/sample/assembler/plus.s +++ b/sample/assembler/plus.s @@ -28,4 +28,6 @@ main: push rax mov rsp, rbp pop rbp - ret + mov rdi, rax + mov rax, 0x3c + syscall diff --git a/sig/caotral/linker.rbs b/sig/caotral/linker.rbs new file mode 100644 index 0000000..493aabe --- /dev/null +++ b/sig/caotral/linker.rbs @@ -0,0 +1,17 @@ +class Caotral::Linker + @input: String + @output: String + @linker: String + @options: Array[String] + @shared: bool + @debug: bool + + def self.link!: (input: String, ?output: String, ?linker: String, ?debug: bool, ?shared: bool) -> void + def initialize: (input: String, ?output: String, ?linker: String, ?linker_options: Array[String], ?shared: bool, ?debug: bool) -> void + def link: (input: String, ?output: String, ?shared: bool, ?debug: bool) -> void + + def link_command: (input: String, ?output: String, ?shared: bool, ?debug: bool) -> String + def libpath: () -> String + def gcc_libpath: () -> String + def to_elf: (input: String, ?output: String, ?debug: bool) -> void +end diff --git a/sig/caotral/linker/elf.rbs b/sig/caotral/linker/elf.rbs new file mode 100644 index 0000000..a295925 --- /dev/null +++ b/sig/caotral/linker/elf.rbs @@ -0,0 +1,3 @@ +class Caotral::Linker::ELF + def initialize: () -> void +end diff --git a/sig/caotral/linker/elf/header.rbs b/sig/caotral/linker/elf/header.rbs new file mode 100644 index 0000000..1593ffc --- /dev/null +++ b/sig/caotral/linker/elf/header.rbs @@ -0,0 +1,13 @@ +class Caotral::Linker::ELF::Header + def set!: ( + ?type: Integer?, + ?entry: Integer?, + ?phoffset: Integer?, + ?shoffset: Integer?, + ?shnum: Integer?, + ?shstrndx: Integer?, + ?phsize: Integer?, + ?phnum: Integer?, + ?ehsize: Integer? + ) -> self +end diff --git a/sig/caotral/linker/elf/program_header.rbs b/sig/caotral/linker/elf/program_header.rbs new file mode 100644 index 0000000..e8391bf --- /dev/null +++ b/sig/caotral/linker/elf/program_header.rbs @@ -0,0 +1,14 @@ +class Caotral::Linker::ELF::ProgramHeader + def initialize: () -> void + def build: () -> String + def set!: ( + ?type: Integer?, + ?flags: Integer?, + ?offset: Integer?, + ?vaddr: Integer?, + ?paddr: Integer?, + ?filesz: Integer?, + ?memsz: Integer?, + ?align: Integer? + ) -> self +end diff --git a/sig/caotral/linker/elf/section.rbs b/sig/caotral/linker/elf/section.rbs new file mode 100644 index 0000000..2d7b111 --- /dev/null +++ b/sig/caotral/linker/elf/section.rbs @@ -0,0 +1,8 @@ +class Caotral::Linker::ELF::Section + attr_accessor header: untyped + attr_accessor body: untyped + attr_accessor section_name: String + + def initialize: (type: Symbol, ?section_name: String?, ?options: Hash[Symbol, untyped]) -> void + def name: () -> String +end diff --git a/sig/caotral/linker/elf/section/rel.rbs b/sig/caotral/linker/elf/section/rel.rbs new file mode 100644 index 0000000..6176c32 --- /dev/null +++ b/sig/caotral/linker/elf/section/rel.rbs @@ -0,0 +1,12 @@ +class Caotral::Linker::ELF::Section::Rel + def initialize: (?addend: bool) -> void + def set!: ( + ?offset: Integer?, + ?info: Integer?, + ?addend: Integer? + ) -> self + def build: () -> String + def offset: () -> Integer + def info: () -> Integer + def addend: () -> Integer +end diff --git a/sig/caotral/linker/elf/section/strtab.rbs b/sig/caotral/linker/elf/section/strtab.rbs new file mode 100644 index 0000000..e5d3c8d --- /dev/null +++ b/sig/caotral/linker/elf/section/strtab.rbs @@ -0,0 +1,8 @@ +class Caotral::Linker::ELF::Section::Strtab + attr_reader names: String + + def initialize: (?String, **untyped) -> void + def build: () -> String + def offset_of: (String) -> Integer? + def lookup: (Integer) -> String +end diff --git a/sig/caotral/linker/elf/section/symtab.rbs b/sig/caotral/linker/elf/section/symtab.rbs new file mode 100644 index 0000000..3a8c272 --- /dev/null +++ b/sig/caotral/linker/elf/section/symtab.rbs @@ -0,0 +1,14 @@ +class Caotral::Linker::ELF::Section::Symtab + attr_accessor name_string: String + + def initialize: (?opts: untyped) -> void + def set!: ( + ?name: Integer?, + ?info: Integer?, + ?other: Integer?, + ?shndx: Integer?, + ?value: Integer?, + ?size: Integer? + ) -> self + def name_offset: () -> Integer +end diff --git a/sig/caotral/linker/elf/section_header.rbs b/sig/caotral/linker/elf/section_header.rbs new file mode 100644 index 0000000..a6c9917 --- /dev/null +++ b/sig/caotral/linker/elf/section_header.rbs @@ -0,0 +1,27 @@ +class Caotral::Linker::ELF::SectionHeader + SHT: Hash[Symbol, Integer] + SHT_BY_VALUE: Hash[Integer, Symbol] + LONG_TYPES: Array[String] + INT_TYPES: Array[String] + + def initialize: () -> void + def build: () -> String + def set!: ( + ?name: Integer?, + ?type: Integer?, + ?flags: Integer?, + ?addr: Integer?, + ?offset: Integer?, + ?size: Integer?, + ?link: Integer?, + ?info: Integer?, + ?addralign: Integer?, + ?entsize: Integer? + ) -> self + def null!: () -> self + def name: () -> Integer + def offset: () -> Integer + def entsize: () -> Integer + def size: () -> Integer + def info: () -> Integer +end diff --git a/sig/caotral/linker/reader.rbs b/sig/caotral/linker/reader.rbs new file mode 100644 index 0000000..689f5e4 --- /dev/null +++ b/sig/caotral/linker/reader.rbs @@ -0,0 +1,5 @@ +class Caotral::Linker::Reader + def self.read!: (input: String, ?debug: bool, ?linker_options: Array[String]) -> Caotral::Linker::ELF + def initialize: (input: String, ?debug: bool, ?linker_options: Array[String]) -> void + def read: () -> Caotral::Linker::ELF +end diff --git a/sig/caotral/linker/writer.rbs b/sig/caotral/linker/writer.rbs new file mode 100644 index 0000000..d4d5f9d --- /dev/null +++ b/sig/caotral/linker/writer.rbs @@ -0,0 +1,5 @@ +class Caotral::Linker::Writer + def self.write!: (elf_obj: Caotral::Linker::ELF, output: String, ?entry: Integer, ?debug: bool) -> String + def initialize: (elf_obj: Caotral::Linker::ELF, output: String, ?entry: Integer, ?debug: bool) -> void + def write: () -> String +end diff --git a/test/caotral/assembler_test.rb b/test/caotral/assembler_test.rb index 780c0e2..5a27538 100644 --- a/test/caotral/assembler_test.rb +++ b/test/caotral/assembler_test.rb @@ -46,9 +46,9 @@ def test_assembler_command def dumped_references [ - [127, 69, 76, 70, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 62, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 64, 0, 8, 0, 7, 0], # elf header + [127, 69, 76, 70, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 62, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 64, 0, 8, 0, 7, 0], # elf header [], # null section - [85, 72, 137, 229, 72, 131, 236, 0, 106, 1, 106, 2, 95, 88, 72, 1, 248, 80, 106, 3, 95, 88, 72, 15, 175, 199, 80, 106,5, 106, 4, 95, 88, 72, 41, 248, 80, 95, 88, 72, 153, 72, 247, 255, 80, 72, 137, 236, 93, 195], # text section + [85, 72, 137, 229, 72, 131, 236, 0, 106, 1, 106, 2, 95, 88, 72, 1, 248, 80, 106, 3, 95, 88, 72, 15, 175, 199, 80, 106,5, 106, 4, 95, 88, 72, 41, 248, 80, 95, 88, 72, 153, 72, 247, 255, 80, 72, 137, 236, 93, 72, 137, 199, 72, 199, 192, 60, 0, 0], # text section [], # data section [], # bss section [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 85, 76, 76, 0, 0, 0, 0],# @@ -58,13 +58,13 @@ def dumped_references [0, 46, 115, 121, 109, 116, 97, 98, 0, 46, 115, 116, 114, 116, 97, 98, 0, 46, 115, 104, 115, 116, 114, 116, 97, 98, 0,46, 116, 101, 120, 116, 0, 46, 100, 97, 116, 97, 0, 46, 98, 115, 115, 0, 46, 110, 111, 116, 101, 46, 103, 110, 117, 46, 112, 114, 111, 112, 101, 114, 116, 121, 0], # shstrtab section # section headers [0]*64, # null - [1, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # text - [7, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # data - [13, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # bss - [18, 0, 0, 0, 7, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # note - [24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0], # symtab - [32, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 188, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # strtab - [40, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 194, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # shstrtab + [1, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # text + [7, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # data + [13, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # bss + [18, 0, 0, 0, 7, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # note + [24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0], # symtab + [32, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 196, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # strtab + [40, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 202, 0, 0, 0, 0, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # shstrtab [], ] end diff --git a/test/caotral/linker/reader_test.rb b/test/caotral/linker/reader_test.rb new file mode 100644 index 0000000..9e02530 --- /dev/null +++ b/test/caotral/linker/reader_test.rb @@ -0,0 +1,17 @@ +require "caotral" +require "test/unit" +class Caotral::Linker::ReaderTest < Test::Unit::TestCase + def setup = Caotral.assemble(input: "sample/assembler/plus.s", output: "plus.o", assembler: "self") + def teardown = File.delete("plus.o") if File.exist?("plus.o") + def test_read + elf_obj = Caotral::Linker::Reader.read!(input: "plus.o", debug: false) + assert_equal elf_obj.header.shoffset.pack("C*").unpack("Q<").first, 264 + assert_equal elf_obj.sections.size, 8 + assert_equal elf_obj.sections[0].section_name, "null" + shstrtab = elf_obj.sections[elf_obj.header.shstrndx] + assert_equal shstrtab.section_name, ".shstrtab" + assert_equal shstrtab.body.names, "\0.text\0.data\0.bss\0.note\0.symtab\0.strtab\0.shstrtab\0" + assert_equal elf_obj.sections[1].section_name, ".text" + assert_equal elf_obj.sections[1].header.size, 61 + end +end diff --git a/test/caotral/linker/writer_test.rb b/test/caotral/linker/writer_test.rb new file mode 100644 index 0000000..2c544bf --- /dev/null +++ b/test/caotral/linker/writer_test.rb @@ -0,0 +1,48 @@ +require "caotral" +require "test/unit" +class Caotral::Linker::WriterTest < Test::Unit::TestCase + def setup + Caotral.assemble(input: "sample/assembler/plus.s", assembler: "self", output: "plus.o") + @elf_obj = Caotral::Linker::Reader.read!(input: "plus.o", debug: false) + end + def teardown + File.delete("plus.o") if File.exist?("plus.o") + File.delete("write.o") if File.exist?("write.o") + File.delete("write") if File.exist?("write") + File.delete("relocatable.o") if File.exist?("relocatable.o") + File.delete("relocated_exec") if File.exist?("relocated_exec") + end + def test_write + written_output = Caotral::Linker::Writer.write!(elf_obj: @elf_obj, output: "write.o", debug: false) + read_written_elf = Caotral::Linker::Reader.read!(input: written_output, debug: false) + assert_equal @elf_obj.header.shoffset.pack("C*").unpack("Q<").first, read_written_elf.header.shoffset.pack("C*").unpack("Q<").first + assert_equal 4, read_written_elf.sections.size + assert_equal 0x401000, read_written_elf.header.entry.pack("C*").unpack("Q<").first + end + + def test_execute_written + written_output = Caotral::Linker::Writer.write!(elf_obj: @elf_obj, output: "write", debug: false) + File.chmod(0755, "./write") + IO.popen("./write").close + exit_code, handle_code = check_process($?.to_i) + assert_equal(9, exit_code) + assert_equal(0, handle_code) + end + + def test_relocation_write_and_execute + IO.popen("gcc -c -fno-pic -fno-pie -o relocatable.o sample/C/rel_text.c").close + elf_obj = Caotral::Linker::Reader.read!(input: "relocatable.o", debug: false) + Caotral::Linker::Writer.write!(elf_obj:, output: "relocated_exec", debug: false) + File.chmod(0755, "./relocated_exec") + IO.popen("./relocated_exec").close + exit_code, _handle_code = check_process($?.to_i) + assert_equal(0, exit_code) + end + + private + def check_process(status) + exit_code = status >> 8 + handle_code = status & 0x7f + [exit_code, handle_code] + end +end