3

iOS技能拓展 初识符号与链接

 2 years ago
source link: https://juejin.cn/post/6961576195332309006
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文主要介绍Mach-O编译链接符号分类(文末有个符号知识题)

符号可能平时开发的时候接触不多,本文会从新手视角介绍一下这个在编译链接阶段默默付出的家伙

一、MachO

1.MachO

  • Mach-O(MachO Object)是macOS、iOS、iPadOS存储程序和库的文件格式。对应系统通过应用二进制接口(application binary interface,缩写为ABI)来运行该格式的文件
  • Mach-O格式用来替代BSD系统a.out格式。Mach-O文件格式保存了在编译过程和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一文件格式
  • Mach-O文件中全部由二进制组成,可以理解成文件配置+二进制代码

2.MachO调用过程

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

二、查看MachO信息

1.查看mach-header

为了方便就新建了一个MacOS的项目代码如下,编译生成可执行文件

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}
复制代码

使用如下命令查看mach-header

/// objdump查看
objdump --macho --private-header machO文件

/// otool查看
otool -h machO文件
复制代码

1.jpg

2.查看__TEXT段

objdump --macho -d machO文件
复制代码

2.jpg

3.编译链接过程

  1. 生成目标文件

image.png 在编译时编译器干了两件事情:

  • 将代码尽可能的转成汇编语言
  • 将符号归类——上例使用的NSLog属于导入符号(存在别的machO文件中)它会在链接时才确定它的内存地址,因此需要暂存起来——放到重定位符号表中(其他用到的系统库API均是如此)
    • 为什么在链接时才能确定它的内存地址,是因为生成目标文件时内存没有虚拟化,本machO文件中符号可以通过地址偏移得到,而导入符号(其他machO文件)却不行
    • 同时也可以通过查看重定位符号表来查看API的使用情况

image.png

/// 查看目标文件的重定向符号表
objdump --macho --reloc 目标文件
复制代码
  1. 生成可执行文件

粗略的讲,链接过程是将多个目标文件的符号表汇总到一张表中(处理目标文件的符号表),最后去生成可执行文件exec image.png

三、符号表

1.符号表

  • Symbol Table:用来保存符号
  • String Table:用来保存符号的名称
  • Indirect Symbol Table:叫做间接符号表,用来保存使用的外部符号。更准确一点就是使用的外部动态库的符号,是Symbol Table的子集,例如使用Foundation库中的NSLog就是间接符号

使用如下命令就可以查看可执行文件中符号表,其中-p表示不排序,-a表示输出全部符号表,包括调试符号

nm -pa xxx(MachO文件路径)
复制代码

image.png 迷迷糊糊能看到mainNSlogobjc_autoreleasePoolPopobjc_autoreleasePoolPush等输出,这不正就是我们代码中的main函数执行嘛!


但是每次使用nm -pa xxx(MachO文件路径)总归有点麻烦,好在我们可以使用脚本(脚本是真的香)image.png

