vendor: vendor ruby_macho library.

This commit is contained in:
William Woodruff 2015-10-15 16:00:12 -04:00 committed by Mike McQuaid
parent 35e2209c10
commit 1cb6a2ad18
12 changed files with 2489 additions and 0 deletions

53
Library/Homebrew/vendor/README.md vendored Normal file
View File

@ -0,0 +1,53 @@
Vendored Dependencies
=====================
* [okjson](https://github.com/kr/okjson), version 43.
* [ruby-macho](https://github.com/woodruffw/ruby-macho), version 0.2.2.
## Licenses:
### okjson
> Copyright 2011, 2012 Keith Rarick
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.
### ruby-macho
> The MIT License
> Copyright (c) 2015 William Woodruff <william @ tuffbizz.com>
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.

16
Library/Homebrew/vendor/macho/macho.rb vendored Normal file
View File

@ -0,0 +1,16 @@
require "#{File.dirname(__FILE__)}/macho/structure"
require "#{File.dirname(__FILE__)}/macho/headers"
require "#{File.dirname(__FILE__)}/macho/load_commands"
require "#{File.dirname(__FILE__)}/macho/sections"
require "#{File.dirname(__FILE__)}/macho/macho_file"
require "#{File.dirname(__FILE__)}/macho/fat_file"
require "#{File.dirname(__FILE__)}/macho/open"
require "#{File.dirname(__FILE__)}/macho/exceptions"
require "#{File.dirname(__FILE__)}/macho/utils"
require "#{File.dirname(__FILE__)}/macho/tools"
# The primary namespace for ruby-macho.
module MachO
# release version
VERSION = "0.2.2".freeze
end

View File

@ -0,0 +1,85 @@
module MachO
# A generic Mach-O error in execution.
class MachOError < RuntimeError
end
# Raised when a file's magic bytes are not valid Mach-O magic.
class MagicError < MachOError
# @param num [Fixnum] the unknown number
def initialize(num)
super "Unrecognized Mach-O magic: 0x#{"%02x" % num}"
end
end
# Raised when a fat binary is loaded with MachOFile.
class FatBinaryError < MachOError
def initialize
super "Fat binaries must be loaded with MachO::FatFile"
end
end
# Raised when a Mach-O is loaded with FatFile.
class MachOBinaryError < MachOError
def initialize
super "Normal binaries must be loaded with MachO::MachOFile"
end
end
# Raised when the CPU type is unknown.
class CPUTypeError < MachOError
# @param num [Fixnum] the unknown number
def initialize(num)
super "Unrecognized CPU type: 0x#{"%02x" % num}"
end
end
# Raised when the CPU subtype is unknown.
class CPUSubtypeError < MachOError
# @param num [Fixnum] the unknown number
def initialize(num)
super "Unrecognized CPU sub-type: 0x#{"%02x" % num}"
end
end
# Raised when a mach-o file's filetype field is unknown.
class FiletypeError < MachOError
# @param num [Fixnum] the unknown number
def initialize(num)
super "Unrecognized Mach-O filetype code: 0x#{"%02x" % num}"
end
end
# Raised when an unknown load command is encountered.
class LoadCommandError < MachOError
# @param num [Fixnum] the unknown number
def initialize(num)
super "Unrecognized Mach-O load command: 0x#{"%02x" % num}"
end
end
# Raised when load commands are too large to fit in the current file.
class HeaderPadError < MachOError
# @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"
end
end
# Raised when attempting to change a dylib name that doesn't exist.
class DylibUnknownError < MachOError
# @param dylib [String] the unknown shared library name
def initialize(dylib)
super "No such dylib name: #{dylib}"
end
end
# Raised when attempting to change an rpath that doesn't exist.
class RpathUnknownError < MachOError
# @param path [String] the unknown runtime path
def initialize(path)
super "No such runtime path: #{path}"
end
end
end

View File

@ -0,0 +1,230 @@
module MachO
# Represents a "Fat" file, which contains a header, a listing of available
# architectures, and one or more Mach-O binaries.
# @see https://en.wikipedia.org/wiki/Mach-O#Multi-architecture_binaries
# @see MachO::MachOFile
class FatFile
# @return [MachO::FatHeader] the file's header
attr_reader :header
# @return [Array<MachO::FatArch>] an array of fat architectures
attr_reader :fat_archs
# @return [Array<MachO::MachOFile>] an array of Mach-O binaries
attr_reader :machos
# Creates a new FatFile from the given filename.
# @param filename [String] the fat file to load from
# @raise [ArgumentError] if the given filename does not exist
def initialize(filename)
raise ArgumentError.new("#{filetype}: no such file") unless File.exist?(filename)
@filename = filename
@raw_data = open(@filename, "rb") { |f| f.read }
@header = get_fat_header
@fat_archs = get_fat_archs
@machos = get_machos
end
# The file's raw fat data.
# @return [String] the raw fat data
def serialize
@raw_data
end
# @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise
def object?
machos.first.object?
end
# @return [Boolean] true if the file is of type `MH_EXECUTE`, false otherwise
def executable?
machos.first.executable?
end
# @return [Boolean] true if the file is of type `MH_FVMLIB`, false otherwise
def fvmlib?
machos.first.fvmlib?
end
# @return [Boolean] true if the file is of type `MH_CORE`, false otherwise
def core?
machos.first.core?
end
# @return [Boolean] true if the file is of type `MH_PRELOAD`, false otherwise
def preload?
machos.first.preload?
end
# @return [Boolean] true if the file is of type `MH_DYLIB`, false otherwise
def dylib?
machos.first.dylib?
end
# @return [Boolean] true if the file is of type `MH_DYLINKER`, false otherwise
def dylinker?
machos.first.dylinker?
end
# @return [Boolean] true if the file is of type `MH_BUNDLE`, false otherwise
def bundle?
machos.first.bundle?
end
# @return [Boolean] true if the file is of type `MH_DSYM`, false otherwise
def dsym?
machos.first.dsym?
end
# @return [Boolean] true if the file is of type `MH_KEXT_BUNDLE`, false otherwise
def kext?
machos.first.kext?
end
# @return [Fixnum] the file's magic number
def magic
header.magic
end
# @return [String] a string representation of the file's magic number
def magic_string
MH_MAGICS[magic]
end
# The file's type. Assumed to be the same for every Mach-O within.
# @return [String] the filetype
def filetype
machos.first.filetype
end
# The file's dylib ID. If the file is not a dylib, returns `nil`.
# @example
# file.dylib_id # => 'libBar.dylib'
# @return [String, nil] the file's dylib ID
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'
# @param new_id [String] the new dylib ID
# @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
if !machos.all?(&:dylib?)
return nil
end
machos.each do |macho|
macho.dylib_id = new_id
end
synchronize_raw_data
end
# All shared libraries linked to the file's Mach-Os.
# @return [Array<String>] an array of all shared libraries
def linked_dylibs
# can machos inside fat binaries have different dylibs?
machos.flat_map(&:linked_dylibs).uniq
end
# Changes all dependent shared library install names from `old_name` to `new_name`.
# In a fat file, this changes install names in all internal Mach-Os.
# @example
# 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)
end
synchronize_raw_data
end
alias :change_dylib :change_install_name
# Extract a Mach-O with the given CPU type from the file.
# @example
# file.extract("CPU_TYPE_I386") # => MachO::MachOFile
# @param cputype [String] the CPU type of the Mach-O being extracted
# @return [MachO::MachOFile, nil] the extracted Mach-O or nil if no Mach-O has the given CPU type
def extract(cputype)
machos.select { |macho| macho.cputype == cputype }.first
end
# Write all (fat) data to the given filename.
# @param filename [String] the file to write to
def write(filename)
File.open(filename, "wb") { |f| f.write(@raw_data) }
end
# Write all (fat) data to the file used to initialize the instance.
# @note Overwrites all data in the file!
def write!
File.open(@filename, "wb") { |f| f.write(@raw_data) }
end
private
# Obtain the fat header from raw file data.
# @return [MachO::FatHeader] the fat header
# @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
# @private
def get_fat_header
magic, nfat_arch = @raw_data[0..7].unpack("N2")
raise MagicError.new(magic) unless MachO.magic?(magic)
raise MachOBinaryError.new unless MachO.fat_magic?(magic)
FatHeader.new(magic, nfat_arch)
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
archs = []
header.nfat_arch.times do |i|
fields = @raw_data[8 + (FatArch.bytesize * i), FatArch.bytesize].unpack("N5")
archs << FatArch.new(*fields)
end
archs
end
# Obtain an array of Mach-O blobs from raw file data.
# @return [Array<MachO::MachOFile>] an array of Mach-Os
# @private
def get_machos
machos = []
fat_archs.each do |arch|
machos << MachOFile.new_from_bin(@raw_data[arch.offset, arch.size])
end
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
def synchronize_raw_data
machos.each_with_index do |macho, i|
arch = fat_archs[i]
@raw_data[arch.offset, arch.size] = macho.serialize
end
end
end
end

View File

@ -0,0 +1,275 @@
module MachO
# big-endian fat magic
FAT_MAGIC = 0xcafebabe
# little-endian fat magic
FAT_CIGAM = 0xbebafeca
# 32-bit big-endian magic
MH_MAGIC = 0xfeedface
# 32-bit little-endian magic
MH_CIGAM = 0xcefaedfe
# 64-bit big-endian magic
MH_MAGIC_64 = 0xfeedfacf
# 64-bit little-endian magic
MH_CIGAM_64 = 0xcffaedfe
# association of magic numbers to string representations
MH_MAGICS = {
FAT_MAGIC => "FAT_MAGIC",
FAT_CIGAM => "FAT_CIGAM",
MH_MAGIC => "MH_MAGIC",
MH_CIGAM => "MH_CIGAM",
MH_MAGIC_64 => "MH_MAGIC_64",
MH_CIGAM_64 => "MH_CIGAM_64"
}
# mask for CPUs with 64-bit architectures (when running a 64-bit ABI?)
CPU_ARCH_ABI64 = 0x01000000
# any CPU (unused?)
CPU_TYPE_ANY = -1
# x86 compatible CPUs
CPU_TYPE_X86 = 0x07
# i386 and later compatible CPUs
CPU_TYPE_I386 = CPU_TYPE_X86
# x86_64 (AMD64) compatible CPUs
CPU_TYPE_X86_64 = (CPU_TYPE_X86 | CPU_ARCH_ABI64)
# PowerPC compatible CPUs (7400 series?)
CPU_TYPE_POWERPC = 0x12
# PowerPC64 compatible CPUs (970 series?)
CPU_TYPE_POWERPC64 = (CPU_TYPE_POWERPC | CPU_ARCH_ABI64)
# association of cpu types to string representations
CPU_TYPES = {
CPU_TYPE_ANY => "CPU_TYPE_ANY",
CPU_TYPE_X86 => "CPU_TYPE_X86",
CPU_TYPE_I386 => "CPU_TYPE_I386",
CPU_TYPE_X86_64 => "CPU_TYPE_X86_64",
CPU_TYPE_POWERPC => "CPU_TYPE_POWERPC",
CPU_TYPE_POWERPC64 => "CPU_TYPE_POWERPC64"
}
# mask for CPU subtype capabilities
CPU_SUBTYPE_MASK = 0xff000000
# 64-bit libraries (undocumented!)
# @see http://llvm.org/docs/doxygen/html/Support_2MachO_8h_source.html
CPU_SUBTYPE_LIB64 = 0x80000000
# all x86-type CPUs
CPU_SUBTYPE_X86_ALL = 3
# all x86-type CPUs (what makes this different from CPU_SUBTYPE_X86_ALL?)
CPU_SUBTYPE_X86_ARCH1 = 4
# association of cpu subtypes to string representations
CPU_SUBTYPES = {
CPU_SUBTYPE_X86_ALL => "CPU_SUBTYPE_X86_ALL",
CPU_SUBTYPE_X86_ARCH1 => "CPU_SUBTYPE_X86_ARCH1"
}
# relocatable object file
MH_OBJECT = 0x1
# demand paged executable file
MH_EXECUTE = 0x2
# fixed VM shared library file
MH_FVMLIB = 0x3
# core dump file
MH_CORE = 0x4
# preloaded executable file
MH_PRELOAD = 0x5
# dynamically bound shared library
MH_DYLIB = 0x6
# dynamic link editor
MH_DYLINKER = 0x7
# dynamically bound bundle file
MH_BUNDLE = 0x8
# shared library stub for static linking only, no section contents
MH_DYLIB_STUB = 0x9
# companion file with only debug sections
MH_DSYM = 0xa
# x86_64 kexts
MH_KEXT_BUNDLE = 0xb
# association of filetypes to string 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"
}
# association of mach header flag symbols to values
# @api private
MH_FLAGS = {
:MH_NOUNDEFS => 0x1,
:MH_INCRLINK => 0x2,
:MH_DYLDLINK => 0x4,
:MH_BINDATLOAD => 0x8,
:MH_PREBOUND => 0x10,
:MH_SPLIT_SEGS => 0x20,
:MH_LAZY_INIT => 0x40,
:MH_TWOLEVEL => 0x80,
:MH_FORCE_FLAT => 0x100,
:MH_NOMULTIDEFS => 0x200,
:MH_NOPREFIXBINDING => 0x400,
:MH_PREBINDABLE => 0x800,
:MH_ALLMODSBOUND => 0x1000,
:MH_SUBSECTIONS_VIA_SYMBOLS => 0x2000,
:MH_CANONICAL => 0x4000,
:MH_WEAK_DEFINES => 0x8000,
:MH_BINDS_TO_WEAK => 0x10000,
:MH_ALLOW_STACK_EXECUTION => 0x20000,
:MH_ROOT_SAFE => 0x40000,
:MH_SETUID_SAFE => 0x80000,
:MH_NO_REEXPORTED_DYLIBS => 0x100000,
:MH_PIE => 0x200000,
:MH_DEAD_STRIPPABLE_DYLIB => 0x400000,
:MH_HAS_TLV_DESCRIPTORS => 0x800000,
:MH_NO_HEAP_EXECUTION => 0x1000000,
:MH_APP_EXTENSION_SAFE => 0x02000000
}
# Fat binary header structure
# @see MachO::FatArch
class FatHeader < MachOStructure
# @return [Fixnum] the magic number of the header (and file)
attr_reader :magic
# @return [Fixnum] the number of fat architecture structures following the header
attr_reader :nfat_arch
FORMAT = "VV"
SIZEOF = 8
# @api private
def initialize(magic, nfat_arch)
@magic = magic
@nfat_arch = nfat_arch
end
end
# Fat binary header architecture structure. A Fat binary has one or more of
# these, representing one or more internal Mach-O blobs.
# @see MachO::FatHeader
class FatArch < MachOStructure
# @return [Fixnum] the CPU type of the Mach-O
attr_reader :cputype
# @return [Fixnum] the CPU subtype of the Mach-O
attr_reader :cpusubtype
# @return [Fixnum] the file offset to the beginning of the Mach-O data
attr_reader :offset
# @return [Fixnum] the size, in bytes, of the Mach-O data
attr_reader :size
# @return [Fixnum] the alignment, as a power of 2
attr_reader :align
FORMAT = "VVVVV"
SIZEOF = 20
# @api private
def initialize(cputype, cpusubtype, offset, size, align)
@cputype = cputype
@cpusubtype = cpusubtype
@offset = offset
@size = size
@align = align
end
end
# 32-bit Mach-O file header structure
class MachHeader < MachOStructure
# @return [Fixnum] the magic number
attr_reader :magic
# @return [Fixnum] the CPU type of the Mach-O
attr_reader :cputype
# @return [Fixnum] the CPU subtype of the Mach-O
attr_reader :cpusubtype
# @return [Fixnum] the file type of the Mach-O
attr_reader :filetype
# @return [Fixnum] the number of load commands in the Mach-O
attr_reader :ncmds
# @return [Fixnum] the size of all load commands, in bytes, in the Mach-O
attr_reader :sizeofcmds
# @return [Fixnum] the header flags associated with the Mach-O
attr_reader :flags
FORMAT = "VVVVVVV"
SIZEOF = 28
# @api private
def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds,
flags)
@magic = magic
@cputype = cputype
@cpusubtype = cpusubtype
@filetype = filetype
@ncmds = ncmds
@sizeofcmds = sizeofcmds
@flags = flags
end
# @example
# puts "this mach-o has position-independent execution" if header.flag?(:MH_PIE)
# @param flag [Symbol] a mach header flag symbol
# @return [Boolean] true if `flag` is present in the header's flag section
def flag?(flag)
flag = MH_FLAGS[flag]
return false if flag.nil?
flags & flag == flag
end
end
# 64-bit Mach-O file header structure
class MachHeader64 < MachHeader
# @return [void]
attr_reader :reserved
FORMAT = "VVVVVVVV"
SIZEOF = 32
# @api private
def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds,
flags, reserved)
super(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags)
@reserved = reserved
end
end
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,531 @@
module MachO
# Represents a Mach-O file, which contains a header and load commands
# as well as binary executable instructions. Mach-O binaries are
# architecture specific.
# @see https://en.wikipedia.org/wiki/Mach-O
# @see MachO::FatFile
class MachOFile
# @return [MachO::MachHeader] if the Mach-O is 32-bit
# @return [MachO::MachHeader64] if the Mach-O is 64-bit
attr_reader :header
# @return [Array<MachO::LoadCommand>] an array of the file's load commands
attr_reader :load_commands
# Creates a new MachOFile instance from a binary string.
# @param bin [String] a binary string containing raw Mach-O data
# @return [MachO::MachOFile] a new MachOFile
def self.new_from_bin(bin)
instance = allocate
instance.initialize_from_bin(bin)
instance
end
# Creates a new FatFile from the given filename.
# @param filename [String] the Mach-O file to load from
# @raise [ArgumentError] if the given filename does not exist
def initialize(filename)
raise ArgumentError.new("#{filetype}: no such file") unless File.exist?(filename)
@filename = filename
@raw_data = open(@filename, "rb") { |f| f.read }
@header = get_mach_header
@load_commands = get_load_commands
end
# @api private
def initialize_from_bin(bin)
@filename = nil
@raw_data = bin
@header = get_mach_header
@load_commands = get_load_commands
end
# The file's raw Mach-O data.
# @return [String] the raw Mach-O data
def serialize
@raw_data
end
# @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
def magic32?
MachO.magic32?(header.magic)
end
# @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise
def magic64?
MachO.magic64?(header.magic)
end
# @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise
def object?
header.filetype == MH_OBJECT
end
# @return [Boolean] true if the file is of type `MH_EXECUTE`, false otherwise
def executable?
header.filetype == MH_EXECUTE
end
# @return [Boolean] true if the file is of type `MH_FVMLIB`, false otherwise
def fvmlib?
header.filetype == MH_FVMLIB
end
# @return [Boolean] true if the file is of type `MH_CORE`, false otherwise
def core?
header.filetype == MH_CORE
end
# @return [Boolean] true if the file is of type `MH_PRELOAD`, false otherwise
def preload?
header.filetype == MH_PRELOAD
end
# @return [Boolean] true if the file is of type `MH_DYLIB`, false otherwise
def dylib?
header.filetype == MH_DYLIB
end
# @return [Boolean] true if the file is of type `MH_DYLINKER`, false otherwise
def dylinker?
header.filetype == MH_DYLINKER
end
# @return [Boolean] true if the file is of type `MH_BUNDLE`, false otherwise
def bundle?
header.filetype == MH_BUNDLE
end
# @return [Boolean] true if the file is of type `MH_DSYM`, false otherwise
def dsym?
header.filetype == MH_DSYM
end
# @return [Boolean] true if the file is of type `MH_KEXT_BUNDLE`, false otherwise
def kext?
header.filetype == MH_KEXT_BUNDLE
end
# @return [Fixnum] the file's magic number
def magic
header.magic
end
# @return [String] a string representation of the file's magic number
def magic_string
MH_MAGICS[magic]
end
# @return [String] a string representation of the Mach-O's filetype
def filetype
MH_FILETYPES[header.filetype]
end
# @return [String] a string representation of the Mach-O's CPU type
def cputype
CPU_TYPES[header.cputype]
end
# @return [String] a string representation of the Mach-O's CPU subtype
def cpusubtype
CPU_SUBTYPES[header.cpusubtype]
end
# @return [Fixnum] the number of load commands in the Mach-O's header
def ncmds
header.ncmds
end
# @return [Fixnum] the size of all load commands, in bytes
def sizeofcmds
header.sizeofcmds
end
# @return [Fixnum] execution flags set by the linker
def flags
header.flags
end
# All load commands of a given name.
# @example
# file.command("LC_LOAD_DYLIB")
# file[:LC_LOAD_DYLIB]
# @param [String, Symbol] name the load command ID
# @return [Array<MachO::LoadCommand>] an array of LoadCommands corresponding to `name`
def command(name)
load_commands.select { |lc| lc.type == name.to_sym }
end
alias :[] :command
# All load commands responsible for loading dylibs.
# @return [Array<MachO::DylibCommand>] an array of DylibCommands
def dylib_load_commands
load_commands.select { |lc| DYLIB_LOAD_COMMANDS.include?(lc.type) }
end
# All segment load commands in the Mach-O.
# @return [Array<MachO::SegmentCommand>] if the Mach-O is 32-bit
# @return [Array<MachO::SegmentCommand64>] if the Mach-O is 64-bit
def segments
if magic32?
command(:LC_SEGMENT)
else
command(:LC_SEGMENT_64)
end
end
# The Mach-O's dylib ID, or `nil` if not a dylib.
# @example
# file.dylib_id # => 'libBar.dylib'
# @return [String, nil] the Mach-O's dylib ID
def dylib_id
if !dylib?
return nil
end
dylib_id_cmd = command(:LC_ID_DYLIB).first
dylib_id_cmd.name.to_s
end
# Changes the Mach-O's dylib ID to `new_id`. Does nothing if not a dylib.
# @example
# file.dylib_id = "libFoo.dylib"
# @param new_id [String] the dylib's new ID
# @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
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
# All shared libraries linked to the Mach-O.
# @return [Array<String>] an array of all shared libraries
def linked_dylibs
dylib_load_commands.map(&:name).map(&:to_s)
end
# Changes the shared library `old_name` to `new_name`
# @example
# 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
# @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?
set_name_in_dylib(dylib_cmd, old_name, new_name)
end
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
def rpaths
command(:LC_RPATH).map(&:path).map(&:to_s)
end
# Changes the runtime path `old_path` to `new_path`
# @example
# file.change_rpath("/usr/lib", "/usr/local/lib")
# @param old_path [String] the old runtime path
# @param new_path [String] the new runtime path
# @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?
set_path_in_rpath(rpath_cmd, old_path, new_path)
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
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(@raw_data.slice(offset, Section.bytesize))
offset += Section.bytesize
else
sections << Section64.new_from_bin(@raw_data.slice(offset, Section64.bytesize))
offset += Section64.bytesize
end
end
sections
end
# Write all Mach-O data to the given filename.
# @param filename [String] the file to write to
# @return [void]
def write(filename)
File.open(filename, "wb") { |f| f.write(@raw_data) }
end
# Write all Mach-O data to the file used to initialize the instance.
# @raise [MachOError] if the instance was created from a binary string
# @return [void]
# @raise [MachO::MachOError] if the instance was initialized without a file
# @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")
else
File.open(@filename, "wb") { |f| f.write(@raw_data) }
end
end
private
# The file's Mach-O header structure.
# @return [MachO::MachHeader] if the Mach-O is 32-bit
# @return [MachO::MachHeader64] if the Mach-O is 64-bit
# @private
def get_mach_header
magic = get_magic
cputype = get_cputype
cpusubtype = get_cpusubtype
filetype = get_filetype
ncmds = get_ncmds
sizeofcmds = get_sizeofcmds
flags = get_flags
if MachO.magic32?(magic)
MachHeader.new(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags)
else
# the reserved field is...reserved, so just fill it with 0
MachHeader64.new(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags, 0)
end
end
# The file's magic number.
# @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_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)
magic
end
# The file's CPU type.
# @return [Fixnum] the CPU type
# @raise [MachO::CPUTypeError] if the CPU type is unknown
# @private
def get_cputype
cputype = @raw_data[4..7].unpack("V").first
raise CPUTypeError.new(cputype) unless CPU_TYPES.key?(cputype)
cputype
end
# The file's CPU subtype.
# @return [Fixnum] the CPU subtype
# @raise [MachO::CPUSubtypeError] if the CPU subtype is unknown
# @private
def get_cpusubtype
cpusubtype = @raw_data[8..11].unpack("V").first
cpusubtype &= ~CPU_SUBTYPE_LIB64 # this mask isn't documented!
raise CPUSubtypeError.new(cpusubtype) unless CPU_SUBTYPES.key?(cpusubtype)
cpusubtype
end
# The file's type.
# @return [Fixnum] the file type
# @raise [MachO::FiletypeError] if the file type is unknown
# @private
def get_filetype
filetype = @raw_data[12..15].unpack("V").first
raise FiletypeError.new(filetype) unless MH_FILETYPES.key?(filetype)
filetype
end
# The number of load commands in the file.
# @return [Fixnum] the number of load commands
# @private
def get_ncmds
@raw_data[16..19].unpack("V").first
end
# The size of all load commands, in bytes.
# return [Fixnum] the size of all load commands
# @private
def get_sizeofcmds
@raw_data[20..23].unpack("V").first
end
# The Mach-O header's flags.
# @return [Fixnum] the flags
# @private
def get_flags
@raw_data[24..27].unpack("V").first
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
offset = header.class.bytesize
load_commands = []
header.ncmds.times do
cmd = @raw_data.slice(offset, 4).unpack("V").first
cmd_sym = LOAD_COMMANDS[cmd]
raise LoadCommandError.new(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, offset, @raw_data.slice(offset, klass.bytesize))
load_commands << command
offset += command.cmdsize
end
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)
new_size = [size].pack("V")
@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 = 2**64 # ULLONGMAX
# calculate the low file offset (offset to first section data)
segments.each do |seg|
sections(seg).each do |sect|
if sect.size != 0 && !sect.flag?(:S_ZEROFILL) &&
!sect.flag?(:S_THREAD_LOCAL_ZEROFILL) &&
sect.offset < low_fileoff
low_fileoff = sect.offset
end
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
@raw_data[cmd.offset + 4, 4] = [new_size].pack("V")
# 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
end
end
end

