1. Mach-O与链接器

Mach-O(Mach Object) 是MacOS,iOS,iPadOS存储 程序和库的文件格式 eg:动态库,静态库。

对应系统通过应用 二进制接口(application binary interface,缩写为ABI) 来运行该格式的的文件。

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

程序编译流程:
image.png
链接 的本质就是 把多个目标文件组合成一个文件

链接器 主要是 配置一些参数,一些符号参数 ,eg:怎么把导出符号隐藏了,通过链接器配置符号的可见性

可执行文件的调用过程

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

2. 查看Macho文件

2.1 Macho 文件

截屏2021-01-18 下午8.12.13.png
其中Macho Header的结构
在64位系统下Macho Header的结构:

  1. /*
  2. * The 64-bit mach header appears at the very beginning of object files for
  3. * 64-bit architectures.
  4. */
  5. struct mach_header_64 {
  6. uint32_t magic; /* mach magic number identifier */
  7. cpu_type_t cputype; /* cpu specifier */
  8. cpu_subtype_t cpusubtype; /* machine specifier */
  9. uint32_t filetype; /* type of file */
  10. uint32_t ncmds; /* number of load commands */
  11. uint32_t sizeofcmds; /* the size of all the load commands */
  12. uint32_t flags; /* flags */
  13. uint32_t reserved; /* reserved */
  14. };

32位系统下Macho Header的结构:

  1. /*
  2. * The 32-bit mach header appears at the very beginning of the object file for
  3. * 32-bit architectures.
  4. */
  5. struct mach_header {
  6. uint32_t magic; /* mach magic number identifier */
  7. cpu_type_t cputype; /* cpu specifier */
  8. cpu_subtype_t cpusubtype; /* machine specifier */
  9. uint32_t filetype; /* type of file */
  10. uint32_t ncmds; /* number of load commands */
  11. uint32_t sizeofcmds; /* the size of all the load commands */
  12. uint32_t flags; /* flags */
  13. };

magic的取值
image.png

2.2 查看Macho headers

  1. objdump --macho --private-headers /Users/mac/Library/Developer/Xcode/DerivedData/LoginApp-eqeqbztqkuqnligyfeahplzeimtv/Build/Products/Debug-iphoneos/LoginApp.app/LoginApp

image.png
在Mach-O 中定义了程序的入口main函数(定义在LC_MAIN的Load command命令),告诉动态链接器去加载程序的入口

  1. objdump --macho --private-headers /Users/mac/Library/Developer/Xcode/DerivedData/LoginApp-eqeqbztqkuqnligyfeahplzeimtv/Build/Products/Debug-iphoneos/LoginApp.app/LoginApp | ag 'LC_MAIN' -A 3

image.png

如果遇到如下报错 Mach-O与链接器 - 图8 请先安装 the_silver_searcher: 终端输入 brew install the_silver_searcher

2.3 解析可执行文件的Macho结构的命令

源码请移步 链接: https://pan.baidu.com/s/1SAqnepcUIvwdYr3WtSZrKQ 提取码: 3t7e
拷贝machinfo到常用目录 /Users/mac/Desktop,终端 cd /Users/mac/Desktop
执行命令

  1. ./machoinfo '可执行文件路径’

image.png

也可以在machoinfo的工程中编辑Scheme,添加Run的Arguments,加入一个可执行文件的路径,此参数会被传递到main函数的第二个参数
image.png

2.4 配置 查看可执行文件结构并输出到终端 的环境

  • 寻找终端

    在终端输入

    1. tty //输出终端的位置 /dev/tty006

    image.png

  • 输出重定向

    1. echo "HT" > /dev/tty006
  • xcode_run_cmd.sh放到工程目录 ```objectivec

    !/bin/sh

RunCommand() {

判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空

[[是 bash 程序语言的关键字。用于判断

if [[ -n “$VERBOSE_SCRIPT_LOGGING” ]]; then

  1. #作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
  2. if [[ -n "$TTY" ]]; then
  3. echo "♦ $@" 1>$TTY
  4. else
  5. echo "♦ $*"
  6. fi
  7. echo "------------------------------------------------------------------------------" 1>$TTY

fi

与$*相同。但是使用时加引号,并在引号中返回每个参数。”$@” 会将各个参数分开,以”$1” “$2” … “$n” 的形式输出所有参数

if [[ -n “$TTY” ]]; then echo $@ &>$TTY else “$@” fi

显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

return $? }

EchoError() {

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

}

RunCMDToTTY() { if [[ ! -e “$TTY” ]]; then EchoError “==========================================” EchoError “ERROR: Not Config tty to output.” exit -1 fi

  1. if [[ -n "$CMD" ]]; then
  2. RunCommand "$CMD"
  3. else
  4. EchoError "=========================================="
  5. EchoError "ERROR:Failed to run CMD. THE CMD must not null"
  6. fi

}

RunCMDToTTY

  1. xcconfig中定义TTYCMD<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1994311/1611073451507-7c777fb5-a001-4cfe-91d6-e06757a7af9b.png#align=left&display=inline&height=207&originHeight=414&originWidth=2232&size=177895&status=done&style=none&width=1116)
  2. - 创建 查看Macho headerxcconfig文件,并输入如下代码
  3. ```bash
  4. CMD_FLAG=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
  5. //objcdump --macho -private-header ${MACH_PATH}
  6. //otool -h ${MACH_PATH}
  7. CMD=objcdump --macho -private-header ${MACH_PATH}
  8. TTY=/dev/tty006
  • 在工程中的Debug下配置xcconfig

