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 " +
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
# @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,7 +531,12 @@ 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
@ -468,7 +569,12 @@ 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

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,24 +305,29 @@ 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")
# @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?
old_lc = command(:LC_ID_DYLIB).first
raise DylibIdMissingError unless old_lc
new_lc = LoadCommand.create(:LC_ID_DYLIB, new_id,
old_lc.timestamp,
old_lc.current_version,
old_lc.compatibility_version)
replace_command(old_lc, new_lc)
end
if !dylib?
return nil
end
dylib_cmd = command(:LC_ID_DYLIB).first
old_id = dylib_id
set_name_in_dylib(dylib_cmd, old_id, new_id)
end
alias dylib_id= change_dylib_id
# All shared libraries linked to the Mach-O.
# @return [Array<String>] an array of all shared libraries
@ -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
# The low file offset (offset to first section data).
# @return [Fixnum] the offset
# @api private
def low_fileoff
offset = @raw_data.size
segments.each do |seg|
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 < offset
offset = sect.offset
end
end
offset
end
# 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
# 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
# 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
next if sect.flag?(:S_ZEROFILL)
next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
next unless sect.offset < low_fileoff
low_fileoff = sect.offset
end
end
if new_sizeofcmds + header.class.bytesize > low_fileoff
raise HeaderPadError.new(@filename)
end
# update sizeofcmds in mach_header
set_sizeofcmds(new_sizeofcmds)
# 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
# @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,7 +100,10 @@ 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
@ -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,7 +153,10 @@ 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

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,7 +1,10 @@
module MachO
# 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 next number >= `value` such that `round` is its divisor
# @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
@ -10,39 +13,84 @@ module MachO
value
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
# 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
# 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 = ""
strings.each do |key, string|
offsets[key] = next_offset
payload << string
payload << "\x00"
next_offset += string.bytesize + 1
end
payload << "\x00" * padding_for(fixed_offset + payload.bytesize, alignment)
[payload, offsets]
end
# 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.has_key?(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