Class: MachO::MachOFile

Inherits:
Object
  • Object
show all
Defined in:
lib/macho/macho_file.rb

Overview

Represents a Mach-O file, which contains a header and load commands as well as binary executable instructions. Mach-O binaries are architecture specific.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename) ⇒ MachOFile

Creates a new FatFile from the given filename.

Parameters:

  • filename (String)

    the Mach-O file to load from

Raises:

  • (ArgumentError)

    if the given file does not exist


35
36
37
38
39
40
41
# File 'lib/macho/macho_file.rb', line 35

def initialize(filename)
  raise ArgumentError.new("#{filename}: no such file") unless File.file?(filename)

  @filename = filename
  @raw_data = File.open(@filename, "rb") { |f| f.read }
  populate_fields
end

Instance Attribute Details

#endiannessSymbol (readonly)

Returns the endianness of the file, :big or :little.

Returns:

  • (Symbol)

    the endianness of the file, :big or :little


12
13
14
# File 'lib/macho/macho_file.rb', line 12

def endianness
  @endianness
end

#filenameString

Returns the filename loaded from, or nil if loaded from a binary string.

Returns:

  • (String)

    the filename loaded from, or nil if loaded from a binary string


9
10
11
# File 'lib/macho/macho_file.rb', line 9

def filename
  @filename
end

#headerMachO::MachHeader, MachO::MachHeader64 (readonly)

Returns:


16
17
18
# File 'lib/macho/macho_file.rb', line 16

def header
  @header
end

#load_commandsArray<MachO::LoadCommand> (readonly)

Note:

load commands are provided in order of ascending offset.

Returns an array of the file's load commands.

Returns:


20
21
22
# File 'lib/macho/macho_file.rb', line 20

def load_commands
  @load_commands
end

Class Method Details

.new_from_bin(bin) ⇒ MachO::MachOFile

Creates a new MachOFile instance from a binary string.

Parameters:

  • bin (String)

    a binary string containing raw Mach-O data

Returns:


25
26
27
28
29
30
# File 'lib/macho/macho_file.rb', line 25

def self.new_from_bin(bin)
  instance = allocate
  instance.initialize_from_bin(bin)

  instance
end

Instance Method Details

#add_command(lc, options = {}) ⇒ void

Note:

This is public, but methods like #add_rpath should be preferred. Setting repopulate to false will leave the instance in an inconsistent state unless #populate_fields is called immediately afterwards.

This method returns an undefined value.

Appends a new load command to the Mach-O.

Parameters:

  • lc (MachO::LoadCommand)

    the load command being added

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :repopulate (Boolean) — default: true

    whether or not to repopulate the instance fields