image.png

  • Build Phases 先的 Run Script 中输入shell命令
    1. /bin/sh "$SRCROOT/xcode_run_cmd.sh"
    image.png
    点击编译,就可以在终端查看到macho header
    image.png

    2.5 查看可执行文件的的__TEXT段

    在2.4的配置基础上修改xcconfig
    image.png
    在终端输出:
    image.png

    2.6 查看重定位符号表

    汇编生成.o文件过程,内存还没有被虚拟化,没有形成虚拟内存,它所干的事是:
    1.将源代码能汇编,生产汇编指令,并对照ABI生成机器指令
    2.符号归类,API的符号放到重定位符号表中

在2.4配置的基础上修改xcconfig文件

  1. //查看.o文件中的重定位符号表
  2. MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
  3. CMD = objdump --macho --reloc test.o
  4. TTY=/dev/ttys000

image.png

2.7 查看符号表

在2.4配置的基础上修改xcconfig文件

  1. MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
  2. //查看符号表
  3. CMD = objdump --macho --syms ${MACH_PATH}
  4. TTY=/dev/ttys006

终端输出结果:
image.png

可以给全局符号添加hidden属性,变成本地符号 double defaultx ; 这样定义的符号是全局符号 g double defaultx __attribute((visibility(“hidden”))); 全局符号添加hidden属性后变成了本地符号 l

补充attribute给编译器传参数,参数的可选值:

  • “hidden”
  • “default”
  • “protected”: 注意clang不支持protected
  • always_inline : 内联
  • cleanup(ASUnlockSet) : 结束时清理函数
  • deprecated : 标记方法被废除

2.8 查看整个Macho的符号链接的信息

在2.4的配置下,修改xcconfig文件

  1. // -map 查看整个Macho的符号和导出的信息
  2. OTHER_LDFLAGS=$(inherited) -Xlinker -S -Xlinker -map -Xlinker /Volumes/disk/ios V9.0/强化班/第一节、符号与链接(下)/上课代码/Symbol.text

image.png

3. 符号的种类与作用

Symbol Table:就是用来保存符号的表(符号叫什么+符号地址)
String Table: 就是用来保持符号的名称的表
Indirect Symbol Table:间接符号表。保存使用的外部符号(eg:NSLog)。更准确一点就是使用的外部动态库的符号。是Symbol Table的子集。

读取符号表是通过读取LC_SYMTAB的Load Command,并找到符号表的内存地址和长度还获取的

3.1 分类

  • 按照存在的空间区分:

non private external
weak private external

  • 按照模块区分:

weak global
weak local
‘ ‘

  • 按照功能分:

f : File
F : Function
O : Data
d: Debug
ABS : Absolute
COM : Common
UND : ?

⚠️ 去除调试符号,配置xcconfig 调试符号:由汇编器生成.o文件时,会生成一个DWARF格式的调试信息,它会被放到DWARF段,在链接是,会将DWARF段放到符号表中,链接后所有的符号都放在符号表中

  1. //去除编译符号
  2. OTHER_LDFLAGS=$(inherited) -Xlinker -S

image.png

  • 按照符号种类划分:

U :undefined(未定义)
A : absolute(绝对符号)
T : text section symbol(TEXT.text)
D : data section symbol(DATA.data)
B : bss section symbol(DATA.bss)
C : common symbol(只能出现在 MH_OBJECT 类型的 Mach-O 文件中)
-: debugger symbol table
S : 除了上面所述的,存放在其他section的内容,例如未初始化的全局变量存放在(DATA.common)中
I : indirect symbol(符号信息相同,代表同意符号)
u : 动态共享库中的小写u表示一个未定义引用对同一库中另一模块中私有外部符号

3.2 导入(Import)导出(Export)符号

export symbol:导出符号意味着,告诉别的模块,有这样一个符号,你可以将其引入(Import)
符号直接影响api的体积
查看导出符号表:在2.4的配置基础上修改xcconfig文件

  1. MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
  2. //查看导出符号表
  3. CMD = objdump --macho --exports-trie ${MACH_PATH}
  4. TTY=/dev/ttys006

image.png

查看间接符号表:在2.4的配置基础上修改xcconfig文件

  1. MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
  2. //查看间接符号表
  3. CMD = objdump --macho --indirect-symbols ${MACH_PATH}
  4. TTY=/dev/ttys006

image.png

strip用来脱动态库的符号的时候,脱去的是非全局符号,也就是说导出符号不能被脱去,而全局符号都是导出符号

因此自定义动态库的时候,如果希望提交尽可能的小,我们需要把不想暴露的符号本地化,或者告诉编译器不导出符号

  1. //不导出符号
  2. OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_LGOneObject

