vendor: Update vendored ruby-macho to 0.2.5.

This commit is contained in:
William Woodruff 2016-08-07 13:36:19 -04:00 committed by Martin Afanasjew
parent d7da1640ff
commit 5caa812e2c
13 changed files with 1296 additions and 505 deletions

View File

@ -3,7 +3,7 @@ Vendored Dependencies
* [okjson](https://github.com/kr/okjson), version 43.
* [ruby-macho](https://github.com/Homebrew/ruby-macho), version 0.2.4
* [ruby-macho](https://github.com/Homebrew/ruby-macho), version 0.2.5
## Licenses:

View File

@ -1,4 +1,5 @@
require "#{File.dirname(__FILE__)}/macho/structure"
require "#{File.dirname(__FILE__)}/macho/view"
require "#{File.dirname(__FILE__)}/macho/headers"
require "#{File.dirname(__FILE__)}/macho/load_commands"
require "#{File.dirname(__FILE__)}/macho/sections"
@ -12,5 +13,5 @@ require "#{File.dirname(__FILE__)}/macho/tools"
# The primary namespace for ruby-macho.
module MachO
# release version
VERSION = "0.2.4".freeze
VERSION = "0.2.5".freeze
end

View File

@ -3,6 +3,26 @@ module MachO
class MachOError < RuntimeError
end
# Raised when a Mach-O file modification fails.
class ModificationError < MachOError
end
# Raised when a Mach-O file modification fails but can be recovered when
# operating on multiple Mach-O slices of a fat binary in non-strict mode.
class RecoverableModificationError < ModificationError
# @return [Fixnum, nil] The index of the Mach-O slice of a fat binary for
# which modification failed or `nil` if not a fat binary. This is used to
# make the error message more useful.
attr_accessor :macho_slice
# @return [String] The exception message.
def to_s
s = super.to_s
s = "While modifying Mach-O slice #{@macho_slice}: #{s}" if @macho_slice
s
end
end
# Raised when a file is not a Mach-O.
class NotAMachOError < MachOError
# @param error [String] the error in question
@ -80,32 +100,89 @@ module MachO
end
end
# Raised when a load command can't be created manually.
class LoadCommandNotCreatableError < MachOError
# @param cmd_sym [Symbol] the uncreatable load command's symbol
def initialize(cmd_sym)
super "Load commands of type #{cmd_sym} cannot be created manually"
end
end
# Raised when the number of arguments used to create a load command manually is wrong.
class LoadCommandCreationArityError < MachOError
# @param cmd_sym [Symbol] the load command's symbol
# @param expected_arity [Fixnum] the number of arguments expected
# @param actual_arity [Fixnum] the number of arguments received
def initialize(cmd_sym, expected_arity, actual_arity)
super "Expected #{expected_arity} arguments for #{cmd_sym} creation, got #{actual_arity}"
end
end
# Raised when a load command can't be serialized.
class LoadCommandNotSerializableError < MachOError
# @param cmd_sym [Symbol] the load command's symbol
def initialize(cmd_sym)
super "Load commands of type #{cmd_sym} cannot be serialized"
end
end
# Raised when a load command string is malformed in some way.
class LCStrMalformedError < MachOError
# @param lc [MachO::LoadCommand] the load command containing the string
def initialize(lc)
super "Load command #{lc.type} at offset #{lc.view.offset} contains a malformed string"
end
end
# Raised when a change at an offset is not valid.
class OffsetInsertionError < ModificationError
# @param offset [Fixnum] the invalid offset
def initialize(offset)
super "Insertion at offset #{offset} is not valid"
end
end
# Raised when load commands are too large to fit in the current file.
class HeaderPadError < MachOError
class HeaderPadError < ModificationError
# @param filename [String] the filename
def initialize(filename)
super "Updated load commands do not fit in the header of " +
"#{filename}. #{filename} needs to be relinked, possibly with " +
"-headerpad or -headerpad_max_install_names"
super "Updated load commands do not fit in the header of " \
"#{filename}. #{filename} needs to be relinked, possibly with " \
"-headerpad or -headerpad_max_install_names"
end
end
# Raised when attempting to change a dylib name that doesn't exist.
class DylibUnknownError < MachOError
class DylibUnknownError < RecoverableModificationError
# @param dylib [String] the unknown shared library name
def initialize(dylib)
super "No such dylib name: #{dylib}"
end
end
# Raised when a dylib is missing an ID
class DylibIdMissingError < RecoverableModificationError
def initialize
super "Dylib is missing a dylib ID"
end
end
# Raised when attempting to change an rpath that doesn't exist.
class RpathUnknownError < MachOError
class RpathUnknownError < RecoverableModificationError
# @param path [String] the unknown runtime path
def initialize(path)
super "No such runtime path: #{path}"
end
end
# Raised when attempting to add an rpath that already exists.
class RpathExistsError < RecoverableModificationError
# @param path [String] the extant path
def initialize(path)
super "#{path} already exists"
end
end
# Raised whenever unfinished code is called.
class UnimplementedError < MachOError
# @param thing [String] the thing that is unimplemented

View File

@ -30,22 +30,24 @@ module MachO
# @param filename [String] the fat file to load from
# @raise [ArgumentError] if the given file does not exist
def initialize(filename)
raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename)
raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
@filename = filename
@raw_data = File.open(@filename, "rb") { |f| f.read }
@header = get_fat_header
@fat_archs = get_fat_archs
@machos = get_machos
@raw_data = File.open(@filename, "rb", &:read)
@header = populate_fat_header
@fat_archs = populate_fat_archs
@machos = populate_machos
end
# Initializes a new FatFile instance from a binary string.
# @see MachO::FatFile.new_from_bin
# @api private
def initialize_from_bin(bin)
@filename = nil
@raw_data = bin
@header = get_fat_header
@fat_archs = get_fat_archs
@machos = get_machos
@header = populate_fat_header
@fat_archs = populate_fat_archs
@machos = populate_machos
end
# The file's raw fat data.
@ -115,7 +117,7 @@ module MachO
end
# The file's type. Assumed to be the same for every Mach-O within.
# @return [String] the filetype
# @return [Symbol] the filetype
def filetype
machos.first.filetype
end
@ -124,34 +126,37 @@ module MachO
# @example
# file.dylib_id # => 'libBar.dylib'
# @return [String, nil] the file's dylib ID
# @see MachO::MachOFile#linked_dylibs
def dylib_id
machos.first.dylib_id
end
# Changes the file's dylib ID to `new_id`. If the file is not a dylib, does nothing.
# @example
# file.dylib_id = 'libFoo.dylib'
# file.change_dylib_id('libFoo.dylib')
# @param new_id [String] the new dylib ID
# @param options [Hash]
# @option options [Boolean] :strict (true) if true, fail if one slice fails.
# if false, fail only if all slices fail.
# @return [void]
# @raise [ArgumentError] if `new_id` is not a String
def dylib_id=(new_id)
if !new_id.is_a?(String)
raise ArgumentError.new("argument must be a String")
end
# @see MachO::MachOFile#linked_dylibs
def change_dylib_id(new_id, options = {})
raise ArgumentError, "argument must be a String" unless new_id.is_a?(String)
return unless machos.all?(&:dylib?)
if !machos.all?(&:dylib?)
return nil
end
machos.each do |macho|
macho.dylib_id = new_id
each_macho(options) do |macho|
macho.change_dylib_id(new_id, options)
end
synchronize_raw_data
end
alias dylib_id= change_dylib_id
# All shared libraries linked to the file's Mach-Os.
# @return [Array<String>] an array of all shared libraries
# @see MachO::MachOFile#linked_dylibs
def linked_dylibs
# Individual architectures in a fat binary can link to different subsets
# of libraries, but at this point we want to have the full picture, i.e.
@ -165,16 +170,74 @@ module MachO
# file.change_install_name('/usr/lib/libFoo.dylib', '/usr/lib/libBar.dylib')
# @param old_name [String] the shared library name being changed
# @param new_name [String] the new name
# @todo incomplete
def change_install_name(old_name, new_name)
machos.each do |macho|
macho.change_install_name(old_name, new_name)
# @param options [Hash]
# @option options [Boolean] :strict (true) if true, fail if one slice fails.
# if false, fail only if all slices fail.
# @return [void]
# @see MachO::MachOFile#change_install_name
def change_install_name(old_name, new_name, options = {})
each_macho(options) do |macho|
macho.change_install_name(old_name, new_name, options)
end
synchronize_raw_data
end
alias :change_dylib :change_install_name
alias change_dylib change_install_name
# All runtime paths associated with the file's Mach-Os.
# @return [Array<String>] an array of all runtime paths
# @see MachO::MachOFile#rpaths
def rpaths
# Can individual architectures have different runtime paths?
machos.map(&:rpaths).flatten.uniq
end
# Change the runtime path `old_path` to `new_path` in the file's Mach-Os.
# @param old_path [String] the old runtime path
# @param new_path [String] the new runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) if true, fail if one slice fails.
# if false, fail only if all slices fail.
# @return [void]
# @see MachO::MachOFile#change_rpath
def change_rpath(old_path, new_path, options = {})
each_macho(options) do |macho|
macho.change_rpath(old_path, new_path, options)
end
synchronize_raw_data
end
# Add the given runtime path to the file's Mach-Os.
# @param path [String] the new runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) if true, fail if one slice fails.
# if false, fail only if all slices fail.
# @return [void]
# @see MachO::MachOFile#add_rpath
def add_rpath(path, options = {})
each_macho(options) do |macho|
macho.add_rpath(path, options)
end
synchronize_raw_data
end
# Delete the given runtime path from the file's Mach-Os.
# @param path [String] the runtime path to delete
# @param options [Hash]
# @option options [Boolean] :strict (true) if true, fail if one slice fails.
# if false, fail only if all slices fail.
# @return void
# @see MachO::MachOFile#delete_rpath
def delete_rpath(path, options = {})
each_macho(options) do |macho|
macho.delete_rpath(path, options)
end
synchronize_raw_data
end
# Extract a Mach-O with the given CPU type from the file.
# @example
@ -197,7 +260,7 @@ module MachO
# @note Overwrites all data in the file!
def write!
if filename.nil?
raise MachOError.new("cannot write to a default file when initialized from a binary string")
raise MachOError, "cannot write to a default file when initialized from a binary string"
else
File.open(@filename, "wb") { |f| f.write(@raw_data) }
end
@ -211,15 +274,15 @@ module MachO
# @raise [MachO::MagicError] if the magic is not valid Mach-O magic
# @raise [MachO::MachOBinaryError] if the magic is for a non-fat Mach-O file
# @raise [MachO::JavaClassFileError] if the file is a Java classfile
# @private
def get_fat_header
# @api private
def populate_fat_header
# the smallest fat Mach-O header is 8 bytes
raise TruncatedFileError.new if @raw_data.size < 8
raise TruncatedFileError if @raw_data.size < 8
fh = FatHeader.new_from_bin(:big, @raw_data[0, FatHeader.bytesize])
raise MagicError.new(fh.magic) unless MachO.magic?(fh.magic)
raise MachOBinaryError.new unless MachO.fat_magic?(fh.magic)
raise MagicError, fh.magic unless Utils.magic?(fh.magic)
raise MachOBinaryError unless Utils.fat_magic?(fh.magic)
# Rationale: Java classfiles have the same magic as big-endian fat
# Mach-Os. Classfiles encode their version at the same offset as
@ -228,15 +291,15 @@ module MachO
# technically possible for a fat Mach-O to have over 30 architectures,
# but this is extremely unlikely and in practice distinguishes the two
# formats.
raise JavaClassFileError.new if fh.nfat_arch > 30
raise JavaClassFileError if fh.nfat_arch > 30
fh
end
# Obtain an array of fat architectures from raw file data.
# @return [Array<MachO::FatArch>] an array of fat architectures
# @private
def get_fat_archs
# @api private
def populate_fat_archs
archs = []
fa_off = FatHeader.bytesize
@ -250,8 +313,8 @@ module MachO
# Obtain an array of Mach-O blobs from raw file data.
# @return [Array<MachO::MachOFile>] an array of Mach-Os
# @private
def get_machos
# @api private
def populate_machos
machos = []
fat_archs.each do |arch|
@ -261,9 +324,37 @@ module MachO
machos
end
# @todo this needs to be redesigned. arch[:offset] and arch[:size] are
# already out-of-date, and the header needs to be synchronized as well.
# @private
# Yield each Mach-O object in the file, rescuing and accumulating errors.
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if at least one Mach-O raises an exception. If false,
# only raises an exception if *all* Mach-Os raise exceptions.
# @raise [MachO::RecoverableModificationError] under the conditions of
# the `:strict` option above.
# @api private
def each_macho(options = {})
strict = options.fetch(:strict, true)
errors = []
machos.each_with_index do |macho, index|
begin
yield macho
rescue RecoverableModificationError => error
error.macho_slice = index
# Strict mode: Immediately re-raise. Otherwise: Retain, check later.
raise error if strict
errors << error
end
end
# Non-strict mode: Raise first error if *all* Mach-O slices failed.
raise errors.first if errors.size == machos.size
end
# Synchronize the raw file data with each internal Mach-O object.
# @return [void]
# @api private
def synchronize_raw_data
machos.each_with_index do |macho, i|
arch = fat_archs[i]

View File

@ -1,64 +1,82 @@
module MachO
# big-endian fat magic
# @api private
FAT_MAGIC = 0xcafebabe
# little-endian fat magic
# this is defined, but should never appear in ruby-macho code because
# fat headers are always big-endian and therefore always unpacked as such.
# @api private
FAT_CIGAM = 0xbebafeca
# 32-bit big-endian magic
# @api private
MH_MAGIC = 0xfeedface
# 32-bit little-endian magic
# @api private
MH_CIGAM = 0xcefaedfe
# 64-bit big-endian magic
# @api private
MH_MAGIC_64 = 0xfeedfacf
# 64-bit little-endian magic
# @api private
MH_CIGAM_64 = 0xcffaedfe
# association of magic numbers to string representations
# @api private
MH_MAGICS = {
FAT_MAGIC => "FAT_MAGIC",
MH_MAGIC => "MH_MAGIC",
MH_CIGAM => "MH_CIGAM",
MH_MAGIC_64 => "MH_MAGIC_64",
MH_CIGAM_64 => "MH_CIGAM_64"
}
MH_CIGAM_64 => "MH_CIGAM_64",
}.freeze
# mask for CPUs with 64-bit architectures (when running a 64-bit ABI?)
# @api private
CPU_ARCH_ABI64 = 0x01000000
# any CPU (unused?)
# @api private
CPU_TYPE_ANY = -1
# m68k compatible CPUs
# @api private
CPU_TYPE_MC680X0 = 0x06
# i386 and later compatible CPUs
# @api private
CPU_TYPE_I386 = 0x07
# x86_64 (AMD64) compatible CPUs
# @api private
CPU_TYPE_X86_64 = (CPU_TYPE_I386 | CPU_ARCH_ABI64)
# 32-bit ARM compatible CPUs
# @api private
CPU_TYPE_ARM = 0x0c
# m88k compatible CPUs
# @api private
CPU_TYPE_MC88000 = 0xd
# 64-bit ARM compatible CPUs
# @api private
CPU_TYPE_ARM64 = (CPU_TYPE_ARM | CPU_ARCH_ABI64)
# PowerPC compatible CPUs
# @api private
CPU_TYPE_POWERPC = 0x12
# PowerPC64 compatible CPUs
# @api private
CPU_TYPE_POWERPC64 = (CPU_TYPE_POWERPC | CPU_ARCH_ABI64)
# association of cpu types to symbol representations
# @api private
CPU_TYPES = {
CPU_TYPE_ANY => :any,
CPU_TYPE_I386 => :i386,
@ -67,156 +85,213 @@ module MachO
CPU_TYPE_ARM64 => :arm64,
CPU_TYPE_POWERPC => :ppc,
CPU_TYPE_POWERPC64 => :ppc64,
}
}.freeze
# mask for CPU subtype capabilities
# @api private
CPU_SUBTYPE_MASK = 0xff000000
# 64-bit libraries (undocumented!)
# @see http://llvm.org/docs/doxygen/html/Support_2MachO_8h_source.html
# @api private
CPU_SUBTYPE_LIB64 = 0x80000000
# the lowest common sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_I386 = 3
# the i486 sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_486 = 4
# the i486SX sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_486SX = 132
# the i586 (P5, Pentium) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_586 = 5
CPU_SUBTYPE_PENT = CPU_SUBTYPE_586
# @see CPU_SUBTYPE_586
# @api private
CPU_SUBTYPE_PENT = CPU_SUBTYPE_586
# the Pentium Pro (P6) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTPRO = 22
# the Pentium II (P6, M3?) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTII_M3 = 54
# the Pentium II (P6, M5?) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTII_M5 = 86
# the Pentium 4 (Netburst) sub-type for `CPU_TYPE_I386`
# @api private
CPU_SUBTYPE_PENTIUM_4 = 10
# the lowest common sub-type for `CPU_TYPE_MC680X0`
# @api private
CPU_SUBTYPE_MC680X0_ALL = 1
# @see CPU_SUBTYPE_MC680X0_ALL
# @api private
CPU_SUBTYPE_MC68030 = CPU_SUBTYPE_MC680X0_ALL
# the 040 subtype for `CPU_TYPE_MC680X0`
# @api private
CPU_SUBTYPE_MC68040 = 2
# the 030 subtype for `CPU_TYPE_MC680X0`
# @api private
CPU_SUBTYPE_MC68030_ONLY = 3
# the lowest common sub-type for `CPU_TYPE_X86_64`
# @api private
CPU_SUBTYPE_X86_64_ALL = CPU_SUBTYPE_I386
# the Haskell sub-type for `CPU_TYPE_X86_64`
# @api private
CPU_SUBTYPE_X86_64_H = 8
# the lowest common sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_ALL = 0
# the v4t sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V4T = 5
# the v6 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V6 = 6
# the v5 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V5TEJ = 7
# the xscale (v5 family) sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_XSCALE = 8
# the v7 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7 = 9
# the v7f (Cortex A9) sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7F = 10
# the v7s ("Swift") sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7S = 11
# the v7k ("Kirkwood40") sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7K = 12
# the v6m sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V6M = 14
# the v7m sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7M = 15
# the v7em sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V7EM = 16
# the v8 sub-type for `CPU_TYPE_ARM`
# @api private
CPU_SUBTYPE_ARM_V8 = 13
# the lowest common sub-type for `CPU_TYPE_ARM64`
# @api private
CPU_SUBTYPE_ARM64_ALL = 0
# the v8 sub-type for `CPU_TYPE_ARM64`
# @api private
CPU_SUBTYPE_ARM64_V8 = 1
# the lowest common sub-type for `CPU_TYPE_MC88000`
# @api private
CPU_SUBTYPE_MC88000_ALL = 0
# @see CPU_SUBTYPE_MC88000_ALL
# @api private
CPU_SUBTYPE_MMAX_JPC = CPU_SUBTYPE_MC88000_ALL
# the 100 sub-type for `CPU_TYPE_MC88000`
# @api private
CPU_SUBTYPE_MC88100 = 1
# the 110 sub-type for `CPU_TYPE_MC88000`
# @api private
CPU_SUBTYPE_MC88110 = 2
# the lowest common sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_ALL = 0
# the 601 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_601 = 1
# the 602 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_602 = 2
# the 603 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_603 = 3
# the 603e (G2) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_603E = 4
# the 603ev sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_603EV = 5
# the 604 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_604 = 6
# the 604e sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_604E = 7
# the 620 sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_620 = 8
# the 750 (G3) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_750 = 9
# the 7400 (G4) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_7400 = 10
# the 7450 (G4 "Voyager") sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_7450 = 11
# the 970 (G5) sub-type for `CPU_TYPE_POWERPC`
# @api private
CPU_SUBTYPE_POWERPC_970 = 100
# any CPU sub-type for CPU type `CPU_TYPE_POWERPC64`
# @api private
CPU_SUBTYPE_POWERPC64_ALL = CPU_SUBTYPE_POWERPC_ALL
# association of CPU types/subtype pairs to symbol representations in
# (very) roughly descending order of commonness
# @see https://opensource.apple.com/source/cctools/cctools-877.8/libstuff/arch.c
# @api private
CPU_SUBTYPES = {
CPU_TYPE_I386 => {
CPU_SUBTYPE_I386 => :i386,
@ -280,53 +355,64 @@ module MachO
}.freeze
# relocatable object file
# @api private
MH_OBJECT = 0x1
# demand paged executable file
# @api private
MH_EXECUTE = 0x2
# fixed VM shared library file
# @api private
MH_FVMLIB = 0x3
# core dump file
# @api private
MH_CORE = 0x4
# preloaded executable file
# @api private
MH_PRELOAD = 0x5
# dynamically bound shared library
# @api private
MH_DYLIB = 0x6
# dynamic link editor
# @api private
MH_DYLINKER = 0x7
# dynamically bound bundle file
# @api private
MH_BUNDLE = 0x8
# shared library stub for static linking only, no section contents
# @api private
MH_DYLIB_STUB = 0x9
# companion file with only debug sections
# @api private
MH_DSYM = 0xa
# x86_64 kexts
# @api private
MH_KEXT_BUNDLE = 0xb
# association of filetypes to string representations
# association of filetypes to Symbol representations
# @api private
MH_FILETYPES = {
MH_OBJECT => "MH_OBJECT",
MH_EXECUTE => "MH_EXECUTE",
MH_FVMLIB => "MH_FVMLIB",
MH_CORE => "MH_CORE",
MH_PRELOAD => "MH_PRELOAD",
MH_DYLIB => "MH_DYLIB",
MH_DYLINKER => "MH_DYLINKER",
MH_BUNDLE => "MH_BUNDLE",
MH_DYLIB_STUB => "MH_DYLIB_STUB",
MH_DSYM => "MH_DSYM",
MH_KEXT_BUNDLE => "MH_KEXT_BUNDLE"
}
MH_OBJECT => :object,
MH_EXECUTE => :execute,
MH_FVMLIB => :fvmlib,
MH_CORE => :core,
MH_PRELOAD => :preload,
MH_DYLIB => :dylib,
MH_DYLINKER => :dylinker,
MH_BUNDLE => :bundle,
MH_DYLIB_STUB => :dylib_stub,
MH_DSYM => :dsym,
MH_KEXT_BUNDLE => :kext_bundle,
}.freeze
# association of mach header flag symbols to values
# @api private
@ -356,8 +442,8 @@ module MachO
:MH_DEAD_STRIPPABLE_DYLIB => 0x400000,
:MH_HAS_TLV_DESCRIPTORS => 0x800000,
:MH_NO_HEAP_EXECUTION => 0x1000000,
:MH_APP_EXTENSION_SAFE => 0x02000000
}
:MH_APP_EXTENSION_SAFE => 0x02000000,
}.freeze
# Fat binary header structure
# @see MachO::FatArch
@ -369,7 +455,12 @@ module MachO
attr_reader :nfat_arch
# always big-endian
FORMAT = "N2"
# @see MachOStructure::FORMAT
# @api private
FORMAT = "N2".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 8
# @api private
@ -399,7 +490,12 @@ module MachO
attr_reader :align
# always big-endian
FORMAT = "N5"
# @see MachOStructure::FORMAT
# @api private
FORMAT = "N5".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 20
# @api private
@ -435,12 +531,17 @@ module MachO
# @return [Fixnum] the header flags associated with the Mach-O
attr_reader :flags
FORMAT = "L=7"
# @see MachOStructure::FORMAT
# @api private
FORMAT = "L=7".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 28
# @api private
def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds,
flags)
flags)
@magic = magic
@cputype = cputype
# For now we're not interested in additional capability bits also to be
@ -468,12 +569,17 @@ module MachO
# @return [void]
attr_reader :reserved
FORMAT = "L=8"
# @see MachOStructure::FORMAT
# @api private
FORMAT = "L=8".freeze
# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 32
# @api private
def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds,
flags, reserved)
flags, reserved)
super(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags)
@reserved = reserved
end

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ module MachO
attr_reader :header
# @return [Array<MachO::LoadCommand>] an array of the file's load commands
# @note load commands are provided in order of ascending offset.
attr_reader :load_commands
# Creates a new MachOFile instance from a binary string.
@ -32,20 +33,20 @@ module MachO
# @param filename [String] the Mach-O file to load from
# @raise [ArgumentError] if the given file does not exist
def initialize(filename)
raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename)
raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
@filename = filename
@raw_data = File.open(@filename, "rb") { |f| f.read }
@header = get_mach_header
@load_commands = get_load_commands
@raw_data = File.open(@filename, "rb", &:read)
populate_fields
end
# Initializes a new MachOFile instance from a binary string.
# @see MachO::MachOFile.new_from_bin
# @api private
def initialize_from_bin(bin)
@filename = nil
@raw_data = bin
@header = get_mach_header
@load_commands = get_load_commands
populate_fields
end
# The file's raw Mach-O data.
@ -56,12 +57,17 @@ module MachO
# @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
def magic32?
MachO.magic32?(header.magic)
Utils.magic32?(header.magic)
end
# @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise
def magic64?
MachO.magic64?(header.magic)
Utils.magic64?(header.magic)
end
# @return [Fixnum] the file's internal alignment
def alignment
magic32? ? 4 : 8
end
# @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise
@ -124,7 +130,7 @@ module MachO
MH_MAGICS[magic]
end
# @return [String] a string representation of the Mach-O's filetype
# @return [Symbol] a string representation of the Mach-O's filetype
def filetype
MH_FILETYPES[header.filetype]
end
@ -164,7 +170,109 @@ module MachO
load_commands.select { |lc| lc.type == name.to_sym }
end
alias :[] :command
alias [] command
# Inserts a load command at the given offset.
# @param offset [Fixnum] the offset to insert at
# @param lc [MachO::LoadCommand] the load command to insert
# @param options [Hash]
# @option options [Boolean] :repopulate (true) whether or not to repopulate
# the instance fields
# @raise [MachO::OffsetInsertionError] if the offset is not in the load command region
# @raise [MachO::HeaderPadError] if the new command exceeds the header pad buffer
# @note Calling this method with an arbitrary offset in the load command
# region **will leave the object in an inconsistent state**.
def insert_command(offset, lc, options = {})
context = LoadCommand::SerializationContext.context_for(self)
cmd_raw = lc.serialize(context)
if offset < header.class.bytesize || offset + cmd_raw.bytesize > low_fileoff
raise OffsetInsertionError, offset
end
new_sizeofcmds = sizeofcmds + cmd_raw.bytesize
if header.class.bytesize + new_sizeofcmds > low_fileoff
raise HeaderPadError, @filename
end
# update Mach-O header fields to account for inserted load command
update_ncmds(ncmds + 1)
update_sizeofcmds(new_sizeofcmds)
@raw_data.insert(offset, cmd_raw)
@raw_data.slice!(header.class.bytesize + new_sizeofcmds, cmd_raw.bytesize)
populate_fields if options.fetch(:repopulate, true)
end
# Replace a load command with another command in the Mach-O, preserving location.
# @param old_lc [MachO::LoadCommand] the load command being replaced
# @param new_lc [MachO::LoadCommand] the load command being added
# @return [void]
# @raise [MachO::HeaderPadError] if the new command exceeds the header pad buffer
# @see {#insert_command}
# @note This is public, but methods like {#dylib_id=} should be preferred.
def replace_command(old_lc, new_lc)
context = LoadCommand::SerializationContext.context_for(self)
cmd_raw = new_lc.serialize(context)
new_sizeofcmds = sizeofcmds + cmd_raw.bytesize - old_lc.cmdsize
if header.class.bytesize + new_sizeofcmds > low_fileoff
raise HeaderPadError, @filename
end
delete_command(old_lc)
insert_command(old_lc.view.offset, new_lc)
end
# Appends a new load command to the Mach-O.
# @param lc [MachO::LoadCommand] the load command being added
# @param options [Hash]
# @option options [Boolean] :repopulate (true) whether or not to repopulate
# the instance fields
# @return [void]
# @see {#insert_command}
# @note This is public, but methods like {#add_rpath} should be preferred.
# Setting `repopulate` to false **will leave the instance in an
# inconsistent state** unless {#populate_fields} is called **immediately**
# afterwards.
def add_command(lc, options = {})
insert_command(header.class.bytesize + sizeofcmds, lc, options)
end
# Delete a load command from the Mach-O.
# @param lc [MachO::LoadCommand] the load command being deleted
# @param options [Hash]
# @option options [Boolean] :repopulate (true) whether or not to repopulate
# the instance fields
# @return [void]
# @note This is public, but methods like {#delete_rpath} should be preferred.
# Setting `repopulate` to false **will leave the instance in an
# inconsistent state** unless {#populate_fields} is called **immediately**
# afterwards.
def delete_command(lc, options = {})
@raw_data.slice!(lc.view.offset, lc.cmdsize)
# update Mach-O header fields to account for deleted load command
update_ncmds(ncmds - 1)
update_sizeofcmds(sizeofcmds - lc.cmdsize)
# pad the space after the load commands to preserve offsets
null_pad = "\x00" * lc.cmdsize
@raw_data.insert(header.class.bytesize + sizeofcmds - lc.cmdsize, null_pad)
populate_fields if options.fetch(:repopulate, true)
end
# Populate the instance's fields with the raw Mach-O data.
# @return [void]
# @note This method is public, but should (almost) never need to be called.
# The exception to this rule is when methods like {#add_command} and
# {#delete_command} have been called with `repopulate = false`.
def populate_fields
@header = populate_mach_header
@load_commands = populate_load_commands
end
# All load commands responsible for loading dylibs.
# @return [Array<MachO::DylibCommand>] an array of DylibCommands
@ -188,9 +296,7 @@ module MachO
# file.dylib_id # => 'libBar.dylib'
# @return [String, nil] the Mach-O's dylib ID
def dylib_id
if !dylib?
return nil
end
return unless dylib?
dylib_id_cmd = command(:LC_ID_DYLIB).first
@ -199,25 +305,30 @@ module MachO
# Changes the Mach-O's dylib ID to `new_id`. Does nothing if not a dylib.
# @example
# file.dylib_id = "libFoo.dylib"
# file.change_dylib_id("libFoo.dylib")
# @param new_id [String] the dylib's new ID
# @param _options [Hash]
# @return [void]
# @raise [ArgumentError] if `new_id` is not a String
def dylib_id=(new_id)
if !new_id.is_a?(String)
raise ArgumentError.new("argument must be a String")
end
# @note `_options` is currently unused and is provided for signature
# compatibility with {MachO::FatFile#change_dylib_id}
def change_dylib_id(new_id, _options = {})
raise ArgumentError, "new ID must be a String" unless new_id.is_a?(String)
return unless dylib?
if !dylib?
return nil
end
old_lc = command(:LC_ID_DYLIB).first
raise DylibIdMissingError unless old_lc
dylib_cmd = command(:LC_ID_DYLIB).first
old_id = dylib_id
new_lc = LoadCommand.create(:LC_ID_DYLIB, new_id,
old_lc.timestamp,
old_lc.current_version,
old_lc.compatibility_version)
set_name_in_dylib(dylib_cmd, old_id, new_id)
replace_command(old_lc, new_lc)
end
alias dylib_id= change_dylib_id
# All shared libraries linked to the Mach-O.
# @return [Array<String>] an array of all shared libraries
def linked_dylibs
@ -233,16 +344,24 @@ module MachO
# file.change_install_name("/usr/lib/libWhatever.dylib", "/usr/local/lib/libWhatever2.dylib")
# @param old_name [String] the shared library's old name
# @param new_name [String] the shared library's new name
# @param _options [Hash]
# @return [void]
# @raise [MachO::DylibUnknownError] if no shared library has the old name
def change_install_name(old_name, new_name)
dylib_cmd = dylib_load_commands.find { |d| d.name.to_s == old_name }
raise DylibUnknownError.new(old_name) if dylib_cmd.nil?
# @note `_options` is currently unused and is provided for signature
# compatibility with {MachO::FatFile#change_install_name}
def change_install_name(old_name, new_name, _options = {})
old_lc = dylib_load_commands.find { |d| d.name.to_s == old_name }
raise DylibUnknownError, old_name if old_lc.nil?
set_name_in_dylib(dylib_cmd, old_name, new_name)
new_lc = LoadCommand.create(old_lc.type, new_name,
old_lc.timestamp,
old_lc.current_version,
old_lc.compatibility_version)
replace_command(old_lc, new_lc)
end
alias :change_dylib :change_install_name
alias change_dylib change_install_name
# All runtime paths searched by the dynamic linker for the Mach-O.
# @return [Array<String>] an array of all runtime paths
@ -255,44 +374,70 @@ module MachO
# file.change_rpath("/usr/lib", "/usr/local/lib")
# @param old_path [String] the old runtime path
# @param new_path [String] the new runtime path
# @param _options [Hash]
# @return [void]
# @raise [MachO::RpathUnknownError] if no such old runtime path exists
# @api private
def change_rpath(old_path, new_path)
rpath_cmd = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
raise RpathUnknownError.new(old_path) if rpath_cmd.nil?
# @raise [MachO::RpathExistsError] if the new runtime path already exists
# @note `_options` is currently unused and is provided for signature
# compatibility with {MachO::FatFile#change_rpath}
def change_rpath(old_path, new_path, _options = {})
old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
raise RpathUnknownError, old_path if old_lc.nil?
raise RpathExistsError, new_path if rpaths.include?(new_path)
set_path_in_rpath(rpath_cmd, old_path, new_path)
new_lc = LoadCommand.create(:LC_RPATH, new_path)
delete_rpath(old_path)
insert_command(old_lc.view.offset, new_lc)
end
# Add the given runtime path to the Mach-O.
# @example
# file.rpaths # => ["/lib"]
# file.add_rpath("/usr/lib")
# file.rpaths # => ["/lib", "/usr/lib"]
# @param path [String] the new runtime path
# @param _options [Hash]
# @return [void]
# @raise [MachO::RpathExistsError] if the runtime path already exists
# @note `_options` is currently unused and is provided for signature
# compatibility with {MachO::FatFile#add_rpath}
def add_rpath(path, _options = {})
raise RpathExistsError, path if rpaths.include?(path)
rpath_cmd = LoadCommand.create(:LC_RPATH, path)
add_command(rpath_cmd)
end
# Delete the given runtime path from the Mach-O.
# @example
# file.rpaths # => ["/lib"]
# file.delete_rpath("/lib")
# file.rpaths # => []
# @param path [String] the runtime path to delete
# @param _options [Hash]
# @return void
# @raise [MachO::RpathUnknownError] if no such runtime path exists
# @note `_options` is currently unused and is provided for signature
# compatibility with {MachO::FatFile#delete_rpath}
def delete_rpath(path, _options = {})
rpath_cmds = command(:LC_RPATH).select { |r| r.path.to_s == path }
raise RpathUnknownError, path if rpath_cmds.empty?
# delete the commands in reverse order, offset descending. this
# allows us to defer (expensive) field population until the very end
rpath_cmds.reverse_each { |cmd| delete_command(cmd, :repopulate => false) }
populate_fields
end
# All sections of the segment `segment`.
# @param segment [MachO::SegmentCommand, MachO::SegmentCommand64] the segment being inspected
# @return [Array<MachO::Section>] if the Mach-O is 32-bit
# @return [Array<MachO::Section64>] if the Mach-O is 64-bit
# @deprecated use {MachO::SegmentCommand#sections} instead
def sections(segment)
sections = []
if !segment.is_a?(SegmentCommand) && !segment.is_a?(SegmentCommand64)
raise ArgumentError.new("not a valid segment")
end
if segment.nsects.zero?
return sections
end
offset = segment.offset + segment.class.bytesize
segment.nsects.times do
if segment.is_a? SegmentCommand
sections << Section.new_from_bin(endianness, @raw_data.slice(offset, Section.bytesize))
offset += Section.bytesize
else
sections << Section64.new_from_bin(endianness, @raw_data.slice(offset, Section64.bytesize))
offset += Section64.bytesize
end
end
sections
segment.sections
end
# Write all Mach-O data to the given filename.
@ -308,7 +453,7 @@ module MachO
# @note Overwrites all data in the file!
def write!
if @filename.nil?
raise MachOError.new("cannot write to a default file when initialized from a binary string")
raise MachOError, "cannot write to a default file when initialized from a binary string"
else
File.open(@filename, "wb") { |f| f.write(@raw_data) }
end
@ -320,13 +465,13 @@ module MachO
# @return [MachO::MachHeader] if the Mach-O is 32-bit
# @return [MachO::MachHeader64] if the Mach-O is 64-bit
# @raise [MachO::TruncatedFileError] if the file is too small to have a valid header
# @private
def get_mach_header
# @api private
def populate_mach_header
# the smallest Mach-O header is 28 bytes
raise TruncatedFileError.new if @raw_data.size < 28
raise TruncatedFileError if @raw_data.size < 28
magic = get_and_check_magic
mh_klass = MachO.magic32?(magic) ? MachHeader : MachHeader64
magic = populate_and_check_magic
mh_klass = Utils.magic32?(magic) ? MachHeader : MachHeader64
mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
check_cputype(mh.cputype)
@ -340,14 +485,14 @@ module MachO
# @return [Fixnum] the magic
# @raise [MachO::MagicError] if the magic is not valid Mach-O magic
# @raise [MachO::FatBinaryError] if the magic is for a Fat file
# @private
def get_and_check_magic
# @api private
def populate_and_check_magic
magic = @raw_data[0..3].unpack("N").first
raise MagicError.new(magic) unless MachO.magic?(magic)
raise FatBinaryError.new if MachO.fat_magic?(magic)
raise MagicError, magic unless Utils.magic?(magic)
raise FatBinaryError if Utils.fat_magic?(magic)
@endianness = MachO.little_magic?(magic) ? :little : :big
@endianness = Utils.little_magic?(magic) ? :little : :big
magic
end
@ -355,47 +500,48 @@ module MachO
# Check the file's CPU type.
# @param cputype [Fixnum] the CPU type
# @raise [MachO::CPUTypeError] if the CPU type is unknown
# @private
# @api private
def check_cputype(cputype)
raise CPUTypeError.new(cputype) unless CPU_TYPES.key?(cputype)
raise CPUTypeError, cputype unless CPU_TYPES.key?(cputype)
end
# Check the file's CPU type/subtype pair.
# @param cpusubtype [Fixnum] the CPU subtype
# @raise [MachO::CPUSubtypeError] if the CPU sub-type is unknown
# @private
# @api private
def check_cpusubtype(cputype, cpusubtype)
# Only check sub-type w/o capability bits (see `get_mach_header`).
# Only check sub-type w/o capability bits (see `populate_mach_header`).
raise CPUSubtypeError.new(cputype, cpusubtype) unless CPU_SUBTYPES[cputype].key?(cpusubtype)
end
# Check the file's type.
# @param filetype [Fixnum] the file type
# @raise [MachO::FiletypeError] if the file type is unknown
# @private
# @api private
def check_filetype(filetype)
raise FiletypeError.new(filetype) unless MH_FILETYPES.key?(filetype)
raise FiletypeError, filetype unless MH_FILETYPES.key?(filetype)
end
# All load commands in the file.
# @return [Array<MachO::LoadCommand>] an array of load commands
# @raise [MachO::LoadCommandError] if an unknown load command is encountered
# @private
def get_load_commands
# @api private
def populate_load_commands
offset = header.class.bytesize
load_commands = []
header.ncmds.times do
fmt = (endianness == :little) ? "L<" : "L>"
fmt = Utils.specialize_format("L=", endianness)
cmd = @raw_data.slice(offset, 4).unpack(fmt).first
cmd_sym = LOAD_COMMANDS[cmd]
raise LoadCommandError.new(cmd) if cmd_sym.nil?
raise LoadCommandError, cmd if cmd_sym.nil?
# why do I do this? i don't like declaring constants below
# classes, and i need them to resolve...
klass = MachO.const_get "#{LC_STRUCTURES[cmd_sym]}"
command = klass.new_from_bin(@raw_data, endianness, offset, @raw_data.slice(offset, klass.bytesize))
klass = MachO.const_get LC_STRUCTURES[cmd_sym]
view = MachOView.new(@raw_data, endianness, offset)
command = klass.new_from_bin(view)
load_commands << command
offset += command.cmdsize
@ -404,108 +550,44 @@ module MachO
load_commands
end
# Updates the size of all load commands in the raw data.
# @param size [Fixnum] the new size, in bytes
# @return [void]
# @private
def set_sizeofcmds(size)
fmt = (endianness == :little) ? "L<" : "L>"
new_size = [size].pack(fmt)
@raw_data[20..23] = new_size
end
# The low file offset (offset to first section data).
# @return [Fixnum] the offset
# @api private
def low_fileoff
offset = @raw_data.size
# Updates the `name` field in a DylibCommand.
# @param dylib_cmd [MachO::DylibCommand] the dylib command
# @param old_name [String] the old dylib name
# @param new_name [String] the new dylib name
# @return [void]
# @private
def set_name_in_dylib(dylib_cmd, old_name, new_name)
set_lc_str_in_cmd(dylib_cmd, dylib_cmd.name, old_name, new_name)
end
# Updates the `path` field in an RpathCommand.
# @param rpath_cmd [MachO::RpathCommand] the rpath command
# @param old_path [String] the old runtime name
# @param new_path [String] the new runtime name
# @return [void]
# @private
def set_path_in_rpath(rpath_cmd, old_path, new_path)
set_lc_str_in_cmd(rpath_cmd, rpath_cmd.path, old_path, new_path)
end
# Updates a generic LCStr field in any LoadCommand.
# @param cmd [MachO::LoadCommand] the load command
# @param lc_str [MachO::LoadCommand::LCStr] the load command string
# @param old_str [String] the old string
# @param new_str [String] the new string
# @raise [MachO::HeaderPadError] if the new name exceeds the header pad buffer
# @private
def set_lc_str_in_cmd(cmd, lc_str, old_str, new_str)
if magic32?
cmd_round = 4
else
cmd_round = 8
end
new_sizeofcmds = header.sizeofcmds
old_str = old_str.dup
new_str = new_str.dup
old_pad = MachO.round(old_str.size + 1, cmd_round) - old_str.size
new_pad = MachO.round(new_str.size + 1, cmd_round) - new_str.size
# pad the old and new IDs with null bytes to meet command bounds
old_str << "\x00" * old_pad
new_str << "\x00" * new_pad
# calculate the new size of the cmd and sizeofcmds in MH
new_size = cmd.class.bytesize + new_str.size
new_sizeofcmds += new_size - cmd.cmdsize
low_fileoff = @raw_data.size
# calculate the low file offset (offset to first section data)
segments.each do |seg|
sections(seg).each do |sect|
next if sect.size == 0
seg.sections.each do |sect|
next if sect.empty?
next if sect.flag?(:S_ZEROFILL)
next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
next unless sect.offset < low_fileoff
next unless sect.offset < offset
low_fileoff = sect.offset
offset = sect.offset
end
end
if new_sizeofcmds + header.class.bytesize > low_fileoff
raise HeaderPadError.new(@filename)
end
offset
end
# update sizeofcmds in mach_header
set_sizeofcmds(new_sizeofcmds)
# Updates the number of load commands in the raw data.
# @param ncmds [Fixnum] the new number of commands
# @return [void]
# @api private
def update_ncmds(ncmds)
fmt = Utils.specialize_format("L=", endianness)
ncmds_raw = [ncmds].pack(fmt)
@raw_data[16..19] = ncmds_raw
end
# update cmdsize in the cmd
fmt = (endianness == :little) ? "L<" : "L>"
@raw_data[cmd.offset + 4, 4] = [new_size].pack(fmt)
# delete the old str
@raw_data.slice!(cmd.offset + lc_str.to_i...cmd.offset + cmd.class.bytesize + old_str.size)
# insert the new str
@raw_data.insert(cmd.offset + lc_str.to_i, new_str)
# pad/unpad after new_sizeofcmds until offsets are corrected
null_pad = old_str.size - new_str.size
if null_pad < 0
@raw_data.slice!(new_sizeofcmds + header.class.bytesize, null_pad.abs)
else
@raw_data.insert(new_sizeofcmds + header.class.bytesize, "\x00" * null_pad)
end
# synchronize fields with the raw data
@header = get_mach_header
@load_commands = get_load_commands
# Updates the size of all load commands in the raw data.
# @param size [Fixnum] the new size, in bytes
# @return [void]
# @api private
def update_sizeofcmds(size)
fmt = Utils.specialize_format("L=", endianness)
size_raw = [size].pack(fmt)
@raw_data[20..23] = size_raw
end
end
end