nm -pa ${BUILD_DIR}/${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/* > /dev/ttys000
复制代码
  • nm:在linux中列出目标文件的符号清单,常用来查看动态链接库中的函数
  • -p:不排序符号,使用该选项后的输出没有按照地址也没有按照符号名称排序
  • -a:输出全部符号表,包括调试符号
  • ${BUILD_DIR}/${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/*:Xcode内置的参数以便于使用相对路径来执行命令
  • /dev/ttys000:终端窗口。可以在终端窗口使用tty查看当前终端

    image.png


也可以在项目根目录下新建一个build.sh,在文件中添加需要执行的脚本命令,同时在Run Script中进行配置脚本(有可能需要赋予执行权限) image.png


image.png从这个图可以看出链接主程序->脚本运行->签名应用

2.调试符号

  • 文件通过汇编器生成目标文件时 会生成一个DWARF格式的调试文件,它被放在machO文件中的DWARF段
  • 而在链接过程中DWARF段会被干掉并放到可执行文件的符号表中

3.剥离调试符号

方案一:Xcode中给我们提供了Strip Symbols选项

但是编译之后终端输出没有任何变化,这是因为剥离符号是在执行脚本之后的


方案二:我们可以通过设置链接器参数来修改链接时的配置,具体可以通过man ld在终端中查看,从而会发现-S参数可以剥离调试符号

那么具体怎么配置呢?

  • 新建Configuration文件
  • ProductConfiguration文件一一对应起来
  • 配置Configuration文件:OTHER_LDFLAGS = -Xlinker -S
    • -Xlinker表示后面的参数是传给链接器
  • 编译之后在BuildSettings中的other link flag中查看是否添加成功

四、符号表分类

1.全局符号和静态符号

将代码改写——添加全局变量和静态变量

#import <Foundation/Foundation.h>

// 全局变量
int global_num = 10;
int global_undefine_num;

// 静态变量
static int static_num = 10;
static int static_undefine_num;

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

复制代码

使用如下命令行查看可执行文件(剥去调试符号更容易查看)

objdump --macho -syms machO文件
复制代码

从终端输出可以看出:

  • 不管是否初始化,全局变量都变成了全局符号
  • 静态变量都变成了本地符号
    • 这里需要注意的是,如果静态变量未使用的话,是会变成调试符号
1.1 全局符号与本地符号

全局符号本地符号的本质区别是其可见性(visibility)可见性分为两种:

  • default:用它定义的符号将被导出
  • hidden:用它定义的符号将不被导出

隐藏全局符号有两种方法:

  1. 使用static修饰(最为简单)
  2. 修改其可见性(全局符号转为本地符号,且未初始化的全局变量会被存放在未初始化的变量区中)
int global_num __attribute__((visibility("hidden"))) = 10;
int global_undefine_num __attribute__((visibility("hidden")));
复制代码



1.2 二级命名空间
  • 动态库实现不对外声明的全局符号+主项目只做声明全局符号
    • 输出结果为动态库的代码 => 全局符号对整个项目可见
  • 动态库实现不对外声明的全局符号+主项目声明&实现全局符号
    • 输出结果为主工程的代码 => 全局符号对整个项目可见
    • 这是由于二级命名空间的缘故——链接器默认采用二级命名空间,除了记录符号名称,还会记录符号属于哪个可执行文件 => 优先使用本工程的符号
  • 动态库实现对外声明的全局符号+主项目声明&实现全局符号
    • 报错/Users/felix/Desktop/FXDemo/FXDemo/ViewController.m:18:6: Redefinition of 'global_symbol'
    • 因为动态库的全局符号对外导出了,在主工程会重新加入符号表
    • 如果不导入声明文件就不会报错
  • 主项目两个不同文件声明同一个全局符号
    • 报错1 duplicate symbol for architecture arm64
    • 因为两个符号命名空间一样
1.3 全局符号总结

全局符号对整个项目可见;本地符号对当前文件可见

  1. 动态库中的全局符号,仅在主项目中声明也可以使用;
  2. 动态库中的静态符号,在其他项目中都不可使用
  3. 在主项目、动态库中分别声明同一名称的符号,就牵扯到二级命名空间问题
  4. 同一项目中不能存在多个全局符号(因为二级命名空间一样)

二级命名空间&一级命名空间,链接器默认会采用二级命名空间,也就是除了记录符号之外,还会记录符号属于哪个machO的,比如记录NSLog属于Foundation

2.导入符号和导出符号

继续拿刚才的NSLog举例:

  • 对于本machO文件来说,导入了NSLog符号(导入符号)
  • 对于Foundation来说,它导出了NSLog符号(导出符号)

可以使用命令行查看本文件中的导出符号

objdump --macho --exports-trie machO地址
复制代码
  • 导出符号结果与全局符号结果相比较,可以看出导出符号一定是全局符号,因为它对整个项目都可见,且提供给别的项目使用
    • 由于符号表是占体积的,我们可以通过剥离符号来减少App体积
    • 而使用到的导出符号NSLog将作为间接符号保存起来,这部分符号是不能被脱去的,否则程序无法正常运行
    • 导出符号一定是全局符号这个结论可知,全局符号也是不能被脱去的

间接符号表用来保存外部符号,即导出符号,可以使用命令行查看本文件中使用到的间接符号表

objdump --macho --indirect-symbols machO地址
复制代码
  • 平时在定义全局符号/全局变量的时候,需要注意它在编译时会作为导出符号被别的空间/模块所使用
  • 一般情况下,全局符号导出符号,但这不是绝对的,我们可以通过链接器来控制它
    • 以动态库举例,它只需要在链接的时候提供导出符号即可,但Objective-C中所有类默认都是导出符号
    • 新建FXPerson的Objective-C对象,再去查看导出符号
    • 即便把Objective-C对象的声明从.h文件放到.m文件中,也丝毫不会改变它创建了一个导出符号的结果

可以通过在Xcconfig文件中这么定义,就能指定对应的“导出符号”不导出——不但可以减少App体积,同时无法通过符号访问对应类会更加安全

// 剥离调试符号
OTHER_LDFLAGS = ${inherited} -Xlinker -S
// 剥离FXPerson元类导出符号
OTHER_LDFLAGS = ${inherited} -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_FXPerson
// 剥离FXPerson类导出符号
OTHER_LDFLAGS = ${inherited} -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_FXPerson
// -unexported_symbol_list可以指定一个需要剥离文件的符号
// -map导出当前machO文件的符号信息以及链接其他库的信息
OTHER_LDFLAGS = ${inherited} -Xlinker -map -Xlinker 地址
复制代码

3.弱引用符号和弱定义符号

  • 弱引用符号(Weak Reference Symbol)如果链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置为弱链接标志
    • 关键字为weak import
    • 可以只做声明不做实现——需要判空使用
    • 不配置链接器参数会报错——Undefined symbol: _weak_import_function
    • 配置链接器参数为-U(告诉链接器这个符号是动态链接的,在编译时不需要理会)
    • OTHER_LDFLAGS = ${inherited} -Xlinker -U -Xlinker _weak_import_function
    • 作用:避免找不到符号实现而崩溃
  • 弱定义符号(Weak Defintion Symbol)如果链接器为此符号找到了另一个非弱定义,则弱定义将被忽略
    • 关键字为weak
    • 本身是一个全局符号/导出符号
    • 只做声明不做实现会报错
    • 声明+多个实现不会报错——动态运行会使用最先找到的弱定义符号,其他都将被忽略
    • 作用:避免多个全局符号的实现冲突

4.重新导出符号

NSLog这种导入符号在machO文件中是UND未定义

  • 如果别的可执行文件想重新使用这个符号的话,需要重新导出——放到本文件的导出符号表中——外界可以使用这个符号
  • 那么就需要用到链接器中的参数-alias(起别名)会把间接符号表变成导出符号
    • 仅限间接符号可以这么使用

5.Swift符号

添加一个Swift文件

import Foundation

private class SwiftPerson {
    func playGame() {
        
    }
}

public class PublicPerson {
    func playGame() {
        
    }
}
复制代码

使用命令行查看符号表并过滤

objdump --macho -syms machO文件 | grep 'Person'
复制代码

image.png

  • Swift文件会生成很多符号
  • publicprivate对应着全局符号本地符号
  • BuildSettings中有配置项可以对Swift符号进行剥离——Strip Swift Symbols

    image.png

五、剥离符号表

  • 动态库要留下导出符号供外部使用
    • 不能剥离全局符号/导出符号——Non-Global Symbols
  • 静态库是目标文件的合集+重定位符号表,只能接触到调试符号
    • 只能剥离调试符号——Debug Symbols
  • App不需要供外部使用,但是需要保留外部导入的符号
    • 不能剥离间接符号表/导入符号(NSLog)——All Symbols

就符号而言,App链接同等代码量的静态库和动态库,哪个包体积更小?

  • 静态库的所有符号都会放到主工程中的符号表中——可能有全局符号本地符号导出符号等(除了导入符号
    • 而App中除了导入符号,其他全部可以被剥离
  • 动态库的导出符号都会放到主工程的间接符号表
    • 动态库的导出符号不会被剥离

所以App链接静态库的体积会小于动态库


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK