iOS技能拓展:初识符号与链接
Posted 程序员大咖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS技能拓展:初识符号与链接相关的知识,希望对你有一定的参考价值。
????????关注后回复 “进群” ,拉你进程序员交流群????????
转自:掘金 我是好宝宝
https://juejin.cn/post/6961576195332309006
写在前面
本文主要介绍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链接静态库
的体积会小于动态库
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
点击????卡片,关注后回复【面试题
】即可获取
在看点这里好文分享给更多人↓↓
以上是关于iOS技能拓展:初识符号与链接的主要内容,如果未能解决你的问题,请参考以下文章