View File

@ -7,17 +7,17 @@ module MachO
# @raise [MachO::TruncatedFileError] if the file is too small to have a valid header
# @raise [MachO::MagicError] if the file's magic is not valid Mach-O magic
def self.open(filename)
raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename)
raise TruncatedFileError.new unless File.stat(filename).size >= 4
raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
raise TruncatedFileError unless File.stat(filename).size >= 4
magic = File.open(filename, "rb") { |f| f.read(4) }.unpack("N").first
if MachO.fat_magic?(magic)
if Utils.fat_magic?(magic)
file = FatFile.new(filename)
elsif MachO.magic?(magic)
elsif Utils.magic?(magic)
file = MachOFile.new(filename)
else
raise MagicError.new(magic)
raise MagicError, magic
end
file

View File

@ -45,7 +45,7 @@ module MachO
:S_ATTR_DEBUG => 0x02000000,
:S_ATTR_SOME_INSTRUCTIONS => 0x00000400,
:S_ATTR_EXT_RELOC => 0x00000200,
:S_ATTR_LOC_RELOC => 0x00000100
:S_ATTR_LOC_RELOC => 0x00000100,
}.freeze
# association of section name symbols to names
@ -62,7 +62,7 @@ module MachO
:SECT_OBJC_STRINGS => "__selector_strs",
:SECT_OBJC_REFS => "__selector_refs",
:SECT_ICON_HEADER => "__header",
:SECT_ICON_TIFF => "__tiff"
:SECT_ICON_TIFF => "__tiff",
}.freeze
# Represents a section of a segment for 32-bit architectures.
@ -91,7 +91,7 @@ module MachO
# @return [Fixnum] the number of relocation entries
attr_reader :nreloc
# @return [Fixnum] flags for type and addrributes of the section
# @return [Fixnum] flags for type and attributes of the section
attr_reader :flags
# @return [void] reserved (for offset or index)
@ -100,12 +100,15 @@ module MachO
# @return [void] reserved (for count or sizeof)
attr_reader :reserved2
FORMAT = "a16a16L=9"
# @see MachOStructure::FORMAT
FORMAT = "a16a16L=9".freeze
# @see MachOStructure::SIZEOF
SIZEOF = 68
# @api private
def initialize(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2)
nreloc, flags, reserved1, reserved2)
@sectname = sectname
@segname = segname
@addr = addr
@ -121,12 +124,17 @@ module MachO
# @return [String] the section's name, with any trailing NULL characters removed
def section_name
@sectname.delete("\x00")
sectname.delete("\x00")
end
# @return [String] the parent segment's name, with any trailing NULL characters removed
def segment_name
@segname.delete("\x00")
segname.delete("\x00")
end
# @return [Boolean] true if the section has no contents (i.e, `size` is 0)
def empty?
size.zero?
end
# @example
@ -145,12 +153,15 @@ module MachO
# @return [void] reserved
attr_reader :reserved3
FORMAT = "a16a16Q=2L=8"
# @see MachOStructure::FORMAT
FORMAT = "a16a16Q=2L=8".freeze
# @see MachOStructure::SIZEOF
SIZEOF = 80
# @api private
def initialize(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2, reserved3)
nreloc, flags, reserved1, reserved2, reserved3)
super(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2)
@reserved3 = reserved3

