iOS开发进阶二:MACH-O与Symbol

什么是MACH-O?

Mach-O(Mach Object)是macOS、iOS、iPadOS存储程序和库的文件格式。对应系统通过应用二进制接口(application binary interface,缩写为 ABI)来运行该格式的文件。

Mach-O格式用来替代BSD系统的a.out格式。Mach-O文件格式保存了在编译过程和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一文件格式。

程序启动时,系统做了哪些事情?

  1. 调用fork函数,创建一个process
  2. 调用execve或其衍生函数,在该进程上加载,执行我们的Mach-O文件
    当我们调用时execve(程序加载器),内核实际上在执行以下操作:
  3. 将文件加载到内存
  4. 开始分析Mach-O中的mach_header,以确认它是有效的Mach-O文件

要理解MACH-O,那就得清楚它到底是什么格式

使用命令 objdump --macho --private-headers #路径

chenshuangchao@chenshuhaodeMBP ~ % objdump --macho --private-headers /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo

MACHO Header文件格式.png

可以将MACHO理解为:文件配置+二进制代码。MACHO中都是二进制。

Load command中:
cmd类型有:LC_SEGMENT_64LC_DYLD_INFO_ONLYLC_SYMTAB
LC_DYSYMTABLC_LOAD_DYLINKERLC_UUID
LC_VERSION_MIN_IPHONEOSLC_SOURCE_VERSIONLC_MAIN
LC_ENCRYPTION_INFO_64LC_LOAD_DYLIBLC_LOAD_DYLIB
LC_RPATHLC_FUNCTION_STARTSLC_DATA_IN_CODE
LC_CODE_SIGNATURE

Main函数指定.png

LC_MAIN告诉动态链接器,加载可执行文件的入口,这个入口并不一定是main函数,可以指定。也就是可以修改MACHO中LC_MAIN的内容,MACHO本质上就是一个二进制文件,是可读可写的。

查看指定的内容 | grep 'LC_MAIN' -A 3

chenshuangchao@chenshuhaodeMBP ~ % objdump --macho --private- headers /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo | grep 'LC_MAIN' -A 3

MACHO简单读写版本

通过终端查看MACHO,终端打印出来的信息太过冗余,有些信息是重复的,不需要的。逻辑教育Cat大神写了一个程序machoinfo,用这个程序查看MACHO可以更加清晰一点。

如何使用?

方法一:

1.进入machoinfo可以执行文件的目录

2.使用项目中支持的指令./machoinfo #路径

可以通过读出信息的过程,看出MACHO就是一个二进制,里面是配置文件加二进制。能读出来那些数据,说明macho的排列是按一定规则的。


machoinfo读取的结果.png

方式二:

直接在项目中Xcode打印,将ipa的可执行文件路径加入到machoinfo的Scheme -->Run -->Arguments --> Arguments Passed On Launch
启动时,将路径作为参数传递到项目中。

直接在machoinfo中运行.png

编译与链接

编译成可执行文件时,中间要经过一个目标文件,也就是我们经常说的.o文件。编译的过程就是要把代码对应的放到MACHO的配置里面,比如代码是全局符号还是本地符号,是外部符号还是内部符号。根据这些特性,在编译的过程中把这些代码进行分类。

链接是干什么呢?链接的本质就是把多个目标文件组合成一个文件。我们的代码可能会被编译成很多个.o文件,最后会合并成一个可执行文件。在这个过程中,会把多个符号表合并到一起。那么在合并过程中,我们能否再对符号进行处理?比如,写在.h和写在.m文件中的变量,他们的性质不一样,那么我们是否可以在合并的时候,再重新修改它们对外暴露的性质。

初识符号表

Symbol Table:就是用来保存符号。

String Table:就是用来保存符号的名称。

Indirect Symbol Table:间接符号表。保存使用的外部符号。更准确一点就是使 用的外部动态库的符号。是Symbol Table的子集。

例如:我们在程序中使用的NSLog,是属于Foundation动态库中的符号,这个符号就会放到间接符号表里面去。

如何实现Xcode在编译的时候,将命令显示到终端上?

在Xcode中执行脚本的地方,写一个命令。


第一步在脚本中写打印语句echo.png

/dev/ttys000就是终端路径


第二步找到终端标识.png

脚本语句中添加重定向到终端


第三步添加重定向到终端.png
第四步运行程序即可在终端中看到执行的指令.png

如何实现在Xcode中执行我们想要的命令?

根据上一篇文章,我们知道可以在XCConfig中实现,而且在XCConfig中定义的变量,可以通过info.plist实现在程序中使用。同样,在XCConfig中定义的变量,也可以在Xcode内置的脚本文件中使用。

