Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/macho/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,14 @@ def initialize(thing)
super "Unimplemented: #{thing}"
end
end

# Raised when attempting to create a {FatFile} from one or more {MachOFile}s
# whose offsets will not fit within the resulting 32-bit {Headers::FatArch#offset} fields.
class FatArchOffsetOverflowError < MachOError
# @param offset [Integer] the offending offset
def initialize(offset)
super "Offset #{offset} exceeds the 32-bit width of a fat_arch offset." \
" Consider merging with `fat64: true`"
end
end
end
44 changes: 29 additions & 15 deletions lib/macho/fat_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,57 @@ class FatFile
# @return [Headers::FatHeader] the file's header
attr_reader :header

# @return [Array<Headers::FatArch>] an array of fat architectures
# @return [Array<Headers::FatArch>, Array<Headers::FatArch64] an array of fat architectures
attr_reader :fat_archs

# @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
# @param fat64 [Boolean] whether to use {Headers::FatArch64}s to represent each slice
# @return [FatFile] a new FatFile containing the give machos
# @raise [ArgumentError] if less than one Mach-O is given
def self.new_from_machos(*machos)
# @raise [FatArchOffsetOverflowError] if the Mach-Os are too big to be represented
# in a 32-bit {Headers::FatArch} and `fat64` is `false`.
def self.new_from_machos(*machos, fat64: false)
raise ArgumentError, "expected at least one Mach-O" if machos.empty?

fa_klass, magic = if fat64
[Headers::FatArch64, Headers::FAT_MAGIC_64]
else
[Headers::FatArch, Headers::FAT_MAGIC]
end

# put the smaller alignments further forwards in fat macho, so that we do less padding
machos = machos.sort_by(&:segment_alignment)

bin = +""

bin << Headers::FatHeader.new(Headers::FAT_MAGIC, machos.size).serialize
offset = Headers::FatHeader.bytesize + (machos.size * Headers::FatArch.bytesize)
bin << Headers::FatHeader.new(magic, machos.size).serialize
offset = Headers::FatHeader.bytesize + (machos.size * fa_klass.bytesize)

macho_pads = {}
macho_bins = {}

machos.each do |macho|
macho_offset = Utils.round(offset, 2**macho.segment_alignment)
macho_offset = Utils.round(offset, 2**macho.segment_alignment)

if !fat64 && macho_offset > (2**32 - 1)
raise FatArchOffsetOverflowError, macho_offset
end

macho_pads[macho] = Utils.padding_for(offset, 2**macho.segment_alignment)
macho_bins[macho] = macho.serialize

bin << Headers::FatArch.new(macho.header.cputype, macho.header.cpusubtype,
macho_offset, macho_bins[macho].bytesize,
macho.segment_alignment).serialize
bin << fa_klass.new(macho.header.cputype, macho.header.cpusubtype,
macho_offset, macho.serialize.bytesize,
macho.segment_alignment).serialize

offset += (macho_bins[macho].bytesize + macho_pads[macho])
offset += (macho.serialize.bytesize + macho_pads[macho])
end

machos.each do |macho|
bin << Utils.nullpad(macho_pads[macho])
bin << macho_bins[macho]
bin << macho.serialize
end

new_from_bin(bin)
Expand Down Expand Up @@ -327,10 +339,12 @@ def populate_fat_header
def populate_fat_archs
archs = []

fa_off = Headers::FatHeader.bytesize
fa_len = Headers::FatArch.bytesize
fa_klass = Utils.fat_magic32?(header.magic) ? Headers::FatArch : Headers::FatArch64
fa_off = Headers::FatHeader.bytesize
fa_len = fa_klass.bytesize

header.nfat_arch.times do |i|
archs << Headers::FatArch.new_from_bin(:big, @raw_data[fa_off + (fa_len * i), fa_len])
archs << fa_klass.new_from_bin(:big, @raw_data[fa_off + (fa_len * i), fa_len])
end

archs
Expand Down
60 changes: 54 additions & 6 deletions lib/macho/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ module Headers
FAT_MAGIC = 0xcafebabe

# little-endian fat magic
# this is defined, but should never appear in ruby-macho code because
# fat headers are always big-endian and therefore always unpacked as such.
# @note This is defined for completeness, but should never appear in ruby-macho code,
# since fat headers are always big-endian.
# @api private
FAT_CIGAM = 0xbebafeca

# 64-bit big-endian fat magic
FAT_MAGIC_64 = 0xcafebabf

# 64-bit little-endian fat magic
# @note This is defined for completeness, but should never appear in ruby-macho code,
# since fat headers are always big-endian.
FAT_CIGAM_64 = 0xbfbafeca