View File

@ -3,9 +3,13 @@ module MachO
# @abstract
class MachOStructure
# The String#unpack format of the data structure.
FORMAT = ""
# @return [String] the unpacking format
# @api private
FORMAT = "".freeze
# The size of the data structure, in bytes.
# @return [Fixnum] the size, in bytes
# @api private
SIZEOF = 0
# @return [Fixnum] the size, in bytes, of the represented structure.
@ -13,26 +17,14 @@ module MachO
self::SIZEOF
end
# @param endianness [Symbol] either :big or :little
# @param endianness [Symbol] either `:big` or `:little`
# @param bin [String] the string to be unpacked into the new structure
# @return [MachO::MachOStructure] a new MachOStructure initialized with `bin`
# @api private
def self.new_from_bin(endianness, bin)
format = specialize_format(self::FORMAT, endianness)
format = Utils.specialize_format(self::FORMAT, endianness)
self.new(*bin.unpack(format))
end
private
# Convert an abstract (native-endian) String#unpack format to big or little.
# @param format [String] the format string being converted
# @param endianness [Symbol] either :big or :little
# @return [String] the converted string
# @api private
def self.specialize_format(format, endianness)
modifier = (endianness == :big) ? ">" : "<"
format.tr("=", modifier)
new(*bin.unpack(format))
end
end
end