image.png

⚠️ 不导出符号 OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_LGOneObject 和hidden属性修饰?

将符号定义为不导出符号后,将变成一个local符号,然后strip动态库的时候,就可将符号脱掉

批量不到出符号 -unexported_symbol_list

3.3 two_leveInamespace & flat_namespace

二级命名空间与一级命名空间。链接器默认采用二级命名空间,也就是除了会记录符号名称,还会记录符号属于哪个Macho的,比如会记录下_NSLog来自Foundation

在同一个工作空间的两个Project中(LGOneFramework,LGApp)

在LGOneFramework中的一个.m中定义一个全局符号
image.png
在LGApp项目中可以声明和调用
image.png
也可在LGApp中重新定义重名的全局符号(符号在编辑时,采用的是二级命名空间,不仅记录符号,还记录符号属于哪个Macho)
image.png

⚠️ 在同一个项目里,全局符号都可见,只可以定义一次

3.4 Weak Symbol

  • Weak Reference Symbol:表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。静态链接器会将此符号设置弱引用链接标志。 ```objectivec // 在.h文件中弱引用声明 void weakimportfunction(void) __attribute((weak_import));

//在.m文件中实现 void weak_import_function(void) { NSLog(@”weak_import_function”); }

  1. 在其他文件中使用,声明 `void weak_import_function(void);` 也可以不声明,配置连接器参数:
  2. ```objectivec
  3. //告诉编译器 指定符号未定义,需要链接器动态查找,编译不检测是否定义
  4. OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function

链接器对未定义的符号 默认是报错,但开发者可以指定动态查找image.png
image.png

使用场景: 将动态库声明为弱引用,当工程没有导入动态库的时候,不会报错

  • Weak defintion Symbol:表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。

声明为弱定义的符号,不影响它作为导出全局符号

  1. // weak def 弱定义
  2. void weak_function(void) __attribute__((weak));
  3. // weak 本地符号
  4. void weak_hidden_function(void) __attribute__((weak, visibility("hidden")));

image.png

⚠️ 弱定义符号的优势:在全局其他文件中可以 再次定义一个同名符号不会编译冲突 报错。 image.png

3.5 Common Symbol

在定义时,未初始化的全局符号。
链接器设置:
-d :强制定义Common Symbol
-commons:指定对待Common Symbol如何响应

3.6 重新导出符号

给间接符号起别名

  1. //给间接符号_NSLog起别名为CAT_NSLog
  2. OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker CAT_NSLog

查看符号表:

  1. //查看符号表,刷选'Cat'
  2. nm -m ${MACH_PATH} | grep 'Cat'

image.png

应用场景:main中 导入一个动态库framework1时,动态库framework1中导入另一个framework2,main是不见framework2的,这时就可以在framework1中重新导出framework2的api,使main可以调用framework2的api

全局符号,本地符号
image.png
隐藏全局符号(将全局符号变成本地符号)

  • 方式一: 给全局符号加上 static关键字修改
  • 方式二:定义全局符号时,给编译器添加hidden属性

3.7 查看符号表

在2.4的基础上修改xcconfig

  1. MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
  2. // 查看符号表
  3. // objdump --macho --syms ${MACH_PATH}
  4. CMD = objdump --macho --syms ${MACH_PATH}
  5. TTY=/dev/ttys002

image.png

3.8 run Script执行的时机

编译完成后,签名之前
image.png

4. strip命令

移除或修改符号表中的符号。

  • App 进行脱符号,可以将本地符号,全局符号全部脱去(All symbols),只留下间接符号表中的符号。

    1)如何脱去调试符号
    image.png
    2) 如何脱去All Symbols
    image.png

  • 静态库进行脱符号,放置在重定位符号表中的符号不能脱去,能脱去的只要调试符号(Debugging Symbols) image.png

调试符号:由汇编器生成.o文件时,会生成一个DWARF格式的调试信息,它会被放到DWARF段,在链接是,会将DWARF段放到符号表中,链接后所有的符号都放在符号表中

  • 动态库进行脱符号,只要不是全局符号都可以被干掉(Non-Global Symbols)

如何脱去Non-Global Symbols
image.png

链接一个静态库,静态库中的符号会合并到APP中,但不会存入间接符号表,然后进行APP脱符号时,可以体积缩小
链接一个动态库,动态库中的符号表会放到APP的间接符号表,然后进行APP脱符号时,间接符号表不能被脱去

4.1 Build Setting的Strip Style选择

  • Debugging Symbols
  • All Symbols
  • Non-Global Symbols

image.png

4.3 Strip执行的时机

编译完成执行脚本后 ,签名之前,所以这是后剥离符号 已经不起作用了
image.png

4.3 Strip剥离符号

  • -x : non_global
  • 无参数: 代表全部符号
  • -S : 剥离调试符号

    app瘦身: 1) 在编译的时生产目标文件时优化 -O1,-Oz;2) dead code strip 在链接的时死代码剥离 ;3) strip 生产Macho文件后剥离符号 ;

给链接器传参 -S (去除调试符号)
image.png

4.4 dead strip

链接器的参数
image.png