machobot/dylib.py
# dylib.py - machobot
# Copyright (c) 2015 Dmitry Rodionov
# https://github.com/rodionovd/machobot
#
# This software may be modified and distributed under the terms
# of the MIT license. See the LICENSE file for details.
import sys
from shutil import copy2
from copy import deepcopy
from collections import namedtuple
from macholib.MachO import MachO
from macholib.ptypes import sizeof
from macholib.mach_o import *
from .common.macho_helpers import modify_macho_file_headers
def insert_load_command(target_path, library_install_name):
""" Inserts a new LC_LOAD_DYLIB load command into the target Mach-O header.
Note: the target file will be overwritten. Consider backing it up first before calling this function.
Returns True if everything is OK. Otherwise rises an exception.
"""
def patchHeader(t):
load_command = generate_dylib_load_command(t, library_install_name)
return insert_load_command_into_header(t, load_command)
return modify_macho_file_headers(target_path, patchHeader)
def macho_dependencies_list(target_path, header_magic=None):
""" Generates a list of libraries the given Mach-O file depends on.
In that list a single library is represented by its "install path": for some
libraries it would be a full file path, and for others it would be a relative
path (sometimes with dyld templates like @executable_path or @rpath in it).
Note: I don't know any reason why would some architectures of a fat Mach-O depend
on certain libraries while others don't, but *it's technically possible*.
So that's why you may want to specify the `header_magic` value for a particular header.
Returns an object with two properties: `weak` and `strong` that hold lists of weak
and strong dependencies respectively.
"""
MachODeprendencies = namedtuple("MachODeprendecies", "weak strong")
# Convert the magic value into macholib representation if needed
if isinstance(header_magic, basestring):
header_magic = _MH_MAGIC_from_string(header_magic)
macho = MachO(target_path)
# Obtain a list of headers for the required magic value (if any)
suggestions = filter(lambda t: t.header.magic == header_magic
or # just add all headers if user didn't specifiy the magic
header_magic == None, macho.headers)
header = None if len(suggestions) <= 0 else suggestions[0]
# filter() above *always* returns a list, so we have to check if it's empty
if header is None:
raise Exception("Unable to find a header for the given MAGIC value in that Mach-O file")
return None
def decodeLoadCommandData(data):
# Also ignore trailing zeros
return data[:data.find(b"\x00")].decode(sys.getfilesystemencoding())
def strongReferencesFromHeader(h):
# List of LC_LOAD_DYLIB commands
list = filter(lambda (lc,cmd,data): lc.cmd == LC_LOAD_DYLIB, h.commands)
# Their contents (aka data) as a file path
return map(lambda (lc,cmd,data): decodeLoadCommandData(data), list)
def weakReferencesFromHeader(h):
list = filter(lambda (lc,cmd,data): lc.cmd == LC_LOAD_WEAK_DYLIB, h.commands)
return map(lambda (lc,cmd,data): decodeLoadCommandData(data), list)
strongRefs = strongReferencesFromHeader(header)
weakRefs = weakReferencesFromHeader(header)
return MachODeprendencies(weak = weakRefs, strong = strongRefs)
def insert_load_command_into_header(header, load_command):
""" Inserts the given load command into the header and adjust its size. """
lc, cmd, path = load_command
header.commands.append((lc, cmd, path))
header.header.ncmds += 1
header.changedHeaderSizeBy(lc.cmdsize)
def generate_dylib_load_command(header, libary_install_name):
""" Generates a LC_LOAD_DYLIB command for the given header and a library install path.
Note: the header must already contain at least one LC_LOAD_DYLIB command (see code comments).
Returns a ready-for-use load_command in terms of macholib.
"""
# One can not simply create instances of `dylib_command` and `load_command` classes,
# because that's just not the way macholib works. If we try then all we'll get is a bunch
# of endian (big/little) issues when these objects are serialized into a file.
# BUT THAT'S PROGRAMMING RIGHT?
# So instead I'll iterate *existing* load commands, find a dyld_command, copy it
# and modify this copy. This existing command is said to be fully initialized.
lc = None
cmd = None
for (command, internal_cmd, data) in header.commands:
if (command.cmd == LC_LOAD_DYLIB) and isinstance(internal_cmd, dylib_command):
lc = deepcopy(command)
cmd = deepcopy(internal_cmd)
break
if not lc or not cmd:
raise Exception("Invalid Mach-O file. I mean, there must be at least one LC_LOAD_DYLIB load command.")
return None
# Well, now we just replace everything with our own stuff
cmd.timestamp = 0
cmd.current_version = cmd.compatibility_version = 0x1000
# Since we store the library's path just after the load command itself, we need to find out it's offset.
base = sizeof(load_command) + sizeof(dylib_command)
# `name` is rather bad name for this property: actually it means a path string offset
cmd.name = base
# Also the whole thing must be aligned by 4 bytes on 32-bit arches and by 8 bytes on 64-bit arches
align = 4 if header.header.magic == MH_MAGIC else 8
aligned_name = libary_install_name + (b'\x00' * (align - (len(libary_install_name) % align)))
# So now we finally can say what size this load_command is
lc.cmdsize = base + len(aligned_name)
return (lc, cmd, aligned_name)
def _MH_MAGIC_from_string(str):
return {
'MH_MAGIC' : MH_MAGIC,
'MH_MAGIC_64': MH_MAGIC_64,
}.get(str, None)