# 32-bit big-endian magic
# @api private
MH_MAGIC = 0xfeedface
Expand All @@ -31,6 +39,7 @@ module Headers
# @api private
MH_MAGICS = {
FAT_MAGIC => "FAT_MAGIC",
FAT_MAGIC_64 => "FAT_MAGIC_64",
MH_MAGIC => "MH_MAGIC",
MH_CIGAM => "MH_CIGAM",
MH_MAGIC_64 => "MH_MAGIC_64",
Expand Down Expand Up @@ -486,8 +495,10 @@ def to_h
end
end

# Fat binary header architecture structure. A Fat binary has one or more of
# these, representing one or more internal Mach-O blobs.
# 32-bit fat binary header architecture structure. A 32-bit fat Mach-O has one or more of
# these, indicating one or more internal Mach-O blobs.
# @note "32-bit" indicates the fact that this structure stores 32-bit offsets, not that the
# Mach-Os that it points to necessarily *are* 32-bit.
# @see MachO::Headers::FatHeader
class FatArch < MachOStructure
# @return [Integer] the CPU type of the Mach-O
Expand All @@ -505,10 +516,10 @@ class FatArch < MachOStructure
# @return [Integer] the alignment, as a power of 2
attr_reader :align

# always big-endian
# @note Always big endian.
# @see MachOStructure::FORMAT
# @api private
FORMAT = "N5".freeze
FORMAT = "L>5".freeze

# @see MachOStructure::SIZEOF
# @api private
Expand Down Expand Up @@ -542,6 +553,43 @@ def to_h
end
end

# 64-bit fat binary header architecture structure. A 64-bit fat Mach-O has one or more of
# these, indicating one or more internal Mach-O blobs.
# @note "64-bit" indicates the fact that this structure stores 64-bit offsets, not that the
# Mach-Os that it points to necessarily *are* 64-bit.
# @see MachO::Headers::FatHeader
class FatArch64 < FatArch
# @return [void]
attr_reader :reserved

# @note Always big endian.
# @see MachOStructure::FORMAT
# @api private
FORMAT = "L>2Q>2L>2".freeze

# @see MachOStructure::SIZEOF
# @api private
SIZEOF = 32

# @api private
def initialize(cputype, cpusubtype, offset, size, align, reserved = 0)
super(cputype, cpusubtype, offset, size, align)
@reserved = reserved
end

# @return [String] the serialized fields of the fat arch
def serialize
[cputype, cpusubtype, offset, size, align, reserved].pack(FORMAT)
end

# @return [Hash] a hash representation of this {FatArch64}
def to_h
{
"reserved" => reserved,
}.merge super
end
end

# 32-bit Mach-O file header structure
class MachHeader < MachOStructure
# @return [Integer] the magic number
Expand Down
5 changes: 3 additions & 2 deletions lib/macho/tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ def self.delete_rpath(filename, old_path, options = {})
# Merge multiple Mach-Os into one universal (Fat) binary.
# @param filename [String] the fat binary to create
# @param files [Array<String>] the files to merge
# @param fat64 [Boolean] whether to use {Headers::FatArch64}s to represent each slice
# @return [void]
def self.merge_machos(filename, *files)
def self.merge_machos(filename, *files, fat64: false)
machos = files.map do |file|
macho = MachO.open(file)
case macho
Expand All @@ -101,7 +102,7 @@ def self.merge_machos(filename, *files)
end
end.flatten

fat_macho = MachO::FatFile.new_from_machos(*machos)
fat_macho = MachO::FatFile.new_from_machos(*machos, :fat64 => fat64)
fat_macho.write(filename)
end
end
Expand Down
14 changes: 14 additions & 0 deletions lib/macho/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,23 @@ def self.magic?(num)
# @param num [Integer] the number being checked
# @return [Boolean] whether `num` is a valid Fat magic number
def self.fat_magic?(num)
[Headers::FAT_MAGIC, Headers::FAT_MAGIC_64].include? num
end

# Compares the given number to valid 32-bit Fat magic numbers.
# @param num [Integer] the number being checked
# @return [Boolean] whether `num` is a valid 32-bit fat magic number
def self.fat_magic32?(num)
num == Headers::FAT_MAGIC
end

# Compares the given number to valid 64-bit Fat magic numbers.
# @param num [Integer] the number being checked
# @return [Boolean] whether `num` is a valid 64-bit fat magic number
def self.fat_magic64?(num)
num == Headers::FAT_MAGIC_64
end

# Compares the given number to valid 32-bit Mach-O magic numbers.
# @param num [Integer] the number being checked
# @return [Boolean] whether `num` is a valid 32-bit magic number
Expand Down