See Also:

  • MachO::MachOFile.{{#insert_command}

239
240
241
# File 'lib/macho/macho_file.rb', line 239

def add_command(lc, options = {})
  insert_command(header.class.bytesize + sizeofcmds, lc, options)
end

#add_rpath(path) ⇒ void

This method returns an undefined value.

Add the given runtime path to the Mach-O.

Examples:

file.rpaths # => ["/lib"]
file.add_rpath("/usr/lib")
file.rpaths # => ["/lib", "/usr/lib"]

Parameters:

  • path (String)

    the new runtime path

Raises:


387
388
389
390
391
392
# File 'lib/macho/macho_file.rb', line 387

def add_rpath(path)
  raise RpathExistsError.new(path) if rpaths.include?(path)

  rpath_cmd = LoadCommand.create(:LC_RPATH, path)
  add_command(rpath_cmd)
end

#alignmentFixnum

Returns the file's internal alignment.

Returns:

  • (Fixnum)

    the file's internal alignment


69
70
71
# File 'lib/macho/macho_file.rb', line 69

def alignment
  magic32? ? 4 : 8
end

#bundle?Boolean

Returns true if the file is of type MH_BUNDLE, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_BUNDLE, false otherwise


109
110
111
# File 'lib/macho/macho_file.rb', line 109

def bundle?
  header.filetype == MH_BUNDLE
end

#change_install_name(old_name, new_name) ⇒ void Also known as: change_dylib

This method returns an undefined value.

Changes the shared library old_name to new_name

Examples:

file.change_install_name("/usr/lib/libWhatever.dylib", "/usr/local/lib/libWhatever2.dylib")

Parameters:

  • old_name (String)

    the shared library's old name

  • new_name (String)

    the shared library's new name

Raises:


342
343
344
345
346
347
348
349
350
# File 'lib/macho/macho_file.rb', line 342

def change_install_name(old_name, new_name)
  old_lc = dylib_load_commands.find { |d| d.name.to_s == old_name }
  raise DylibUnknownError.new(old_name) if old_lc.nil?

  new_lc = LoadCommand.create(old_lc.type, new_name,
    old_lc.timestamp, old_lc.current_version, old_lc.compatibility_version)

  replace_command(old_lc, new_lc)
end

#change_rpath(old_path, new_path) ⇒ void

This method returns an undefined value.

Changes the runtime path old_path to new_path

Examples:

file.change_rpath("/usr/lib", "/usr/local/lib")

Parameters:

  • old_path (String)

    the old runtime path

  • new_path (String)

    the new runtime path

Raises:


368
369
370
371
372
373
374
375
376
377
# File 'lib/macho/macho_file.rb', line 368

def change_rpath(old_path, new_path)
  old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
  raise RpathUnknownError.new(old_path) if old_lc.nil?
  raise RpathExistsError.new(new_path) if rpaths.include?(new_path)

  new_lc = LoadCommand.create(:LC_RPATH, new_path)

  delete_rpath(old_path)
  insert_command(old_lc.view.offset, new_lc)
end

#command(name) ⇒ Array<MachO::LoadCommand> Also known as: []

All load commands of a given name.

Examples:

file.command("LC_LOAD_DYLIB")
file[:LC_LOAD_DYLIB]

Parameters:

  • name (String, Symbol)

    the load command ID

Returns:


169
170
171
# File 'lib/macho/macho_file.rb', line 169

def command(name)
  load_commands.select { |lc| lc.type == name.to_sym }
end

#core?Boolean

Returns true if the file is of type MH_CORE, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_CORE, false otherwise


89
90
91
# File 'lib/macho/macho_file.rb', line 89

def core?
  header.filetype == MH_CORE
end

#cpusubtypeSymbol

Returns a symbol representation of the Mach-O's CPU subtype.

Returns:

  • (Symbol)

    a symbol representation of the Mach-O's CPU subtype


144
145
146
# File 'lib/macho/macho_file.rb', line 144

def cpusubtype
  CPU_SUBTYPES[header.cputype][header.cpusubtype]
end

#cputypeSymbol

Returns a symbol representation of the Mach-O's CPU type.

Returns:

  • (Symbol)

    a symbol representation of the Mach-O's CPU type


139
140
141
# File 'lib/macho/macho_file.rb', line 139

def cputype
  CPU_TYPES[header.cputype]
end

#delete_command(lc, options = {}) ⇒ void

Note:

This is public, but methods like #delete_rpath should be preferred. Setting repopulate to false will leave the instance in an inconsistent state unless #populate_fields is called immediately afterwards.

This method returns an undefined value.

Delete a load command from the Mach-O.

Parameters:

  • lc (MachO::LoadCommand)

    the load command being deleted

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :repopulate (Boolean) — default: true

    whether or not to repopulate the instance fields


253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/macho/macho_file.rb', line 253

def delete_command(lc, options = {})
  @raw_data.slice!(lc.view.offset, lc.cmdsize)

  # update Mach-O header fields to account for deleted load command
  set_ncmds(ncmds - 1)
  set_sizeofcmds(sizeofcmds - lc.cmdsize)

  # pad the space after the load commands to preserve offsets
  null_pad = "\x00" * lc.cmdsize
  @raw_data.insert(header.class.bytesize + sizeofcmds - lc.cmdsize, null_pad)

  populate_fields if options.fetch(:repopulate, true)
end

#delete_rpath(path) ⇒ Object

Delete the given runtime path from the Mach-O.

Examples:

file.rpaths # => ["/lib"]
file.delete_rpath("/lib")
file.rpaths # => []

Parameters:

  • path (String)

    the runtime path to delete

Returns:

  • void

Raises:


402
403
404
405
406
407
408
409
410
411
# File 'lib/macho/macho_file.rb', line 402

def delete_rpath(path)
  rpath_cmds = command(:LC_RPATH).select { |r| r.path.to_s == path }
  raise RpathUnknownError.new(path) if rpath_cmds.empty?

  # delete the commands in reverse order, offset descending. this
  # allows us to defer (expensive) field population until the very end
  rpath_cmds.reverse_each { |cmd| delete_command(cmd, repopulate: false) }

  populate_fields
end

#dsym?Boolean

Returns true if the file is of type MH_DSYM, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_DSYM, false otherwise


114
115
116
# File 'lib/macho/macho_file.rb', line 114

def dsym?
  header.filetype == MH_DSYM
end

#dylib?Boolean

Returns true if the file is of type MH_DYLIB, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_DYLIB, false otherwise


99
100
101
# File 'lib/macho/macho_file.rb', line 99

def dylib?
  header.filetype == MH_DYLIB
end

#dylib_idString?

The Mach-O's dylib ID, or nil if not a dylib.

Examples:

file.dylib_id # => 'libBar.dylib'

Returns:

  • (String, nil)

    the Mach-O's dylib ID


298
299
300
301
302
303
304
# File 'lib/macho/macho_file.rb', line 298

def dylib_id
  return unless dylib?

  dylib_id_cmd = command(:LC_ID_DYLIB).first

  dylib_id_cmd.name.to_s
end

#dylib_id=(new_id) ⇒ void

This method returns an undefined value.

Changes the Mach-O's dylib ID to new_id. Does nothing if not a dylib.

Examples:

file.dylib_id = "libFoo.dylib"

Parameters:

  • new_id (String)

    the dylib's new ID

Raises:

  • (ArgumentError)

    if new_id is not a String


312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/macho/macho_file.rb', line 312

def dylib_id=(new_id)
  raise ArgumentError.new("new ID must be a String") unless new_id.is_a?(String)
  return unless dylib?

  old_lc = command(:LC_ID_DYLIB).first
  raise DylibIdMissingError.new unless old_lc

  new_lc = LoadCommand.create(:LC_ID_DYLIB, new_id, old_lc.timestamp,
    old_lc.current_version, old_lc.compatibility_version)

  replace_command(old_lc, new_lc)
end

#dylib_load_commandsArray<MachO::DylibCommand>

All load commands responsible for loading dylibs.

Returns:


279
280
281
# File 'lib/macho/macho_file.rb', line 279

def dylib_load_commands
  load_commands.select { |lc| DYLIB_LOAD_COMMANDS.include?(lc.type) }
end

#dylinker?Boolean

Returns true if the file is of type MH_DYLINKER, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_DYLINKER, false otherwise


104
105
106
# File 'lib/macho/macho_file.rb', line 104

def dylinker?
  header.filetype == MH_DYLINKER
end

#executable?Boolean

Returns true if the file is of type MH_EXECUTE, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_EXECUTE, false otherwise


79
80
81
# File 'lib/macho/macho_file.rb', line 79

def executable?
  header.filetype == MH_EXECUTE
end

#filetypeSymbol

Returns a string representation of the Mach-O's filetype.

Returns:

  • (Symbol)

    a string representation of the Mach-O's filetype


134
135
136
# File 'lib/macho/macho_file.rb', line 134

def filetype
  MH_FILETYPES[header.filetype]
end

#flagsFixnum

Returns execution flags set by the linker.

Returns:

  • (Fixnum)

    execution flags set by the linker


159
160
161
# File 'lib/macho/macho_file.rb', line 159

def flags
  header.flags
end

#fvmlib?Boolean

Returns true if the file is of type MH_FVMLIB, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_FVMLIB, false otherwise


84
85
86
# File 'lib/macho/macho_file.rb', line 84

def fvmlib?
  header.filetype == MH_FVMLIB
end

#initialize_from_bin(bin) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initializes a new MachOFile instance from a binary string.

See Also:


46
47
48
49
50
# File 'lib/macho/macho_file.rb', line 46

def initialize_from_bin(bin)
  @filename = nil
  @raw_data = bin
  populate_fields
end

#insert_command(offset, lc, options = {}) ⇒ Object

Note:

Calling this method with an arbitrary offset in the load command region will leave the object in an inconsistent state.

Inserts a load command at the given offset.

Parameters:

  • offset (Fixnum)

    the offset to insert at

  • lc (MachO::LoadCommand)

    the load command to insert

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :repopulate (Boolean) — default: true

    whether or not to repopulate the instance fields

Raises:


185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/macho/macho_file.rb', line 185

def insert_command(offset, lc, options = {})
  context = LoadCommand::SerializationContext.context_for(self)
  cmd_raw = lc.serialize(context)

  if offset < header.class.bytesize || offset + cmd_raw.bytesize > low_fileoff
    raise OffsetInsertionError.new(offset)
  end

  new_sizeofcmds = sizeofcmds + cmd_raw.bytesize

  if header.class.bytesize + new_sizeofcmds > low_fileoff
    raise HeaderPadError.new(@filename)
  end

  # update Mach-O header fields to account for inserted load command
  set_ncmds(ncmds + 1)
  set_sizeofcmds(new_sizeofcmds)

  @raw_data.insert(offset, cmd_raw)
  @raw_data.slice!(header.class.bytesize + new_sizeofcmds, cmd_raw.bytesize)

  populate_fields if options.fetch(:repopulate, true)
end

#kext?Boolean

Returns true if the file is of type MH_KEXT_BUNDLE, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_KEXT_BUNDLE, false otherwise


119
120
121
# File 'lib/macho/macho_file.rb', line 119

def kext?
  header.filetype == MH_KEXT_BUNDLE
end

#linked_dylibsArray<String>

All shared libraries linked to the Mach-O.

Returns:

  • (Array<String>)

    an array of all shared libraries


327
328
329
330
331
332
333
# File 'lib/macho/macho_file.rb', line 327

def linked_dylibs
  # Some linkers produce multiple `LC_LOAD_DYLIB` load commands for the same
  # library, but at this point we're really only interested in a list of
  # unique libraries this Mach-O file links to, thus: `uniq`. (This is also
  # for consistency with `FatFile` that merges this list across all archs.)
  dylib_load_commands.map(&:name).map(&:to_s).uniq
end

#magicFixnum

Returns the file's magic number.

Returns:

  • (Fixnum)

    the file's magic number


124
125
126
# File 'lib/macho/macho_file.rb', line 124

def magic
  header.magic
end

#magic32?Boolean

Returns true if the Mach-O has 32-bit magic, false otherwise.

Returns:

  • (Boolean)

    true if the Mach-O has 32-bit magic, false otherwise


59
60
61
# File 'lib/macho/macho_file.rb', line 59

def magic32?
  Utils.magic32?(header.magic)
end

#magic64?Boolean

Returns true if the Mach-O has 64-bit magic, false otherwise.

Returns:

  • (Boolean)

    true if the Mach-O has 64-bit magic, false otherwise


64
65
66
# File 'lib/macho/macho_file.rb', line 64

def magic64?
  Utils.magic64?(header.magic)
end

#magic_stringString

Returns a string representation of the file's magic number.

Returns:

  • (String)

    a string representation of the file's magic number


129
130
131
# File 'lib/macho/macho_file.rb', line 129

def magic_string
  MH_MAGICS[magic]
end

#ncmdsFixnum

Returns the number of load commands in the Mach-O's header.

Returns:

  • (Fixnum)

    the number of load commands in the Mach-O's header


149
150
151
# File 'lib/macho/macho_file.rb', line 149

def ncmds
  header.ncmds
end

#object?Boolean

Returns true if the file is of type MH_OBJECT, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_OBJECT, false otherwise


74
75
76
# File 'lib/macho/macho_file.rb', line 74

def object?
  header.filetype == MH_OBJECT
end

#populate_fieldsvoid

Note:

This method is public, but should (almost) never need to be called. The exception to this rule is when methods like #add_command and #delete_command have been called with repopulate = false.

This method returns an undefined value.

Populate the instance's fields with the raw Mach-O data.


272
273
274
275
# File 'lib/macho/macho_file.rb', line 272

def populate_fields
  @header = get_mach_header
  @load_commands = get_load_commands
end

#preload?Boolean

Returns true if the file is of type MH_PRELOAD, false otherwise.

Returns:

  • (Boolean)

    true if the file is of type MH_PRELOAD, false otherwise


94
95
96
# File 'lib/macho/macho_file.rb', line 94

def preload?
  header.filetype == MH_PRELOAD
end

#replace_command(old_lc, new_lc) ⇒ void

Note:

This is public, but methods like #dylib_id= should be preferred.

This method returns an undefined value.

Replace a load command with another command in the Mach-O, preserving location.

Parameters:

Raises:

See Also:

  • MachO::MachOFile.{{#insert_command}

216
217
218
219
220
221
222
223
224
225
226
# File 'lib/macho/macho_file.rb', line 216

def replace_command(old_lc, new_lc)
  context = LoadCommand::SerializationContext.context_for(self)
  cmd_raw = new_lc.serialize(context)
  new_sizeofcmds = sizeofcmds + cmd_raw.bytesize - old_lc.cmdsize
  if header.class.bytesize + new_sizeofcmds > low_fileoff
    raise HeaderPadError.new(@filename)
  end

  delete_command(old_lc)
  insert_command(old_lc.view.offset, new_lc)
end

#rpathsArray<String>

All runtime paths searched by the dynamic linker for the Mach-O.

Returns:

  • (Array<String>)

    an array of all runtime paths


356
357
358
# File 'lib/macho/macho_file.rb', line 356

def rpaths
  command(:LC_RPATH).map(&:path).map(&:to_s)
end

#sections(segment) ⇒ Array<MachO::Section>, Array<MachO::Section64>

All sections of the segment segment.

Parameters:

Returns:


417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/macho/macho_file.rb', line 417

def sections(segment)
  section_klass = case segment
  when MachO::SegmentCommand
    MachO::Section
  when MachO::SegmentCommand64
    MachO::Section64
  else
    raise ArgumentError.new("not a valid segment")
  end

  sections = []
  return sections if segment.nsects.zero?

  offset = segment.view.offset + segment.class.bytesize

  segment.nsects.times do
    section_bin = @raw_data[offset, section_klass.bytesize]
    sections << section_klass.new_from_bin(endianness, section_bin)
    offset += section_klass.bytesize
  end

  sections
end

#segmentsArray<MachO::SegmentCommand>, Array<MachO::SegmentCommand64>

All segment load commands in the Mach-O.

Returns:


286
287
288
289
290
291
292
# File 'lib/macho/macho_file.rb', line 286

def segments
  if magic32?
    command(:LC_SEGMENT)
  else
    command(:LC_SEGMENT_64)
  end
end

#serializeString

The file's raw Mach-O data.

Returns:

  • (String)

    the raw Mach-O data


54
55
56
# File 'lib/macho/macho_file.rb', line 54

def serialize
  @raw_data
end

#sizeofcmdsFixnum

Returns the size of all load commands, in bytes.

Returns:

  • (Fixnum)

    the size of all load commands, in bytes


154
155
156
# File 'lib/macho/macho_file.rb', line 154

def sizeofcmds
  header.sizeofcmds
end

#write(filename) ⇒ void

This method returns an undefined value.

Write all Mach-O data to the given filename.

Parameters:

  • filename (String)

    the file to write to


444
445
446
# File 'lib/macho/macho_file.rb', line 444

def write(filename)
  File.open(filename, "wb") { |f| f.write(@raw_data) }
end

#write!void

Note:

Overwrites all data in the file!

This method returns an undefined value.

Write all Mach-O data to the file used to initialize the instance.

Raises:


452
453
454
455
456
457
458
# File 'lib/macho/macho_file.rb', line 452

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