iOS技能拓展 初识符号与链接
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调用过程
- 调用
fork
函数,创建一个process
- 调用
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文件
复制代码
2.查看__TEXT段
objdump --macho -d machO文件
复制代码
3.编译链接过程
- 生成目标文件
在编译时编译器干了两件事情:
- 将代码尽可能的转成汇编语言
- 将符号归类——上例使用的
NSLog
属于导入符号
(存在别的machO文件中)它会在链接时才确定它的内存地址,因此需要暂存起来——放到重定位符号表
中(其他用到的系统库API均是如此)- 为什么在链接时才能确定它的内存地址,是因为生成
目标文件
时内存没有虚拟化,本machO文件中符号可以通过地址偏移得到,而导入符号
(其他machO文件)却不行 - 同时也可以通过查看
重定位符号表
来查看API的使用情况
- 为什么在链接时才能确定它的内存地址,是因为生成
/// 查看目标文件的重定向符号表
objdump --macho --reloc 目标文件
复制代码
- 生成可执行文件
粗略的讲,链接过程是将多个目标文件
的符号表汇总到一张表中(处理目标文件的符号表),最后去生成可执行文件exec
三、符号表
1.符号表
Symbol Table
:用来保存符号String Table
:用来保存符号的名称Indirect Symbol Table
:叫做间接符号表,用来保存使用的外部符号。更准确一点就是使用的外部动态库的符号,是Symbol Table
的子集,例如使用Foundation库
中的NSLog
就是间接符号
使用如下命令就可以查看可执行文件中符号表,其中-p
表示不排序,-a表示输出全部符号表,包括调试符号
nm -pa xxx(MachO文件路径)
复制代码
迷迷糊糊能看到main
、NSlog
、objc_autoreleasePoolPop
、objc_autoreleasePoolPush
等输出,这不正就是我们代码中的main
函数执行嘛!
但是每次使用nm -pa xxx(MachO文件路径)
总归有点麻烦,好在我们可以使用脚本(脚本是真的香)
nm -pa ${BUILD_DIR}/${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/* > /dev/ttys000
复制代码
nm
:在linux
中列出目标文件的符号清单,常用来查看动态链接库中的函数-p
:不排序符号,使用该选项后的输出没有按照地址也没有按照符号名称排序-a
:输出全部符号表,包括调试符号${BUILD_DIR}/${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/*
:Xcode内置的参数以便于使用相对路径来执行命令/dev/ttys000
:终端窗口。可以在终端窗口使用tty
查看当前终端
也可以在项目根目录下新建一个build.sh
,在文件中添加需要执行的脚本命令,同时在Run Script
中进行配置脚本(有可能需要赋予执行权限)
从这个图可以看出链接主程序
->脚本运行
->签名应用
2.调试符号
- 文件通过汇编器生成目标文件时 会生成一个
DWARF格式
的调试文件,它被放在machO文件中的DWARF段
; - 而在链接过程中
DWARF段
会被干掉并放到可执行文件的符号表中
3.剥离调试符号
方案一:Xcode中给我们提供了Strip Symbols
选项
但是编译之后终端输出没有任何变化,这是因为剥离符号
是在执行脚本
之后的
方案二:我们可以通过设置链接器参数来修改链接时的配置,具体可以通过man ld
在终端中查看,从而会发现-S
参数可以剥离调试符号
那么具体怎么配置呢?
- 新建
Configuration
文件 - 将
Product
和Configuration
文件一一对应起来 - 配置
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
:用它定义的符号将不被导出
隐藏全局符号
有两种方法:
- 使用
static
修饰(最为简单) - 修改其可见性(全局符号转为本地符号,且未初始化的全局变量会被存放在未初始化的变量区中)
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 全局符号总结
全局符号对整个项目可见;本地符号对当前文件可见
- 动态库中的全局符号,仅在主项目中声明也可以使用;
- 动态库中的静态符号,在其他项目中都不可使用
- 在主项目、动态库中分别声明同一名称的符号,就牵扯到
二级命名空间
问题 - 同一项目中不能存在多个全局符号(因为二级命名空间一样)
二级命名空间&一级命名空间,链接器默认会采用二级命名空间,也就是除了记录符号之外,还会记录符号属于哪个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'
复制代码
- Swift文件会生成很多符号
public
和private
对应着全局符号
和本地符号
BuildSettings
中有配置项可以对Swift符号进行剥离——Strip Swift Symbols
五、剥离符号表
动态库
要留下导出符号
供外部使用- 不能剥离
全局符号
/导出符号
——Non-Global Symbols
- 不能剥离
静态库
是目标文件的合集+重定位符号表,只能接触到调试符号- 只能剥离
调试符号
——Debug Symbols
- 只能剥离
App
不需要供外部使用,但是需要保留外部导入的符号- 不能剥离
间接符号表
/导入符号
(NSLog)——All Symbols
- 不能剥离
就符号而言,App链接同等代码量的静态库和动态库,哪个包体积更小?
- 静态库的所有符号都会放到主工程中的符号表中——可能有
全局符号
、本地符号
、导出符号
等(除了导入符号
)- 而App中除了
导入符号
,其他全部可以被剥离
- 而App中除了
- 动态库的
导出符号
都会放到主工程的间接符号表
中- 动态库的
导出符号
不会被剥离
- 动态库的
所以App链接静态库
的体积会小于动态库
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK