vendor: Update ruby-macho to 1.1.0.

This commit is contained in:
William Woodruff 2017-03-26 01:23:14 -04:00
parent 422afa0b49
commit 024264c381
No known key found for this signature in database
GPG Key ID: 85AE00C504833B3C
12 changed files with 2358 additions and 2257 deletions

View File

@ -3,7 +3,7 @@ Vendored Dependencies
* [plist](https://github.com/bleything/plist), version 3.1.0
* [ruby-macho](https://github.com/Homebrew/ruby-macho), version 0.2.6
* [ruby-macho](https://github.com/Homebrew/ruby-macho), version 1.1.0
## Licenses:
@ -33,7 +33,7 @@ Vendored Dependencies
### ruby-macho
> The MIT License
> Copyright (c) 2015, 2016 William Woodruff <william @ tuffbizz.com>
> Copyright (c) 2015, 2016, 2017 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

View File

@ -5,7 +5,6 @@ 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"
@ -13,5 +12,29 @@ require "#{File.dirname(__FILE__)}/macho/tools"
# The primary namespace for ruby-macho.
module MachO
# release version
VERSION = "0.2.6".freeze
VERSION = "1.1.0".freeze
# Opens the given filename as a MachOFile or FatFile, depending on its magic.
# @param filename [String] the file being opened
# @return [MachOFile] if the file is a Mach-O
# @return [FatFile] if the file is a Fat file
# @raise [ArgumentError] if the given file does not exist
# @raise [TruncatedFileError] if the file is too small to have a valid header
# @raise [MagicError] if the file's magic is not valid Mach-O magic
def self.open(filename)
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 Utils.fat_magic?(magic)
file = FatFile.new(filename)
elsif Utils.magic?(magic)
file = MachOFile.new(filename)
else
raise MagicError, magic
end
file
end
end

View File

@ -80,7 +80,8 @@ module MachO
# @param cputype [Fixnum] the CPU type of the unknown pair
# @param cpusubtype [Fixnum] the CPU sub-type of the unknown pair
def initialize(cputype, cpusubtype)
super "Unrecognized CPU sub-type: 0x#{"%08x" % cpusubtype} (for CPU type: 0x#{"%08x" % cputype})"
super "Unrecognized CPU sub-type: 0x#{"%08x" % cpusubtype}" \
" (for CPU type: 0x#{"%08x" % cputype})"
end
end
@ -108,13 +109,15 @@ module MachO
end
end
# Raised when the number of arguments used to create a load command manually is wrong.
# 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}"
super "Expected #{expected_arity} arguments for #{cmd_sym} creation," \
" got #{actual_arity}"
end
end
@ -130,7 +133,8 @@ module MachO
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"
super "Load command #{lc.type} at offset #{lc.view.offset} contains a" \
" malformed string"
end
end

View File

@ -1,24 +1,50 @@
require "forwardable"
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
# @see MachOFile
class FatFile
extend Forwardable
# @return [String] the filename loaded from, or nil if loaded from a binary string
attr_accessor :filename
# @return [MachO::FatHeader] the file's header
# @return [Headers::FatHeader] the file's header
attr_reader :header
# @return [Array<MachO::FatArch>] an array of fat architectures
# @return [Array<Headers::FatArch>] an array of fat architectures
attr_reader :fat_archs
# @return [Array<MachO::MachOFile>] an array of Mach-O binaries
# @return [Array<MachOFile>] an array of Mach-O binaries
attr_reader :machos
# Creates a new FatFile from the given (single-arch) Mach-Os
# @param machos [Array<MachOFile>] the machos to combine
# @return [FatFile] a new FatFile containing the give machos
def self.new_from_machos(*machos)
header = Headers::FatHeader.new(Headers::FAT_MAGIC, machos.size)
offset = Headers::FatHeader.bytesize + (machos.size * Headers::FatArch.bytesize)
fat_archs = []
machos.each do |macho|
fat_archs << Headers::FatArch.new(macho.header.cputype,
macho.header.cpusubtype,
offset, macho.serialize.bytesize,
macho.alignment)
offset += macho.serialize.bytesize
end
bin = header.serialize
bin << fat_archs.map(&:serialize).join
bin << machos.map(&:serialize).join
new_from_bin(bin)
end
# Creates a new FatFile instance from a binary string.
# @param bin [String] a binary string containing raw Mach-O data
# @return [MachO::FatFile] a new FatFile
# @return [FatFile] a new FatFile
def self.new_from_bin(bin)
instance = allocate
instance.initialize_from_bin(bin)
@ -38,7 +64,7 @@ module MachO
end
# Initializes a new FatFile instance from a binary string.
# @see MachO::FatFile.new_from_bin
# @see new_from_bin
# @api private
def initialize_from_bin(bin)
@filename = nil
@ -52,70 +78,41 @@ module MachO
@raw_data
end
# @return [Boolean] true if the file is of type `MH_OBJECT`, false otherwise
def object?
machos.first.object?
end
# @!method object?
# @return (see MachO::MachOFile#object?)
# @!method executable?
# @return (see MachO::MachOFile#executable?)
# @!method fvmlib?
# @return (see MachO::MachOFile#fvmlib?)
# @!method core?
# @return (see MachO::MachOFile#core?)
# @!method preload?
# @return (see MachO::MachOFile#preload?)
# @!method dylib?
# @return (see MachO::MachOFile#dylib?)
# @!method dylinker?
# @return (see MachO::MachOFile#dylinker?)
# @!method bundle?
# @return (see MachO::MachOFile#bundle?)
# @!method dsym?
# @return (see MachO::MachOFile#dsym?)
# @!method kext?
# @return (see MachO::MachOFile#kext?)
# @!method filetype
# @return (see MachO::MachOFile#filetype)
# @!method dylib_id
# @return (see MachO::MachOFile#dylib_id)
def_delegators :canonical_macho, :object?, :executable?, :fvmlib?,
:core?, :preload?, :dylib?, :dylinker?, :bundle?,
:dsym?, :kext?, :filetype, :dylib_id
# @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
# @!method magic
# @return (see MachO::Headers::FatHeader#magic)
def_delegators :header, :magic
# @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 [Symbol] the filetype
def filetype
machos.first.filetype
Headers::MH_MAGICS[magic]
end
# Populate the instance's fields with the raw Fat Mach-O data.
@ -128,21 +125,13 @@ module MachO
end
# All load commands responsible for loading dylibs in the file's Mach-O's.
# @return [Array<MachO::DylibCommand>] an array of DylibCommands
# @return [Array<LoadCommands::DylibCommand>] an array of DylibCommands
def dylib_load_commands
machos.map(&:dylib_load_commands).flatten
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
# @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.
# Changes the file's dylib ID to `new_id`. If the file is not a dylib,
# does nothing.
# @example
# file.change_dylib_id('libFoo.dylib')
# @param new_id [String] the new dylib ID
@ -151,7 +140,7 @@ module MachO
# if false, fail only if all slices fail.
# @return [void]
# @raise [ArgumentError] if `new_id` is not a String
# @see MachO::MachOFile#linked_dylibs
# @see 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?)
@ -167,7 +156,7 @@ module MachO
# All shared libraries linked to the file's Mach-Os.
# @return [Array<String>] an array of all shared libraries
# @see MachO::MachOFile#linked_dylibs
# @see 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.
@ -175,8 +164,9 @@ module MachO
machos.map(&:linked_dylibs).flatten.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.
# 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
@ -185,7 +175,7 @@ module MachO
# @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
# @see 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)
@ -198,7 +188,7 @@ module MachO
# All runtime paths associated with the file's Mach-Os.
# @return [Array<String>] an array of all runtime paths
# @see MachO::MachOFile#rpaths
# @see MachOFile#rpaths
def rpaths
# Can individual architectures have different runtime paths?
machos.map(&:rpaths).flatten.uniq
@ -211,7 +201,7 @@ module MachO
# @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
# @see MachOFile#change_rpath
def change_rpath(old_path, new_path, options = {})
each_macho(options) do |macho|
macho.change_rpath(old_path, new_path, options)
@ -226,7 +216,7 @@ module MachO
# @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
# @see MachOFile#add_rpath
def add_rpath(path, options = {})
each_macho(options) do |macho|
macho.add_rpath(path, options)
@ -241,7 +231,7 @@ module MachO
# @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
# @see MachOFile#delete_rpath
def delete_rpath(path, options = {})
each_macho(options) do |macho|
macho.delete_rpath(path, options)
@ -254,20 +244,21 @@ module MachO
# @example
# file.extract(:i386) # => MachO::MachOFile
# @param cputype [Symbol] 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
# @return [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
# @return [void]
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.
# @return [void]
# @raise [MachO::MachOError] if the instance was initialized without a file
# @raise [MachOError] if the instance was initialized without a file
# @note Overwrites all data in the file!
def write!
if filename.nil?
@ -280,17 +271,18 @@ module MachO
private
# Obtain the fat header from raw file data.
# @return [MachO::FatHeader] the fat header
# @raise [MachO::TruncatedFileError] if the file is too small to have a valid 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
# @raise [MachO::JavaClassFileError] if the file is a Java classfile
# @return [Headers::FatHeader] the fat header
# @raise [TruncatedFileError] if the file is too small to have a
# valid header
# @raise [MagicError] if the magic is not valid Mach-O magic
# @raise [MachOBinaryError] if the magic is for a non-fat Mach-O file
# @raise [JavaClassFileError] if the file is a Java classfile
# @api private
def populate_fat_header
# the smallest fat Mach-O header is 8 bytes
raise TruncatedFileError if @raw_data.size < 8
fh = FatHeader.new_from_bin(:big, @raw_data[0, FatHeader.bytesize])
fh = Headers::FatHeader.new_from_bin(:big, @raw_data[0, Headers::FatHeader.bytesize])
raise MagicError, fh.magic unless Utils.magic?(fh.magic)
raise MachOBinaryError unless Utils.fat_magic?(fh.magic)
@ -308,22 +300,22 @@ module MachO
end
# Obtain an array of fat architectures from raw file data.
# @return [Array<MachO::FatArch>] an array of fat architectures
# @return [Array<Headers::FatArch>] an array of fat architectures
# @api private
def populate_fat_archs
archs = []
fa_off = FatHeader.bytesize
fa_len = FatArch.bytesize
fa_off = Headers::FatHeader.bytesize
fa_len = Headers::FatArch.bytesize
header.nfat_arch.times do |i|
archs << FatArch.new_from_bin(:big, @raw_data[fa_off + (fa_len * i), fa_len])
archs << Headers::FatArch.new_from_bin(:big, @raw_data[fa_off + (fa_len * i), fa_len])
end
archs
end
# Obtain an array of Mach-O blobs from raw file data.
# @return [Array<MachO::MachOFile>] an array of Mach-Os
# @return [Array<MachOFile>] an array of Mach-Os
# @api private
def populate_machos
machos = []
@ -351,7 +343,7 @@ module MachO
# @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
# @raise [RecoverableModificationError] under the conditions of
# the `:strict` option above.
# @api private
def each_macho(options = {})
@ -373,5 +365,13 @@ module MachO
# Non-strict mode: Raise first error if *all* Mach-O slices failed.
raise errors.first if errors.size == machos.size
end
# Return a single-arch Mach-O that represents this fat Mach-O for purposes
# of delegation.
# @return [MachOFile] the Mach-O file
# @api private
def canonical_macho
machos.first
end
end
end

View File

@ -1,4 +1,6 @@
module MachO
# Classes and constants for parsing the headers of Mach-O binaries.
module Headers
# big-endian fat magic
# @api private
FAT_MAGIC = 0xcafebabe
@ -468,11 +470,16 @@ module MachO
@magic = magic
@nfat_arch = nfat_arch
end
# @return [String] the serialized fields of the fat header
def serialize
[magic, nfat_arch].pack(FORMAT)
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
# @see MachO::Headers::FatHeader
class FatArch < MachOStructure
# @return [Fixnum] the CPU type of the Mach-O
attr_reader :cputype
@ -506,6 +513,11 @@ module MachO
@size = size
@align = align
end
# @return [String] the serialized fields of the fat arch
def serialize
[cputype, cpusubtype, offset, size, align].pack(FORMAT)
end
end
# 32-bit Mach-O file header structure
@ -562,6 +574,71 @@ module MachO
return false if flag.nil?
flags & flag == flag
end
# @return [Boolean] whether or not the file is of type `MH_OBJECT`
def object?
filetype == Headers::MH_OBJECT
end
# @return [Boolean] whether or not the file is of type `MH_EXECUTE`
def executable?
filetype == Headers::MH_EXECUTE
end
# @return [Boolean] whether or not the file is of type `MH_FVMLIB`
def fvmlib?
filetype == Headers::MH_FVMLIB
end
# @return [Boolean] whether or not the file is of type `MH_CORE`
def core?
filetype == Headers::MH_CORE
end
# @return [Boolean] whether or not the file is of type `MH_PRELOAD`
def preload?
filetype == Headers::MH_PRELOAD
end
# @return [Boolean] whether or not the file is of type `MH_DYLIB`
def dylib?
filetype == Headers::MH_DYLIB
end
# @return [Boolean] whether or not the file is of type `MH_DYLINKER`
def dylinker?
filetype == Headers::MH_DYLINKER
end
# @return [Boolean] whether or not the file is of type `MH_BUNDLE`
def bundle?
filetype == Headers::MH_BUNDLE
end
# @return [Boolean] whether or not the file is of type `MH_DSYM`
def dsym?
filetype == Headers::MH_DSYM
end
# @return [Boolean] whether or not the file is of type `MH_KEXT_BUNDLE`
def kext?
filetype == Headers::MH_KEXT_BUNDLE
end
# @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
def magic32?
Utils.magic32?(magic)
end
# @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise
def magic64?
Utils.magic64?(magic)
end
# @return [Fixnum] the file's internal alignment
def alignment
magic32? ? 4 : 8
end
end
# 64-bit Mach-O file header structure
@ -585,3 +662,4 @@ module MachO
end
end
end
end

View File

@ -1,6 +1,8 @@
module MachO
# Classes and constants for parsing load commands in Mach-O binaries.
module LoadCommands
# load commands added after OS X 10.1 need to be bitwise ORed with
# LC_REQ_DYLD to be recognized by the dynamic linder (dyld)
# LC_REQ_DYLD to be recognized by the dynamic linker (dyld)
# @api private
LC_REQ_DYLD = 0x80000000
@ -85,14 +87,21 @@ module MachO
LC_STRUCTURES = {
:LC_SEGMENT => "SegmentCommand",
:LC_SYMTAB => "SymtabCommand",
:LC_SYMSEG => "SymsegCommand", # obsolete
:LC_THREAD => "ThreadCommand", # seems obsolete, but not documented as such
# "obsolete"
:LC_SYMSEG => "SymsegCommand",
# seems obsolete, but not documented as such
:LC_THREAD => "ThreadCommand",
:LC_UNIXTHREAD => "ThreadCommand",
:LC_LOADFVMLIB => "FvmlibCommand", # obsolete
:LC_IDFVMLIB => "FvmlibCommand", # obsolete
:LC_IDENT => "IdentCommand", # obsolete
:LC_FVMFILE => "FvmfileCommand", # reserved for internal use only
:LC_PREPAGE => "LoadCommand", # reserved for internal use only, no public struct
# "obsolete"
:LC_LOADFVMLIB => "FvmlibCommand",
# "obsolete"
:LC_IDFVMLIB => "FvmlibCommand",
# "obsolete"
:LC_IDENT => "IdentCommand",
# "reserved for internal use only"
:LC_FVMFILE => "FvmfileCommand",
# "reserved for internal use only", no public struct
:LC_PREPAGE => "LoadCommand",
:LC_DYSYMTAB => "DysymtabCommand",
:LC_LOAD_DYLIB => "DylibCommand",
:LC_ID_DYLIB => "DylibCommand",
@ -180,7 +189,7 @@ module MachO
# Instantiates a new LoadCommand given a view into its origin Mach-O
# @param view [MachO::MachOView] the load command's raw view
# @return [MachO::LoadCommand] the new load command
# @return [LoadCommand] the new load command
# @api private
def self.new_from_bin(view)
bin = view.raw_data.slice(view.offset, bytesize)
@ -195,7 +204,7 @@ module MachO
def self.create(cmd_sym, *args)
raise LoadCommandNotCreatableError, cmd_sym unless CREATABLE_LOAD_COMMANDS.include?(cmd_sym)
klass = MachO.const_get LC_STRUCTURES[cmd_sym]
klass = LoadCommands.const_get LC_STRUCTURES[cmd_sym]
cmd = LOAD_COMMAND_CONSTANTS[cmd_sym]
# cmd will be filled in, view and cmdsize will be left unpopulated
@ -216,12 +225,12 @@ module MachO
@cmdsize = cmdsize
end
# @return [Boolean] true if the load command can be serialized, false otherwise
# @return [Boolean] whether the load command can be serialized
def serializable?
CREATABLE_LOAD_COMMANDS.include?(LOAD_COMMANDS[cmd])
end
# @param context [MachO::LoadCommand::SerializationContext] the context
# @param context [SerializationContext] the context
# to serialize into
# @return [String, nil] the serialized fields of the load command, or nil
# if the load command can't be serialized
@ -238,14 +247,16 @@ module MachO
view.offset
end
# @return [Symbol] a symbol representation of the load command's identifying number
# @return [Symbol] a symbol representation of the load command's
# identifying number
def type
LOAD_COMMANDS[cmd]
end
alias to_sym type
# @return [String] a string representation of the load command's identifying number
# @return [String] a string representation of the load command's
# identifying number
def to_s
type.to_s
end
@ -255,9 +266,9 @@ module MachO
# pretend that strings stored in LCs are immediately available without
# explicit operations on the raw Mach-O data.
class LCStr
# @param lc [MachO::LoadCommand] the load command
# @param lc_str [Fixnum, String] the offset to the beginning of the string,
# or the string itself if not being initialized with a view.
# @param lc [LoadCommand] the load command
# @param lc_str [Fixnum, String] the offset to the beginning of the
# string, or the string itself if not being initialized with a view.
# @raise [MachO::LCStrMalformedError] if the string is malformed
# @todo devise a solution such that the `lc_str` parameter is not
# interpreted differently depending on `lc.view`. The current behavior
@ -284,7 +295,8 @@ module MachO
@string
end
# @return [Fixnum] the offset to the beginning of the string in the load command
# @return [Fixnum] the offset to the beginning of the string in the
# load command
def to_i
@string_offset
end
@ -296,11 +308,13 @@ module MachO
# @return [Symbol] the endianness of the serialized load command
attr_reader :endianness
# @return [Fixnum] the constant alignment value used to pad the serialized load command
# @return [Fixnum] the constant alignment value used to pad the
# serialized load command
attr_reader :alignment
# @param macho [MachO::MachOFile] the file to contextualize
# @return [MachO::LoadCommand::SerializationContext] the resulting context
# @return [SerializationContext] the
# resulting context
def self.context_for(macho)
new(macho.endianness, macho.alignment)
end
@ -315,8 +329,9 @@ module MachO
end
end
# A load command containing a single 128-bit unique random number identifying
# an object produced by static link editor. Corresponds to LC_UUID.
# A load command containing a single 128-bit unique random number
# identifying an object produced by static link editor. Corresponds to
# LC_UUID.
class UUIDCommand < LoadCommand
# @return [Array<Fixnum>] the UUID
attr_reader :uuid
@ -401,17 +416,20 @@ module MachO
end
# All sections referenced within this segment.
# @return [Array<MachO::Section>] if the Mach-O is 32-bit
# @return [Array<MachO::Section64>] if the Mach-O is 64-bit
# @return [Array<MachO::Sections::Section>] if the Mach-O is 32-bit
# @return [Array<MachO::Sections::Section64>] if the Mach-O is 64-bit
def sections
klass = case self
when MachO::SegmentCommand64
MachO::Section64
when MachO::SegmentCommand
MachO::Section
when SegmentCommand64
MachO::Sections::Section64
when SegmentCommand
MachO::Sections::Section
end
bins = view.raw_data[view.offset + self.class.bytesize, nsects * klass.bytesize]
offset = view.offset + self.class.bytesize
length = nsects * klass.bytesize
bins = view.raw_data[offset, length]
bins.unpack("a#{klass.bytesize}" * nsects).map do |bin|
klass.new_from_bin(view.endianness, bin)
end
@ -441,10 +459,11 @@ module MachO
end
# A load command representing some aspect of shared libraries, depending
# on filetype. Corresponds to LC_ID_DYLIB, LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB,
# and LC_REEXPORT_DYLIB.
# on filetype. Corresponds to LC_ID_DYLIB, LC_LOAD_DYLIB,
# LC_LOAD_WEAK_DYLIB, and LC_REEXPORT_DYLIB.
class DylibCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the library's path name as an LCStr
# @return [LCStr] the library's path
# name as an LCStr
attr_reader :name
# @return [Fixnum] the library's build time stamp
@ -465,7 +484,8 @@ module MachO
SIZEOF = 24
# @api private
def initialize(view, cmd, cmdsize, name, timestamp, current_version, compatibility_version)
def initialize(view, cmd, cmdsize, name, timestamp, current_version,
compatibility_version)
super(view, cmd, cmdsize)
@name = LCStr.new(self, name)
@timestamp = timestamp
@ -473,12 +493,15 @@ module MachO
@compatibility_version = compatibility_version
end
# @param context [MachO::LoadCcommand::SerializationContext] the context
# @param context [SerializationContext]
# the context
# @return [String] the serialized fields of the load command
# @api private
def serialize(context)
format = Utils.specialize_format(FORMAT, context.endianness)
string_payload, string_offsets = Utils.pack_strings(SIZEOF, context.alignment, :name => name.to_s)
string_payload, string_offsets = Utils.pack_strings(SIZEOF,
context.alignment,
:name => name.to_s)
cmdsize = SIZEOF + string_payload.bytesize
[cmd, cmdsize, string_offsets[:name], timestamp, current_version,
compatibility_version].pack(format) + string_payload
@ -489,7 +512,8 @@ module MachO
# on filetype. Corresponds to LC_ID_DYLINKER, LC_LOAD_DYLINKER, and
# LC_DYLD_ENVIRONMENT.
class DylinkerCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the dynamic linker's path name as an LCStr
# @return [LCStr] the dynamic linker's
# path name as an LCStr
attr_reader :name
# @see MachOStructure::FORMAT
@ -506,12 +530,15 @@ module MachO
@name = LCStr.new(self, name)
end
# @param context [MachO::LoadCcommand::SerializationContext] the context
# @param context [SerializationContext]
# the context
# @return [String] the serialized fields of the load command
# @api private
def serialize(context)
format = Utils.specialize_format(FORMAT, context.endianness)
string_payload, string_offsets = Utils.pack_strings(SIZEOF, context.alignment, :name => name.to_s)
string_payload, string_offsets = Utils.pack_strings(SIZEOF,
context.alignment,
:name => name.to_s)
cmdsize = SIZEOF + string_payload.bytesize
[cmd, cmdsize, string_offsets[:name]].pack(format) + string_payload
end
@ -520,7 +547,8 @@ module MachO
# A load command used to indicate dynamic libraries used in prebinding.
# Corresponds to LC_PREBOUND_DYLIB.
class PreboundDylibCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the library's path name as an LCStr
# @return [LCStr] the library's path
# name as an LCStr
attr_reader :name
# @return [Fixnum] the number of modules in the library
@ -547,7 +575,8 @@ module MachO
end
# A load command used to represent threads.
# @note cctools-870 has all fields of thread_command commented out except common ones (cmd, cmdsize)
# @note cctools-870 and onwards have all fields of thread_command commented
# out except the common ones (cmd, cmdsize)
class ThreadCommand < LoadCommand
# @see MachOStructure::FORMAT
# @api private
@ -565,7 +594,8 @@ module MachO
# @return [Fixnum] the address of the initialization routine
attr_reader :init_address
# @return [Fixnum] the index into the module table that the init routine is defined in
# @return [Fixnum] the index into the module table that the init routine
# is defined in
attr_reader :init_module
# @return [void]
@ -625,7 +655,7 @@ module MachO
# A load command signifying membership of a subframework containing the name
# of an umbrella framework. Corresponds to LC_SUB_FRAMEWORK.
class SubFrameworkCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the umbrella framework name as an LCStr
# @return [LCStr] the umbrella framework name as an LCStr
attr_reader :umbrella
# @see MachOStructure::FORMAT
@ -646,7 +676,7 @@ module MachO
# A load command signifying membership of a subumbrella containing the name
# of an umbrella framework. Corresponds to LC_SUB_UMBRELLA.
class SubUmbrellaCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the subumbrella framework name as an LCStr
# @return [LCStr] the subumbrella framework name as an LCStr
attr_reader :sub_umbrella
# @see MachOStructure::FORMAT
@ -667,7 +697,7 @@ module MachO
# A load command signifying a sublibrary of a shared library. Corresponds
# to LC_SUB_LIBRARY.
class SubLibraryCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the sublibrary name as an LCStr
# @return [LCStr] the sublibrary name as an LCStr
attr_reader :sub_library
# @see MachOStructure::FORMAT
@ -688,7 +718,7 @@ module MachO
# A load command signifying a shared library that is a subframework of
# an umbrella framework. Corresponds to LC_SUB_CLIENT.
class SubClientCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the subclient name as an LCStr
# @return [LCStr] the subclient name as an LCStr
attr_reader :sub_client
# @see MachOStructure::FORMAT
@ -841,7 +871,8 @@ module MachO
# @return [Fixnum] the number of hints in the hint table
attr_reader :nhints
# @return [MachO::TwolevelHintsCommand::TwolevelHintTable] the hint table
# @return [TwolevelHintsTable]
# the hint table
attr_reader :table
# @see MachOStructure::FORMAT
@ -863,7 +894,7 @@ module MachO
# A representation of the two-level namespace lookup hints table exposed
# by a {TwolevelHintsCommand} (`LC_TWOLEVEL_HINTS`).
class TwolevelHintsTable
# @return [Array<MachO::TwoLevelHintsTable::TwoLevelHint>] all hints in the table
# @return [Array<TwolevelHint>] all hints in the table
attr_reader :hints
# @param view [MachO::MachOView] the view into the current Mach-O
@ -921,7 +952,7 @@ module MachO
# be added to the current run path used to find @rpath prefixed dylibs.
# Corresponds to LC_RPATH.
class RpathCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the path to add to the run path as an LCStr
# @return [LCStr] the path to add to the run path as an LCStr
attr_reader :path
# @see MachOStructure::FORMAT
@ -938,20 +969,23 @@ module MachO
@path = LCStr.new(self, path)
end
# @param context [MachO::LoadCcommand::SerializationContext] the context
# @param context [SerializationContext] the context
# @return [String] the serialized fields of the load command
# @api private
def serialize(context)
format = Utils.specialize_format(FORMAT, context.endianness)
string_payload, string_offsets = Utils.pack_strings(SIZEOF, context.alignment, :path => path.to_s)
string_payload, string_offsets = Utils.pack_strings(SIZEOF,
context.alignment,
:path => path.to_s)
cmdsize = SIZEOF + string_payload.bytesize
[cmd, cmdsize, string_offsets[:path]].pack(format) + string_payload
end
end
# A load command representing the offsets and sizes of a blob of data in
# the __LINKEDIT segment. Corresponds to LC_CODE_SIGNATURE, LC_SEGMENT_SPLIT_INFO,
# LC_FUNCTION_STARTS, LC_DATA_IN_CODE, LC_DYLIB_CODE_SIGN_DRS, and LC_LINKER_OPTIMIZATION_HINT.
# the __LINKEDIT segment. Corresponds to LC_CODE_SIGNATURE,
# LC_SEGMENT_SPLIT_INFO, LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
# LC_DYLIB_CODE_SIGN_DRS, and LC_LINKER_OPTIMIZATION_HINT.
class LinkeditDataCommand < LoadCommand
# @return [Fixnum] offset to the data in the __LINKEDIT segment
attr_reader :dataoff
@ -1038,7 +1072,8 @@ module MachO
end
# A load command containing the minimum OS version on which the binary
# was built to run. Corresponds to LC_VERSION_MIN_MACOSX and LC_VERSION_MIN_IPHONEOS.
# was built to run. Corresponds to LC_VERSION_MIN_MACOSX and
# LC_VERSION_MIN_IPHONEOS.
class VersionMinCommand < LoadCommand
# @return [Fixnum] the version X.Y.Z packed as x16.y8.z8
attr_reader :version
@ -1247,9 +1282,9 @@ module MachO
end
end
# An obsolete load command containing a free format string table. Each string
# is null-terminated and the command is zero-padded to a multiple of 4.
# Corresponds to LC_IDENT.
# An obsolete load command containing a free format string table. Each
# string is null-terminated and the command is zero-padded to a multiple of
# 4. Corresponds to LC_IDENT.
class IdentCommand < LoadCommand
# @see MachOStructure::FORMAT
# @api private
@ -1263,7 +1298,7 @@ module MachO
# An obsolete load command containing the path to a file to be loaded into
# memory. Corresponds to LC_FVMFILE.
class FvmfileCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the pathname of the file being loaded
# @return [LCStr] the pathname of the file being loaded
attr_reader :name
# @return [Fixnum] the virtual address being loaded at
@ -1284,10 +1319,10 @@ module MachO
end
end
# An obsolete load command containing the path to a library to be loaded into
# memory. Corresponds to LC_LOADFVMLIB and LC_IDFVMLIB.
# An obsolete load command containing the path to a library to be loaded
# into memory. Corresponds to LC_LOADFVMLIB and LC_IDFVMLIB.
class FvmlibCommand < LoadCommand
# @return [MachO::LoadCommand::LCStr] the library's target pathname
# @return [LCStr] the library's target pathname
attr_reader :name
# @return [Fixnum] the library's minor version number
@ -1312,3 +1347,4 @@ module MachO
end
end
end
end

View File

@ -1,27 +1,33 @@
require "forwardable"
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
# @see FatFile
class MachOFile
# @return [String] the filename loaded from, or nil if loaded from a binary string
extend Forwardable
# @return [String] the filename loaded from, or nil if loaded from a binary
# string
attr_accessor :filename
# @return [Symbol] the endianness of the file, :big or :little
attr_reader :endianness
# @return [MachO::MachHeader] if the Mach-O is 32-bit
# @return [MachO::MachHeader64] if the Mach-O is 64-bit
# @return [Headers::MachHeader] if the Mach-O is 32-bit
# @return [Headers::MachHeader64] if the Mach-O is 64-bit
attr_reader :header
# @return [Array<MachO::LoadCommand>] an array of the file's load commands
# @return [Array<LoadCommands::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.
# @param bin [String] a binary string containing raw Mach-O data
# @return [MachO::MachOFile] a new MachOFile
# @return [MachOFile] a new MachOFile
def self.new_from_bin(bin)
instance = allocate
instance.initialize_from_bin(bin)
@ -55,109 +61,63 @@ module MachO
@raw_data
end
# @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
def magic32?
Utils.magic32?(header.magic)
end
# @return [Boolean] true if the Mach-O has 64-bit magic, false otherwise
def magic64?
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
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
# @!method magic
# @return (see MachO::Headers::MachHeader#magic)
# @!method ncmds
# @return (see MachO::Headers::MachHeader#ncmds)
# @!method sizeofcmds
# @return (see MachO::Headers::MachHeader#sizeofcmds)
# @!method flags
# @return (see MachO::Headers::MachHeader#flags)
# @!method object?
# @return (see MachO::Headers::MachHeader#object?)
# @!method executable?
# @return (see MachO::Headers::MachHeader#executable?)
# @!method fvmlib?
# @return (see MachO::Headers::MachHeader#fvmlib?)
# @!method core?
# @return (see MachO::Headers::MachHeader#core?)
# @!method preload?
# @return (see MachO::Headers::MachHeader#preload?)
# @!method dylib?
# @return (see MachO::Headers::MachHeader#dylib?)
# @!method dylinker?
# @return (see MachO::Headers::MachHeader#dylinker?)
# @!method bundle?
# @return (see MachO::Headers::MachHeader#bundle?)
# @!method dsym?
# @return (see MachO::Headers::MachHeader#dsym?)
# @!method kext?
# @return (see MachO::Headers::MachHeader#kext?)
# @!method magic32?
# @return (see MachO::Headers::MachHeader#magic32?)
# @!method magic64?
# @return (see MachO::Headers::MachHeader#magic64?)
# @!method alignment
# @return (see MachO::Headers::MachHeader#alignment)
def_delegators :header, :magic, :ncmds, :sizeofcmds, :flags, :object?,
:executable?, :fvmlib?, :core?, :preload?, :dylib?,
:dylinker?, :bundle?, :dsym?, :kext?, :magic32?, :magic64?,
:alignment
# @return [String] a string representation of the file's magic number
def magic_string
MH_MAGICS[magic]
Headers::MH_MAGICS[magic]
end
# @return [Symbol] a string representation of the Mach-O's filetype
def filetype
MH_FILETYPES[header.filetype]
Headers::MH_FILETYPES[header.filetype]
end
# @return [Symbol] a symbol representation of the Mach-O's CPU type
def cputype
CPU_TYPES[header.cputype]
Headers::CPU_TYPES[header.cputype]
end
# @return [Symbol] a symbol representation of the Mach-O's CPU subtype
def cpusubtype
CPU_SUBTYPES[header.cputype][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
Headers::CPU_SUBTYPES[header.cputype][header.cpusubtype]
end
# All load commands of a given name.
@ -165,7 +125,8 @@ module MachO
# 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`
# @return [Array<LoadCommands::LoadCommand>] an array of load commands
# corresponding to `name`
def command(name)
load_commands.select { |lc| lc.type == name.to_sym }
end
@ -174,16 +135,16 @@ module MachO
# 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 lc [LoadCommands::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
# @raise [OffsetInsertionError] if the offset is not in the load command region
# @raise [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)
context = LoadCommands::LoadCommand::SerializationContext.context_for(self)
cmd_raw = lc.serialize(context)
if offset < header.class.bytesize || offset + cmd_raw.bytesize > low_fileoff
@ -207,14 +168,14 @@ module MachO
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
# @param old_lc [LoadCommands::LoadCommand] the load command being replaced
# @param new_lc [LoadCommands::LoadCommand] the load command being added
# @return [void]
# @raise [MachO::HeaderPadError] if the new command exceeds the header pad buffer
# @see {#insert_command}
# @raise [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)
context = LoadCommands::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
@ -226,12 +187,12 @@ module MachO
end
# Appends a new load command to the Mach-O.
# @param lc [MachO::LoadCommand] the load command being added
# @param lc [LoadCommands::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}
# @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**
@ -241,7 +202,7 @@ module MachO
end
# Delete a load command from the Mach-O.
# @param lc [MachO::LoadCommand] the load command being deleted
# @param lc [LoadCommands::LoadCommand] the load command being deleted
# @param options [Hash]
# @option options [Boolean] :repopulate (true) whether or not to repopulate
# the instance fields
@ -275,14 +236,14 @@ module MachO
end
# All load commands responsible for loading dylibs.
# @return [Array<MachO::DylibCommand>] an array of DylibCommands
# @return [Array<LoadCommands::DylibCommand>] an array of DylibCommands
def dylib_load_commands
load_commands.select { |lc| DYLIB_LOAD_COMMANDS.include?(lc.type) }
load_commands.select { |lc| LoadCommands::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
# @return [Array<LoadCommands::SegmentCommand>] if the Mach-O is 32-bit
# @return [Array<LoadCommands::SegmentCommand64>] if the Mach-O is 64-bit
def segments
if magic32?
command(:LC_SEGMENT)
@ -319,7 +280,7 @@ module MachO
old_lc = command(:LC_ID_DYLIB).first
raise DylibIdMissingError unless old_lc
new_lc = LoadCommand.create(:LC_ID_DYLIB, new_id,
new_lc = LoadCommands::LoadCommand.create(:LC_ID_DYLIB, new_id,
old_lc.timestamp,
old_lc.current_version,
old_lc.compatibility_version)
@ -341,19 +302,19 @@ module MachO
# Changes the shared library `old_name` to `new_name`
# @example
# file.change_install_name("/usr/lib/libWhatever.dylib", "/usr/local/lib/libWhatever2.dylib")
# file.change_install_name("abc.dylib", "def.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
# @raise [DylibUnknownError] if no shared library has the old name
# @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?
new_lc = LoadCommand.create(old_lc.type, new_name,
new_lc = LoadCommands::LoadCommand.create(old_lc.type, new_name,
old_lc.timestamp,
old_lc.current_version,
old_lc.compatibility_version)
@ -376,8 +337,8 @@ module MachO
# @param new_path [String] the new runtime path
# @param _options [Hash]
# @return [void]
# @raise [MachO::RpathUnknownError] if no such old runtime path exists
# @raise [MachO::RpathExistsError] if the new runtime path already exists
# @raise [RpathUnknownError] if no such old runtime path exists
# @raise [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 = {})
@ -385,7 +346,7 @@ module MachO
raise RpathUnknownError, old_path if old_lc.nil?
raise RpathExistsError, new_path if rpaths.include?(new_path)
new_lc = LoadCommand.create(:LC_RPATH, new_path)
new_lc = LoadCommands::LoadCommand.create(:LC_RPATH, new_path)
delete_rpath(old_path)
insert_command(old_lc.view.offset, new_lc)
@ -399,13 +360,13 @@ module MachO
# @param path [String] the new runtime path
# @param _options [Hash]
# @return [void]
# @raise [MachO::RpathExistsError] if the runtime path already exists
# @raise [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)
rpath_cmd = LoadCommands::LoadCommand.create(:LC_RPATH, path)
add_command(rpath_cmd)
end
@ -417,7 +378,7 @@ module MachO
# @param path [String] the runtime path to delete
# @param _options [Hash]
# @return void
# @raise [MachO::RpathUnknownError] if no such runtime path exists
# @raise [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 = {})
@ -431,15 +392,6 @@ module MachO
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)
segment.sections
end
# Write all Mach-O data to the given filename.
# @param filename [String] the file to write to
# @return [void]
@ -449,7 +401,7 @@ module MachO
# Write all Mach-O data to the file used to initialize the instance.
# @return [void]
# @raise [MachO::MachOError] if the instance was initialized without a file
# @raise [MachOError] if the instance was initialized without a file
# @note Overwrites all data in the file!
def write!
if @filename.nil?
@ -462,16 +414,16 @@ module MachO
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
# @raise [MachO::TruncatedFileError] if the file is too small to have a valid header
# @return [Headers::MachHeader] if the Mach-O is 32-bit
# @return [Headers::MachHeader64] if the Mach-O is 64-bit
# @raise [TruncatedFileError] if the file is too small to have a valid header
# @api private
def populate_mach_header
# the smallest Mach-O header is 28 bytes
raise TruncatedFileError if @raw_data.size < 28
magic = populate_and_check_magic
mh_klass = Utils.magic32?(magic) ? MachHeader : MachHeader64
mh_klass = Utils.magic32?(magic) ? Headers::MachHeader : Headers::MachHeader64
mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
check_cputype(mh.cputype)
@ -483,8 +435,8 @@ module MachO
# Read just the file's magic number and check its validity.
# @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
# @raise [MagicError] if the magic is not valid Mach-O magic
# @raise [FatBinaryError] if the magic is for a Fat file
# @api private
def populate_and_check_magic
magic = @raw_data[0..3].unpack("N").first
@ -499,32 +451,32 @@ module MachO
# Check the file's CPU type.
# @param cputype [Fixnum] the CPU type
# @raise [MachO::CPUTypeError] if the CPU type is unknown
# @raise [CPUTypeError] if the CPU type is unknown
# @api private
def check_cputype(cputype)
raise CPUTypeError, cputype unless CPU_TYPES.key?(cputype)
raise CPUTypeError, cputype unless Headers::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
# @raise [CPUSubtypeError] if the CPU sub-type is unknown
# @api private
def check_cpusubtype(cputype, cpusubtype)
# Only check sub-type w/o capability bits (see `populate_mach_header`).
raise CPUSubtypeError.new(cputype, cpusubtype) unless CPU_SUBTYPES[cputype].key?(cpusubtype)
raise CPUSubtypeError.new(cputype, cpusubtype) unless Headers::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
# @raise [FiletypeError] if the file type is unknown
# @api private
def check_filetype(filetype)
raise FiletypeError, filetype unless MH_FILETYPES.key?(filetype)
raise FiletypeError, filetype unless Headers::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
# @return [Array<LoadCommands::LoadCommand>] an array of load commands
# @raise [LoadCommandError] if an unknown load command is encountered
# @api private
def populate_load_commands
offset = header.class.bytesize
@ -533,13 +485,13 @@ module MachO
header.ncmds.times do
fmt = Utils.specialize_format("L=", endianness)
cmd = @raw_data.slice(offset, 4).unpack(fmt).first
cmd_sym = LOAD_COMMANDS[cmd]
cmd_sym = LoadCommands::LOAD_COMMANDS[cmd]
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]
klass = LoadCommands.const_get LoadCommands::LC_STRUCTURES[cmd_sym]
view = MachOView.new(@raw_data, endianness, offset)
command = klass.new_from_bin(view)

View File

@ -1,25 +0,0 @@
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
# @raise [ArgumentError] if the given file does not exist
# @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, "#{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 Utils.fat_magic?(magic)
file = FatFile.new(filename)
elsif Utils.magic?(magic)
file = MachOFile.new(filename)
else
raise MagicError, magic
end
file
end
end

View File

@ -1,4 +1,6 @@
module MachO
# Classes and constants for parsing sections in Mach-O binaries.
module Sections
# type mask
SECTION_TYPE = 0x000000ff
@ -70,7 +72,8 @@ module MachO
# @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
# @return [String] the name of the segment's section, including null
# pad bytes
attr_reader :segname
# @return [Fixnum] the memory address of the section
@ -122,17 +125,19 @@ module MachO
@reserved2 = reserved2
end
# @return [String] the section's name, with any trailing NULL characters removed
# @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
# @return [String] the parent segment's name, with any trailing NULL
# characters removed
def segment_name
segname.delete("\x00")
end
# @return [Boolean] true if the section has no contents (i.e, `size` is 0)
# @return [Boolean] whether the section is empty (i.e, {size} is 0)
def empty?
size.zero?
end
@ -140,7 +145,7 @@ module MachO
# @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
# @return [Boolean] whether the flag is present in the section's {flags}
def flag?(flag)
flag = SECTION_FLAGS[flag]
return false if flag.nil?
@ -168,3 +173,4 @@ module MachO
end
end
end
end

View File

@ -19,7 +19,7 @@ module MachO
# @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`
# @return [MachO::MachOStructure] the resulting structure
# @api private
def self.new_from_bin(endianness, bin)
format = Utils.specialize_format(self::FORMAT, endianness)

View File

@ -1,5 +1,6 @@
module MachO
# A collection of convenient methods for common operations on Mach-O and Fat binaries.
# 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
@ -9,7 +10,8 @@ module MachO
file.linked_dylibs
end
# Changes the dylib ID of a Mach-O or Fat binary, overwriting the source file.
# 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]
@ -23,7 +25,8 @@ module MachO
file.write!
end
# Changes a shared library install name in a Mach-O or Fat binary, overwriting the source file.
# 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
@ -38,7 +41,8 @@ module MachO
file.write!
end
# Changes a runtime path in a Mach-O or Fat binary, overwriting the source file.
# 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
@ -67,7 +71,8 @@ module MachO
file.write!
end
# Delete a runtime path from a Mach-O or Fat binary, overwriting the source file.
# 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]
@ -80,5 +85,24 @@ module MachO
file.delete_rpath(old_path, options)
file.write!
end
# Merge multiple Mach-Os into one universal (Fat) binary.
# @param filename [String] the fat binary to create
# @param files [Array<MachO::MachOFile, MachO::FatFile>] the files to merge
# @return [void]
def self.merge_machos(filename, *files)
machos = files.map do |file|
macho = MachO.open(file)
case macho
when MachO::MachOFile
macho
else
macho.machos
end
end.flatten
fat_macho = MachO::FatFile.new_from_machos(*machos)
fat_macho.write(filename)
end
end
end

View File

@ -5,7 +5,7 @@ module MachO
# @param value [Fixnum] the number being rounded
# @param round [Fixnum] the number being rounded with
# @return [Fixnum] the rounded value
# @see https://www.opensource.apple.com/source/cctools/cctools-870/libstuff/rnd.c
# @see http://www.opensource.apple.com/source/cctools/cctools-870/libstuff/rnd.c
def self.round(value, round)
round -= 1
value += round
@ -13,7 +13,8 @@ module MachO
value
end
# Returns the number of bytes needed to pad the given size to the given alignment.
# 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
@ -21,7 +22,8 @@ module MachO
round(size, alignment) - size
end
# Converts an abstract (native-endian) String#unpack format to big or little.
# 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
@ -31,7 +33,8 @@ module MachO
end
# Packs tagged strings into an aligned payload.
# @param fixed_offset [Fixnum] the baseline offset for the first packed string
# @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
@ -53,44 +56,44 @@ module MachO
# 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
# @return [Boolean] whether `num` is a valid Mach-O magic number
def self.magic?(num)
MH_MAGICS.key?(num)
Headers::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
# @return [Boolean] whether `num` is a valid Fat magic number
def self.fat_magic?(num)
num == FAT_MAGIC
num == Headers::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
# @return [Boolean] whether `num` is a valid 32-bit magic number
def self.magic32?(num)
num == MH_MAGIC || num == MH_CIGAM
num == Headers::MH_MAGIC || num == Headers::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
# @return [Boolean] whether `num` is a valid 64-bit magic number
def self.magic64?(num)
num == MH_MAGIC_64 || num == MH_CIGAM_64
num == Headers::MH_MAGIC_64 || num == Headers::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
# @return [Boolean] whether `num` is a valid little-endian magic number
def self.little_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
num == Headers::MH_CIGAM || num == Headers::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
# @return [Boolean] whether `num` is a valid big-endian magic number
def self.big_magic?(num)
num == MH_CIGAM || num == MH_CIGAM_64
num == Headers::MH_CIGAM || num == Headers::MH_CIGAM_64
end
end
end