View File

@ -12,12 +12,14 @@ module MachO
# Changes the dylib ID of a Mach-O or Fat binary, overwriting the source file.
# @param filename [String] the Mach-O or Fat binary being modified
# @param new_id [String] the new dylib ID for the binary
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void]
# @todo unstub for fat files
def self.change_dylib_id(filename, new_id)
def self.change_dylib_id(filename, new_id, options = {})
file = MachO.open(filename)
file.dylib_id = new_id
file.change_dylib_id(new_id, options)
file.write!
end
@ -25,12 +27,14 @@ module MachO
# @param filename [String] the Mach-O or Fat binary being modified
# @param old_name [String] the old shared library name
# @param new_name [String] the new shared library name
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void]
# @todo unstub for fat files
def self.change_install_name(filename, old_name, new_name)
def self.change_install_name(filename, old_name, new_name, options = {})
file = MachO.open(filename)
file.change_install_name(old_name, new_name)
file.change_install_name(old_name, new_name, options)
file.write!
end
@ -38,28 +42,43 @@ module MachO
# @param filename [String] the Mach-O or Fat binary being modified
# @param old_path [String] the old runtime path
# @param new_path [String] the new runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void]
# @todo unstub
def self.change_rpath(filename, old_path, new_path)
raise UnimplementedError.new("changing rpaths in a Mach-O")
def self.change_rpath(filename, old_path, new_path, options = {})
file = MachO.open(filename)
file.change_rpath(old_path, new_path, options)
file.write!
end
# Add a runtime path to a Mach-O or Fat binary, overwriting the source file.
# @param filename [String] the Mach-O or Fat binary being modified
# @param new_path [String] the new runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void]
# @todo unstub
def self.add_rpath(filename, new_path)
raise UnimplementedError.new("adding rpaths to a Mach-O")
def self.add_rpath(filename, new_path, options = {})
file = MachO.open(filename)
file.add_rpath(new_path, options)
file.write!
end
# Delete a runtime path from a Mach-O or Fat binary, overwriting the source file.
# @param filename [String] the Mach-O or Fat binary being modified
# @param old_path [String] the old runtime path
# @param options [Hash]
# @option options [Boolean] :strict (true) whether or not to fail loudly
# with an exception if the change cannot be performed
# @return [void]
# @todo unstub
def self.delete_rpath(filename, old_path)
raise UnimplementedError.new("removing rpaths from a Mach-O")
def self.delete_rpath(filename, old_path, options = {})
file = MachO.open(filename)
file.delete_rpath(old_path, options)
file.write!
end
end
end