第一步在XCConfig中设置一个变量.png

第二步执行时会将HOST_URL变量打印到终端.png

下面是一个Shell脚本,一共是定义3个函数,RunCommandEchoErrorRunCMDToTTY,在脚本最后一行,调用RunCMDToTTY函数,在RunCMDToTTY函数中使用了RunCommandEchoError函数。

RunCMDToTTY函数中使用了3个参数,$TTY$CMD${CMD_FLAG}

CMD = 运行到命令

CMD_FLAG = 运行到命令参数

TTY = 终端

RunCommand() {
  #判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空
  #[[是 bash 程序语言的关键字。用于判断
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
    #作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
      if [[ -n "$TTY" ]]; then
          echo "♦ $@" 1>$TTY
      else
          echo "♦ $*"
      fi
      echo "------------------------------------------------------------------------------" 1>$TTY
  fi
  #与$*相同。但是使用时加引号,并在引号中返回每个参数。"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数
  if [[ -n "$TTY" ]]; then
      echo `$@ &>$TTY`
  else
      "$@"
  fi
  #显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
  return $?
}

EchoError() {
    #在shell脚本中,默认情况下,总是有三个文件处于打开状态,标准输入(键盘输入)、标准输出(输出到屏幕)、标准错误(也是输出到屏幕),它们分别对应的文件描述符是0,1,2
    # >  默认为标准输出重定向,与 1> 相同
    # 2>&1  意思是把 标准错误输出 重定向到 标准输出.
    # &>file  意思是把标准输出 和 标准错误输出 都重定向到文件file中
    # 1>&2 将标准输出重定向到标准错误输出。实际上就是打印所有参数已标准错误格式
    if [[ -n "$TTY" ]]; then
        echo "$@" 1>&2>$TTY
    else
        echo "$@" 1>&2
    fi
    
}

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
}


RunCMDToTTY

Xcode内置的脚本,可以执行shell,在Shell中可以使用XCConfig中的变量,那么我们只需要在XCConfig中将脚本需要的参数传递过去就可以了。

Shell中的CMD,CMD_FLAG,TTY,是我们定义的Shell变量。在Xcode中也定义了一些自带的shell变量,比如说

$SRCROOT代表的是当前代码的路径。

$BUILD_DIR代表的是当前编译的路径。

如何不知道路径究竟代表什么,可以在xcode内置脚本中用echo命令。
echo "$BUILD_DIR" > /dev/ttys000

在XCConfig中设置参数

CMD = nm

// -pa 可执行文件路径
CMD_FLAG = -pa /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo

TTY = /dev/ttys000

到此时,脚本已经准备完毕,在终端上执行 nm -pa /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo

执行后在终端输出了很多东西。说明我们的配置起效了。

操作符号的命令

指令的具体意义
nm -pa /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo

-p: 不排序
-a: 显示所有符号,包含调试符号

这个路径太长了,而且文件是随便找的,如果想要使用当前项目生成的可执行文件要怎么做?再新建一个变量。MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)/$(EFFECTIVE_PLATFORM_NAME)/*

根据Xcode自带变量自动调试当前项目可执行文件.png

Xcode内置的变量,会根据Project的位置自动改变。到这里,可以说是万事具备,可以开始研究了。

Xcode在哪一步执行脚本

脚本执行的时间顺序.png

Strip指令脱去无用符号

Strip脱去符号是在脚本执行之前还是之后?


strip在build_setting中的位置.png

平时我们项目中Strip命令很少起作用,因为xcode默认设置,只有打包的时候才起作用。如果要在开发的时候也能脱去符号,可以将Deployment Postprocessing设置为Yes后,Strip Style设置为Non-Global Symbols或者Debugging Symbols就起作用了。

是否可以更自由的控制符号剥离操作呢?链接器是将多个目标文件合在一起,那在做融合动作的时候,就可以对目标文件的符号做一些操作。

man ld查看链接器参数

command + K清除终端

man ld查看链接器支持的参数

/代表查找

/-S自动跳转到-S指令的位置。

-S 的含义 Do not put debug information (STABS or DWARF) in the output file.脱去调试符号。

在XCConfig中配置 OTHER_LDFLAGS = -Xlinker -S即可脱去调试符号。

Strip执行的顺序在脚本执行之后.png

符号相关知识点

  1. -O1 -Oz 生成目标文件,编译时候优化
  2. dead code strip 死代码剥离,链接时候优化
  3. strip 剥离符号,已经生成Mach-O文件后修改

后两步主要是处理符号

strip指令,什么都不加,就是剥离所有的符号。

你可能感兴趣的:(iOS开发进阶二:MACH-O与Symbol)