View File

@ -0,0 +1,16 @@
module MachO
# Opens the given filename as a MachOFile or FatFile, depending on its magic.
# @param filename [String] the file being opened
# @return [MachO::MachOFile] if the file is a Mach-O
# @return [MachO::FatFile] if the file is a Fat file
def self.open(filename)
# open file and test magic instead of using exceptions for control?
begin
file = MachOFile.new(filename)
rescue FatBinaryError
file = FatFile.new(filename)
end
file
end
end

View File

@ -0,0 +1,159 @@
module MachO
# type mask
SECTION_TYPE = 0x000000ff
# attributes mask
SECTION_ATTRIBUTES = 0xffffff00
# user settable attributes mask
SECTION_ATTRIBUTES_USR = 0xff000000
# system settable attributes mask
SECTION_ATTRIBUTES_SYS = 0x00ffff00
# association of section flag symbols to values
# @api private
SECTION_FLAGS = {
:S_REGULAR => 0x0,
:S_ZEROFILL => 0x1,
:S_CSTRING_LITERALS => 0x2,
:S_4BYTE_LITERALS => 0x3,
:S_8BYTE_LITERALS => 0x4,
:S_LITERAL_POINTERS => 0x5,
:S_NON_LAZY_SYMBOL_POINTERS => 0x6,
:S_LAZY_SYMBOL_POINTERS => 0x7,
:S_SYMBOL_STUBS => 0x8,
:S_MOD_INIT_FUNC_POINTERS => 0x9,
:S_MOD_TERM_FUNC_POINTERS => 0xa,
:S_COALESCED => 0xb,
:S_GB_ZEROFILE => 0xc,
:S_INTERPOSING => 0xd,
:S_16BYTE_LITERALS => 0xe,
:S_DTRACE_DOF => 0xf,
:S_LAZY_DYLIB_SYMBOL_POINTERS => 0x10,
:S_THREAD_LOCAL_REGULAR => 0x11,
:S_THREAD_LOCAL_ZEROFILL => 0x12,
:S_THREAD_LOCAL_VARIABLES => 0x13,
:S_THREAD_LOCAL_VARIABLE_POINTERS => 0x14,
:S_THREAD_LOCAL_INIT_FUNCTION_POINTERS => 0x15,
:S_ATTR_PURE_INSTRUCTIONS => 0x80000000,
:S_ATTR_NO_TOC => 0x40000000,
:S_ATTR_STRIP_STATIC_SYMS => 0x20000000,
:S_ATTR_NO_DEAD_STRIP => 0x10000000,
:S_ATTR_LIVE_SUPPORT => 0x08000000,
:S_ATTR_SELF_MODIFYING_CODE => 0x04000000,
:S_ATTR_DEBUG => 0x02000000,
:S_ATTR_SOME_INSTRUCTIONS => 0x00000400,
:S_ATTR_EXT_RELOC => 0x00000200,
:S_ATTR_LOC_RELOC => 0x00000100
}
# association of section name symbols to names
# @api private
SECTION_NAMES = {
:SECT_TEXT => "__text",
:SECT_FVMLIB_INIT0 => "__fvmlib_init0",
:SECT_FVMLIB_INIT1 => "__fvmlib_init1",
:SECT_DATA => "__data",
:SECT_BSS => "__bss",
:SECT_COMMON => "__common",
:SECT_OBJC_SYMBOLS => "__symbol_table",
:SECT_OBJC_MODULES => "__module_info",
:SECT_OBJC_STRINGS => "__selector_strs",
:SECT_OBJC_REFS => "__selector_refs",
:SECT_ICON_HEADER => "__header",
:SECT_ICON_TIFF => "__tiff"
}
# Represents a section of a segment for 32-bit architectures.
class Section < MachOStructure
# @return [String] the name of the section, including null pad bytes
attr_reader :sectname
# @return [String] the name of the segment's section, including null pad bytes
attr_reader :segname
# @return [Fixnum] the memory address of the section
attr_reader :addr
# @return [Fixnum] the size, in bytes, of the section
attr_reader :size
# @return [Fixnum] the file offset of the section
attr_reader :offset
# @return [Fixnum] the section alignment (power of 2) of the section
attr_reader :align
# @return [Fixnum] the file offset of the section's relocation entries
attr_reader :reloff
# @return [Fixnum] the number of relocation entries
attr_reader :nreloc
# @return [Fixnum] flags for type and addrributes of the section
attr_reader :flags
# @return [void] reserved (for offset or index)
attr_reader :reserved1
# @return [void] reserved (for count or sizeof)
attr_reader :reserved2
FORMAT = "a16a16VVVVVVVVV"
SIZEOF = 68
# @api private
def initialize(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2)
@sectname = sectname
@segname = segname
@addr = addr
@size = size
@offset = offset
@align = align
@reloff = reloff
@nreloc = nreloc
@flags = flags
@reserved1 = reserved1
@reserved2 = reserved2
end
# @return [String] the section's name, with any trailing NULL characters removed
def section_name
@sectname.delete("\x00")
end
# @return [String] the parent segment's name, with any trailing NULL characters removed
def segment_name
@segname.delete("\x00")
end
# @example
# puts "this section is regular" if sect.flag?(:S_REGULAR)
# @param flag [Symbol] a section flag symbol
# @return [Boolean] true if `flag` is present in the section's flag field
def flag?(flag)
flag = SECTION_FLAGS[flag]
return false if flag.nil?
flags & flag == flag
end
end
# Represents a section of a segment for 64-bit architectures.
class Section64 < Section
# @return [void] reserved
attr_reader :reserved3
FORMAT = "a16a16QQVVVVVVVV"
SIZEOF = 80
# @api private
def initialize(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2, reserved3)
super(sectname, segname, addr, size, offset, align, reloff,
nreloc, flags, reserved1, reserved2)
@reserved3 = reserved3
end
end
end