View File

@ -1,48 +1,96 @@
module MachO
# @param value [Fixnum] the number being rounded
# @param round [Fixnum] the number being rounded with
# @return [Fixnum] the next number >= `value` such that `round` is its divisor
# @see http://www.opensource.apple.com/source/cctools/cctools-870/libstuff/rnd.c
def self.round(value, round)
round -= 1
value += round
value &= ~round
value
end
# A collection of utility functions used throughout ruby-macho.
module Utils
# Rounds a value to the next multiple of the given round.
# @param value [Fixnum] the number being rounded
# @param round [Fixnum] the number being rounded with
# @return [Fixnum] the rounded value
# @see http://www.opensource.apple.com/source/cctools/cctools-870/libstuff/rnd.c
def self.round(value, round)
round -= 1
value += round
value &= ~round
value
end
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid Mach-O magic number, false otherwise
def self.magic?(num)
MH_MAGICS.has_key?(num)
end
# Returns the number of bytes needed to pad the given size to the given alignment.
# @param size [Fixnum] the unpadded size
# @param alignment [Fixnum] the number to alignment the size with
# @return [Fixnum] the number of pad bytes required
def self.padding_for(size, alignment)
round(size, alignment) - size
end
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid Fat magic number, false otherwise
def self.fat_magic?(num)
num == FAT_MAGIC
end
# Converts an abstract (native-endian) String#unpack format to big or little.
# @param format [String] the format string being converted
# @param endianness [Symbol] either `:big` or `:little`
# @return [String] the converted string
def self.specialize_format(format, endianness)
modifier = endianness == :big ? ">" : "<"
format.tr("=", modifier)
end
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid 32-bit magic number, false otherwise
def self.magic32?(num)
num == MH_MAGIC || num == MH_CIGAM
end
# Packs tagged strings into an aligned payload.
# @param fixed_offset [Fixnum] the baseline offset for the first packed string
# @param alignment [Fixnum] the alignment value to use for packing
# @param strings [Hash] the labeled strings to pack
# @return [Array<String, Hash>] the packed string and labeled offsets
def self.pack_strings(fixed_offset, alignment, strings = {})
offsets = {}
next_offset = fixed_offset
payload = ""
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid 64-bit magic number, false otherwise
def self.magic64?(num)
num == MH_MAGIC_64 || num == MH_CIGAM_64
end
strings.each do |key, string|
offsets[key] = next_offset
payload << string
payload << "\x00"
next_offset += string.bytesize + 1
end
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid little-endian magic number, false otherwise
def self.little_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
end
payload << "\x00" * padding_for(fixed_offset + payload.bytesize, alignment)
[payload, offsets]
end
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid big-endian magic number, false otherwise
def self.big_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
# Compares the given number to valid Mach-O magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid Mach-O magic number, false otherwise
def self.magic?(num)
MH_MAGICS.key?(num)
end
# Compares the given number to valid Fat magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid Fat magic number, false otherwise
def self.fat_magic?(num)
num == FAT_MAGIC
end
# Compares the given number to valid 32-bit Mach-O magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid 32-bit magic number, false otherwise
def self.magic32?(num)
num == MH_MAGIC || num == MH_CIGAM
end
# Compares the given number to valid 64-bit Mach-O magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid 64-bit magic number, false otherwise
def self.magic64?(num)
num == MH_MAGIC_64 || num == MH_CIGAM_64
end
# Compares the given number to valid little-endian magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid little-endian magic number, false otherwise
def self.little_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
end
# Compares the given number to valid big-endian magic numbers.
# @param num [Fixnum] the number being checked
# @return [Boolean] true if `num` is a valid big-endian magic number, false otherwise
def self.big_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
end
end
end

View File

@ -0,0 +1,23 @@
module MachO
# A representation of some unspecified Mach-O data.
class MachOView
# @return [String] the raw Mach-O data
attr_reader :raw_data
# @return [Symbol] the endianness of the data (`:big` or `:little`)
attr_reader :endianness
# @return [Fixnum] the offset of the relevant data (in {#raw_data})
attr_reader :offset
# Creates a new MachOView.
# @param raw_data [String] the raw Mach-O data
# @param endianness [Symbol] the endianness of the data
# @param offset [Fixnum] the offset of the relevant data
def initialize(raw_data, endianness, offset)
@raw_data = raw_data
@endianness = endianness
@offset = offset
end
end
end