Mach-O简单分析

2,435 阅读3分钟

主要内容:

  1. Mach-O的读写特性
  2. 查看命令文档
  3. 查看Mach-O
  4. 查看Symbol
  5. 配置Mach-O终端调试工具

一、Mach-O的读写特性

Mach-O文件其实等价于: 文件配置 + 二进制代码

Mach-O文件是可读可写的,分析Mach-O文件的结构,可以解释程序运行中的许多问题;

MachO-简化结构.png

1. Mach-O可读
  1. Header:提供读取所需的身份信息,包括CPU类型文件类型等;
  2. Load Commands:提供读取所需的文件的信息,以Load Command _TEXT为例,其中记录有
    1. _TEXT代码段的起始位置;
    2. _TEXT代码段的大小;
  3. __TEXT,__DATA,符号表等,这些都是代码编译后的文件、数据存放的位置;
2.Mach-O可写
  1. Mach-O能被执行是因为有签名,当我们修改了Mach-O文件,需要重新签名才能被苹果系统所接受;
  2. 破解软件都需要重新签名,正是这种原因;

二、查看命令文档

有时候,需要在终端查询一些命令的具体用法,有两种方式(以查询nm命令为例):

man nm      //推荐使用
nm --help   

当终端里显示nm的使用文档后,还可快速定位查询,以查询"-a"参数为例:

  • /-a:快速匹配文档中-a的位置;
  • n:向下定位-a位置;
  • N:向上定位-a位置;
  • q:退出文档查看;

三、查看Mach-O

1.常用的查看命令
// 查看Header(${MACH_PATH}表示Mach-O文件位置)
objdump --macho --private-header ${MACH_PATH}

// 查看Header 和 Load commands
objdump --macho --private-headers ${MACH_PATH}

// 查看__TEXT
objdump --macho -d ${MACH_PATH}

// 查看符号表
objdump --macho --syms ${MACH_PATH}

// 查看导出符号
objdump --macho --exports-trie ${MACH_PATH}

// 查看间接符号表
objdump --macho --indirect-symbols ${MACH_PATH}
2.测试查看命令

为了避免过多文件的干扰,创建测试工程时,选择选择MacOS ->Command Line Tool

进入工程后,在main.m文件中写入代码如下:

#import <Foundation/Foundation.h>

int global_init_value = 10;
static int static_init_value = 9;

int main(int argc, const char * argv[]) {
    NSLog(@"%d", static_init_value);
	return 0;
}

Xcode运行成功后,Products下的黑色文件即Mach-O可执行文件,如下图:

MachO-可执行文件位置.png

切换到可执行文件的目录下,使用objdump命令,查看Header:

objdump --macho --private-header MachOTest

终端的打印结果如下:

objdump-查看Header.png

使用objdump命令,查看Header以及Load commands

objdump --macho --private-headers MachOTest

终端的打印结果如下:

objdump-查看Header&Commands.png

如上,我们在Mach-O文件的Load Commands中,可以找到程序入口函数以及依赖系统动态库的相关信息;

四、查看Symbol

查看符号,除了可以使用objdump命令以外,还可以使用nm命令;

1.nm命令简介
  1. nmname的缩写,它显示指定文件中的符号信息,文件可以是对象文件、可执行文件或对象文件库;
  2. 如果文件中没有包含符号信息,nm报告该情况,但不把他解释为出错;
  3. nm缺省情况下报告十进制符号表示法下的数字值;

可以在终端使用man nm命令查看其具体用法:

 % man nm
 nm - display name list (symbol table)

即,nm是一个可以查看展示符号表信息的命令。

2.常用命令参数
 -a //显示符号表所有内容
 -p //不排序,显示符号本来的顺序
 -g //显示全局符号
 -r //逆转排序
 -u //显示未定义符号
 -m //显示N_SECT类型的符号(Mach-O符号)显示
3.使用举例

使用nm命令,查看MachO可执行文件中的符号:

nm -pa ${MACH_PATH}

终端的打印结果如下:

nm-查看符号.png

其中,第二列符号标签的含义如下:

nm-符号类型.png

注:标记Type,⼩写代表本地符号(local symbol);

五、配置Mach-O终端测试工具

如上的调试过程十分繁琐,每次都需要等到程序运行完成,然后再切换到终端测试命令。为了更方便分析Mach-O,这里可以对工程进一步配置,使其运行时可直接将结果打印在终端里,具体的步骤如下:

1.测试将Xcode打印重定向到终端

1.新建一个终端,使用tty命令获取其标识位置:

TTYDemo-获取终端位置.png

2.在Xcode中依次打开: Build Phases ->Run Script,输入脚本命令使其能够在运行时打印:

TTYDemo-配置RunScript.png

3.运行工程,可以看到终端的打印如下

TTYDemo-测试RunScript.png

2.测试Xcode脚本命令执行项目中的代码

虽然成功打印信息到终端,但是"Hello world"这段信息却是固定的,为了动态获取执行参数,我们可以自定义配置文件,并在其中定义如下的参数:

DEBUG_URL = www.debug.com

然后,修改Run Script

echo "Hello World" > /dev/ttys000
echo ${DEBUG_URL} > /dev/ttys000

终端打印结果:

Hello World
www.debug.com

这里就说明,Xcode脚本执行与配置文件是在同一个环境下,我们可以做到将配置文件中的各种参数传递给脚本中使用。

3. 实现通过脚本命令打印符号信息

在具备以上条件后,可以实现通过脚本命令打印符号信息到终端,具体的做法如下:

将执行命令的操作定义在一个脚本文件中,并起名xcode_run_cmd.sh,其中的关键代码如下:

RunCMDToTTY() {
    if [[ ! -e "$TTY" ]]; then
        EchoError "======================="
        EchoError "ERROR: Not Config tty to output."
        exit -1
    fi
    
    if [[ -n "$CMD" ]]; then
        RunCommand "$CMD" ${CMD_FLAG}
    else
        EchoError "========================"
        EchoError "ERROR:Failed to run CMD. THE CMD must not null"
    fi
}

EchoError() {
    if [[ -n "$TTY" ]]; then
        echo "$@" 1>&2>$TTY
    else
        echo "$@" 1>&2
    fi
}

RunCommand() {
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
      if [[ -n "$TTY" ]]; then
          echo "♦ $@" 1>$TTY
      else
          echo "♦ $*"
      fi
      echo "-----------------------------------------" 1>$TTY
  fi

  if [[ -n "$TTY" ]]; then
      echo `$@ &>$TTY`
  else
      "$@"
  fi
  return $?
}

RunCMDToTTY

然后,在配置文件里增加三个参数:CMDCMD_FLAGTTY,即需要在.xcconfig中进行定义:

// Config-TestProject.debug.xcconfig
 
// DEBUG_URL = www.debug.com

// MACHO_PATH:Mach-O文件路径,即可执行文件路径,这里用到了环境变量来表示具体路径
// TTY = 终端位置标识
// CMD:运行命令
// CMD_FLAG = 命令参数

MACHO_PATH = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}.app/${PRODUCT_NAME}
TTY = /dev/ttys000
CMD = nm
CMD_FLAG = -pa ${MACHO_PATH}

配置完成后的工程如下:

TTYDemo-配置文件.png

最后,我们需要更改Run Script指令,使其执行脚本文件:

TTYDemo-配置脚本Script.png

此时,再次运行项目,就可以直接在控制台看到使用nm命令分析得到的Mach-O信息了,而且我们可以更换CMDCMD_FLAG参数,用以调试更多的其他命令。

参考链接

  1. 文中演示使用的Demo仓库