View File

@ -0,0 +1,22 @@
module MachO
# A general purpose pseudo-structure.
# @abstract
class MachOStructure
# The format of the data structure, in String#unpack format.
FORMAT = ""
# The size of the data structure, in bytes.
SIZEOF = 0
# @return [Fixnum] the size, in bytes, of the represented structure.
def self.bytesize
self::SIZEOF
end
# @return [MachO::MachOStructure] a new MachOStructure initialized with `bin`
# @api private
def self.new_from_bin(bin)
self.new(*bin.unpack(self::FORMAT))
end
end
end

View File

@ -0,0 +1,65 @@
module MachO
# A collection of convenient methods for common operations on Mach-O and Fat binaries.
module Tools
# @param filename [String] the Mach-O or Fat binary being read
# @return [Array<String>] an array of all dylibs linked to the binary
def self.dylibs(filename)
file = MachO.open(filename)
file.linked_dylibs
end
# 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
# @return [void]
# @todo unstub for fat files
def self.change_dylib_id(filename, new_id)
file = MachO.open(filename)
file.dylib_id = new_id
file.write!
end
# Changes a shared library install name in a Mach-O or Fat binary, overwriting the source file.
# @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
# @return [void]
# @todo unstub for fat files
def self.change_install_name(filename, old_name, new_name)
file = MachO.open(filename)
file.change_install_name(old_name, new_name)
file.write!
end
# Changes a runtime path in 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 new_path [String] the new runtime path
# @return [void]
# @todo unstub
def self.change_rpath(filename, old_path, new_path)
raise "stub"
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
# @return [void]
# @todo unstub
def self.add_rpath(filename, new_path)
raise "stub"
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
# @return [void]
# @todo unstub
def self.delete_rpath(filename, old_path)
raise "stub"
end
end
end

View File

@ -0,0 +1,36 @@
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
# @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
# @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 || num == FAT_CIGAM
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
# @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
end