作者:米广,有赞 iOS 开发,喜欢折腾,微信订阅号:剁手指北, bilibili频道:yz06276
审核:
五子棋,老司机技能周报修改,首要致力于研讨一站式机器学习平台 — MNN 工作台,咱们能够前往 www.mnn.zone 下载
Damonwong,iOS 开发,老司机技能周报修改,就职于淘系技能部
前语
符号化能协助咱们在定位 bug 、溃散和功能瓶颈时,从运转时日志与仓库找到底子的代码原因; 信任咱们了解 atos 或 dSYM 等常用符号化东西,但这些东西是怎么运作的? 本篇文章将围绕符号化的界说、原理、实践与技巧,带领咱们对符号化进一步深层次了解; 本篇文章是根据 Session 10211 – Symbolication: Beyond the basics 编撰,Session 的演讲者是Apple – 功能东西团队的 Alejandro Lucena 工程师
什么是符号化?
「将 App 运转时信息映射为源码」
长话短说便是将运转时信息转化为源码信息,符号化是一种机制,将咱们在设备运转时 App 的内存地址和相关的指令信息转化为源码文件中具体文件名、办法名、行数等;
能够了解为将运转时机器怎么看待处理咱们 App
的信息转化成咱们开发者怎么看待处理咱们的 App(源码)。
假如短少这层转化,哪怕只要几行的代码的 App
,bug 定位也变得难以进行;
Demo
本文为了带领咱们了解符号化的原理,全文所用到的项目是一个简略的只要几行的Demo App,他全部代码如下:
demo 的逻辑很简略:
randomValue()
能够生成值区间在1-100间的随机数
numberChoices()
能够生成一个包括 10 个上述随机数的数组
selectMagicNumber(choices: numbers)
能够从入参 numbers 数组中,取出一个指定下标的元素
generateMagicNumber()
按部履行上述操作,回来取出下标的元素
此处的 MAGIC_CHOICE 是一个随机值
日常溃散日志的符号化
第一次履行这个 App
就溃散了,查看生成的过错日志,里边没有很直观的信息,是一堆内存地址,我只能看到 App
在主线程上 crash
了;
我测验直接 debug
我的 App
,但在履行中没有复现该问题,看来调试器也纷歧定能帮得了忙;屡次测验之后终于复现了,但程序溃散在汇编中,也没有直观的信息,汇编太硬核,搞不定。
上面的溃散日志和汇编仓库显然都不能直接处理问题,但在符号化的协助下,咱们能够不从这些原始内存地址中发掘过错;
信任咱们都知道在 Xcode Organizer
中载入 App
的 dSYM
文件,他会重新处理溃散日志,载入后咱们就能够得到下面这种可读的、能够取得调用信息、文件名、具体行数的溃散日志,溃散日志直接告知我,溃散时产生了数组越界拜访,十分直观;依据这些信息回溯到代码,咱们也容易发现随机值 MAGIC_CHOICE
容易导致,在拜访只要 10 个长度数组拜访时,产生数组越界;
运用 atos
指令行东西,咱们也能够得到上述信息
日常 Instruments
仓库的符号化
另一个符号化的比如是,在 Instruments 中进行功能优化时,检测提示该 App 会周期性的履行许多写入操作,呈现了周期性的高负荷区间和低负荷区间;但是默许右下角显现的仓库信息只能提示 App 正在写入文件,无论高负荷还是低负荷,都提示了相同仓库; 很明显这两个区间不会履行相同代码,这原因是由于当时的 Instruments 仓库是被部分符号化的,一般而言,在仓库中没有具体文件名和具体行数时,符号化是不彻底,此时咱们也能够手动在 Instruments 载入 dSYM 文件,载入后,咱们再查看高负荷区,明确提示有剩余的调试代码 addDebugLog() ,而在低负荷区没有该办法调用;dSYM 不只能够使只包括内存地址信息的溃散日志可读,还能够协助 Instruments 仓库信息更加有用,这些都能帮咱们找到问题背面的代码问题;
符号化原理
既然符号化的东西能够协助咱们定位代码问题,你肯定会问,What ?why?为什么dSYM 能够协助符号化?How?dSYM怎么协助完结了符号化?dSYM是符号化的全部吗?除了溃散日志和Instruments
,别的当地还能载入 dSYM
吗?atos
的 -o
-i
-l
各自有什么用处?Instruments
为什么未能直接供给彻底符号化的仓库?Xcode
编译设置对符号化有何影响?带着这些问题,让咱们深化探究一些符号化的原理。
为此咱们首要分解介绍符号化的两个进程: 第一步:从内存地址回溯到文件 第二步:复原运转时调试信息
第一步 – 与符号化相关的地址与转化
从内存地址回溯到文件地址,指的是将运转时随机的内存地址转化为磁盘上二进制文件中稳定可用的文件信息;正如内存地址有内存空间相同,二进制文件在磁盘上也有地址空间;但这两种地址空间不能直接转化,需求一种地址转化机制;
磁盘上的地址空间与二进制文件地址
磁盘地址空间的地址是编译时 Linker
链接器赋予二进制文件的地址;具体而言,linker
会把二进制代码分组,分组后的部分称为段 Segment
,每个二进制段都包括了一些数据和特点,例如段的称号,巨细,地址等;举例来说,二进制文件中的 __TEXT
段会包括对应的办法和函数,__DATA
段会包括程序的大局状态,例如大局变量;每个段都被赋予了一个绝无仅有的开端地址,这种规划保证了段与段之间不会重叠;
具体而言 linker
会把段信息记载在可履行文件头部,作为 Mach-O
头的一部分;众所周知, Mach-O
是一种可履行文件和库的文件格局,Mach-O
头中包括许多与段的特点信息相关的 load
指令,操作系统内核经过读取这些 load
指令来把对应的二进制段加载入内存;假如 App
用到了 Universal2
打包技能,那每种架构都会有与之对应的 Mach-O
头和相关段信息;
上面讲了段信息和 load
指令,让咱们来结合开端的小 demo,实践查看一下相关的 load
指令;咱们能够经过 otool -l
来输出 load
指令信息,结合 grep
(字符串挑选东西)能够过滤出 LC_SEGMENT_64
的 load
指令,如下图所示;输出成果提示 __TEXT
段的开端方位为 vmaddr
所示地址,段的长度为 vmsize
所示字节巨细;
将二进制文件载入内存
由以上信息,咱们了解到 load
指令会包括载入的地址和巨细,那为什么内核实践经过 load
指令载入后,二进制段的内存地址和这个 linker
生成的地址纷歧致?下图中内存地址和 linker
的 A
、B
、C
地址有啥联系?后文中会将 linker
生成的地址简称为 A
、B
、C
Address space layout randomization – 地址空间布局随机化技能
事实上,现在操作系统中都会有一种「地址空间布局随机化」技能,该技能是一种防备内存损坏缝隙被运用的核算机安全技能。ASLR
经过随机放置进程要害数据区域的地址空间来防止进犯者能牢靠地跳转到内存的特定方位来进犯拟定函数。现代操作系统一般都加设这一机制,以防备恶意程序对已知地址进行 Return-to-libc
进犯。简言之,内核在加载二进制段前,会初始化一个随机值,称为 ASLR Slide
「内存空间随机散布偏移量」,后文中会把该偏移量简称为 S
;之后内核会将该偏移量 S
叠加到 linker
生成的 load
指令的地址 A
、B
、C
上;因而,内核在履行 load
指令时,不会依照原始的 linker
地址直接载入到内存地址 A
、B
、C
中,而是载入到 A+S
、B+S
、C+S
,咱们能够把这些实践的 load
加载地址称为 Load Address
「加载地址」,后文中,Load Address
将简称为 L
经过了解 ASLR
技能,咱们弄明白了 linker address
和 load address
之间的差值是 ASLR Slide
随机内存地址散布偏移量;咱们能够得到该公式 ALSR Slide = Load Address - Linker Address
, 简化为 S = L - A
怎么获取实践的 Linker Address 和 Load Address
前面现已说到 otool
能够协助咱们查看二进制文件的 load
指令信息,从而得到 linker address
(该地址也能够视为 file address
「文件地址」)
而取得运转时内存地址中的 Load Address
,能够经过溃散日志中的 Binary Image
列表,Instruments
供给的仓库,或者经过 vmmap
指令行东西来获取;具体怎么运用 vmmap
在后文中会有解说
核算 ASLR Slide 随机内存偏移量
结合实践,咱们需求知道 ASLR Slide
随机内存偏移量,才能够从溃散日志和 Instruments
仓库中的内存地址,减去 ASLR Slide
而取得文件地址;因而需求先核算出 ASLR Slide
,核算 ASLR Slide
一般以特定段(如 __TEXT
)的 load address
和 linker address
来相减得出,怎么获取这俩地址上面现已说了,结合实践咱们从溃散日志中获取了 __TEXT
二进制段的 load address
为 0x10045c000
; 经过 otool
我能够取得 __TEXT
二进制段的 linker address
为 0x100000000
;将这两者相减咱们就能够的得到 ASLR Slide = 0x45c000
;
有了 ASLR Slide
,咱们能够从溃散日志的运转时内存地址,换算出磁盘地址空间中的文件地址,如下图所示,咱们能够得到咱们 demo 中溃散的仓库的文件地址为 0x10003b70
,有了文件地址,咱们能够用来查看源码,这个后续再说。咱们先继续探索一下其他核算 ASLR Slide
的姿态
如下图所示,otool
指令行东西能够用来查看溃散时产生问题的指令信息, 传入 -tV
能够输出汇编仓库;-arch arm64
是为了让 otool
正确处理 Universal 2
技能编译的产物;输出结构对应上述文件地址,显现此是 brk
指令,汇编中的 brk
一般代表着 App
呈现了异常或问题;
atos
指令行东西也能够帮咱们核算 ASLR Slide
, atos
的 -o
指令会输出 file segment address
, -l
指令会输出 load address
;
除了 atos
和 otool
,还有 vmmap
指令行东西也能够协助咱们获取 load address
,咱们能够用 vmmap
来验证上面的核算成果, vmmap
输出溃散时 __TEXT segment
的 load address
,运用之前公式能够核算出本次运转的 ASLR Slide
为 0x104d14000
,将本次溃散日志中的 runtime address - ASLR Slide
得到了 file address
为 0x100003b70
,和之前核算的 file address
相同;
上述两次不同运转时, 不同溃散日志,不同的 ASLR Slide
能够得到同一个 file address
,这不是巧合;是由于内核每次运转的 ASLR Slide
都不同,因而不一起间,不同设备的溃散日志中所对应的内存地址会改变,但实践的 linkder address
是相同的;根据此,尽管内存地址每次改变,咱们依然能够定位到相同的 file address
;
至此,咱们发现了一种机制,能让我在随机的运转时内存中,定位到咱们 App
源码等级所产生的的事;经过这种映射机制能够让咱们从运转时的仓库信息中,回溯到 App
源码中;
小结 – 从内存地址回溯到文件地址
以上内容便是「符号化两步走」中的第一步:从内存地址回溯到文件,总结一下该进程中的内容和东西
-
App
和 库的二进制文件格局是Mach-O
,其间Mach-O
的头中存放了二进制段的相关信息和load
指令,这些二进制段是linker
创建的,其间包括了二进制段的地址信息linker address
; -
otool -l
能够协助咱们输出Mach-O
中指定二进制段的地址和特点信息,其间包括linker address
; - 溃散日志中的
binary image
列表中能够获取溃散产生时的load address
; -
vmmap
也能够取得正在运转App
的load address
-
ASLR Slide + Linker address = Load address
第二步 – 剖析调试信息
有了以上基础,咱们能够进一步评论符号化的第二步:剖析调试信息;调试信息一般包括了 file address
和源码之间的联系信息;Xcode
会在编译时生成这些联系信息并存放为 dSYM
文件,也能够把这些联系信息内置在二进制编译产物中;
这些调试信息有三种类型,每一种都供给了不同等级与 file address 相关的调试信息;
Function starts
Nlist symbol table
DWARF
下图中展现了这三种东西别离供给了对应维度的调试信息
Function Starts
从上图中可知,function starts
相较于其他东西供给最少的信息,该东西只能供给函数对应的开端地址,具体而言,function starts
会供给函数的开端地址和其调用的地点的地址;但这其间不会告知你这调用地址里是否有其他函数,他只能告知你这里有个函数出问题 ;
function starts
经过编码 __LINKEDIT
二进制段中的 linker
地址列表来供给该功能;
function starts
根据直接内置在 App
编译产物中,经过 mach-O
文件的 load
指令的 LC_FUNCTION_STARTS
来描绘 function starts
;
实践中,能够经过 symbols -onlyFuncStartsData
指令行东西来输出 function starts
相关信息,如下图所示,其间的 null
是由于 function starts
不供给函数称号,所以用 null
来做函数称号的占位符;
根据 function starts
咱们能够对未符号化的溃散日志进行处理,先从溃散日志的内存地址 0x10045fb70
减去之前核算好的 ASLR Slide
0x45c000
得到 file address
0x100003b70
;
然后结合 function starts
输出成果,咱们发现只要第一个地址 0x100003a68
小于咱们算出的 file address
0x100003b70
,所以只要这第一个地址包括了过错产生的地址;
根据此咱们核算这两个地址之间偏移了 0x108
,换算成十进制 是 264
,也便是咱们 file address
与实践过错产生地址之间有 264
字节的偏移量;
至此 function starts
能够协助咱们了解溃散日志中的函数怎么被设置,修改了哪些寄存器;但由于 function starts
不供给函数名,咱们只能在初级的机器码层面来剖析这些过错日志,关于调试开发 App
来说挺有用,但关于剖析过错日志,咱们还需求其他东西;
Nlist symbols List – Nlist 符号表
nlist
是一个结构体,他具体结构如下图所示,nlist
符号表建立在 function starts
和一个编码后的 __LINKEDIT
segment
的信息列表,当然 nlist
有自己的 load
指令;与 function starts
不同的是 nlist
不只是编码内存地址,他在其结构体中编码了更多信息;
如下图所示,nlist
结构体中包括了称号和其他几个特点,具体而言 nlist
的类型由 n_type
所决议
n_type
有三种类型是咱们符号化所感爱好的,这里咱们先着重聊一聊其间两种;
第一种是 direct symbole
– 直接符号;直接符号相关的是在 App
和二方库中,包括了已被完好界说的办法和函数;直接符号在 nlist_64
结构体中存储了函数姓名和函数文件地址;
Nlist 直接符号
n_type
中的指定二进制位的值决议了该 nlist
的类型,具体而言,n_type
中的第二、三、四的二进制位为 1
时,表明该 nlist
类型为直接符号,这三个位的组合还被叫做 N_SECT
;
咱们能够经过 nm -defined-only —numberic-sort
指令行东西来查看 N_SECT
;在这里 nm
遍历了 magicNumbers
App
的拟定符号,并以地址次序罗列出来,具体参照下图中的输出;注意此处咱们还是用了 xcrun -swift-demangle
来解析 Swift mangling
后的函数称号;
上图所示,咱们现已能够从成果中取得了办法名 numberChoices()
、类名 MagicNumbers
、文件名 main
;这是由于这些信息直接在 App
内界说;
symbols
查看直接符号
和 nm
东西相似, symbols
指令行东西也供给查看 nlist
数据的办法,而且支持主动 demangle
,具体如下图
以上两个办法,让咱们从溃散日志中的内存地址,相关到了源码中的具体函数称号,至此,溃散日志的符号化的信息丰厚程度更进一步;
至此,咱们经过 fuction starts 供给的函数进口偏移地址从 direct symbols 中匹配到一个函数进口,而且这个进口有姓名,把这些信息放在一起,咱们能够发现 crash 产生在 main 办法地址的 264字节偏移处;但 main 并不是溃散中仅有的函数,这表明咱们还有更多的信息有待发掘;例如咱们还没有弄清楚代码中的行数信息
咱们现已弄清 main
并不是仅有与溃散相关的函数,咱们还有更多的信息有待发掘;例如咱们还没取得文件的行数信息;而且在上述符号化中,部分函数被序列化,还有部分仓库和溃散日志信息没有被符号化
咱们在 Instruments
的仓库中遇到了相似的状况,一些函数名被符号化而可读,但部分仍是内存地址;产生这种现象的原因是,直接符号表中所包括的函数,只限于在链接时被直接链接的部分,动态库等运转时加载的二进制文件不被包括在内,这些未能符号化的办法便是跨模块从动态库中调用的办法;咱们需求其他手法了符号化这些调试信息;
这种直接符号表的逻辑,有助于削减编译产物体积;究竟换位考虑,假如把打包时全部相关函数信息都存入符号表,这种操作才有违常识;关于 Frameworks
和 Libraries
,咱们需求处理记载那些被调用的办法,而剥离没用到的;当然了假如把直接符号表里的主程序内的函数剥离,那符号表里啥也不剩了;
Xcode 编译设置对 nlist 直接符号的影响
在 Xcode
的编译设置中,strip
装备项有 strip linked product
、strip style
、strip swift symbols
三个选项。这些编译设置的选项操控了 App
在编译链接进程中的剥离剩余符号表的逻辑;具体来讲,strip linked product
为 YES
时,二进制文件中将依据 strip style
的值进行符号表剥离;举例来说,strip style
值为 all symbols
时,符号表中将履行最激进的剥离战略,终究符号表中只包括最核心的办法;Non globals
类型会剥离运用中不同模块中共同运用的直接符号,但会留下用于其他 APP
中的符号;Debugging symbols
则删除了第三种 nlist
类型的符号,这个后续评论 DWARF
时会讲到,但该类型的剥离会保存直接用到的符号。
举例来说,这里有一个界说了两个 public interface
接口和一个 internal shared
完结的办法的 framework
,由于全部这些函数在链接环节中有用,他们都具有直接的符号项。
假如我依照 non globals
进行剥离,那只要两个 interface
会留下;由于同享完结的函数只在 framework
内运用,所以它不是大局的,从而也不会被放入符号表;
相似的假如是 all symbols
剥离战略不时,假如这两个 interface
有被 framework
外部所调用时,他们依然会被留下;
symbols —onlyNListData
会输出一些散布在直接符号之间 function starts
的条目;这些条目也表明了函数是存在于直接符号表中,亦或是现已被剥离了。你能够运用这些剥离设定,来完结你需求的符号表可见性;有了这些信息,咱们就能够确定什么时分需求直接符号表。在实践运用中,有时分咱们能符号化出函数名,但没有具体行数和文件名;或者符号化成果包括了办法名和办法开端地址,正如此处 framework
的 symbols
指令的比如;
直接符号 – Indirect symbols
与直接符号相似,直接符号的 n_type
的第一位二进制位为 1
,或称为 n_EXT
经过 nm -m -arch arm64 -undefined-only --numberic-sort MagicNumbers
输出直接符号的信息;这其间运用 —undefined-only
来替换 —defined-only
,该指令用于查看直接符号; -m
,这能够让你看到这些办法源自哪个 framework
或 libraries
。下面图中的输出成果提示 MagicNumbers
App
依靠了 libSwiftCore
中的一系列 Swift
基础办法如 print()
。
####小结 – Function starts 与 nlist 符号表
文章开头,咱们约定了要评论 function starts
、nlist 符号表
和 DWARF
三种符号化东西;截止现在现已评论了前两种,在此回顾一下;
-
Function starts
能供给地址列表,短少办法名,能够协助核算溃散对应的文件地址偏移量; -
Nlist 符号表
把相关到一个地址的具体信息构成结构体存储,nlist
符号能供给函数称号,还能够描绘在App
内界说的直接符号和在二方库中供给的直接符号;直接符号表一般保存与链接有关的函数,Xcode
项目设置中的strip build style
会影响直接符号表中的内容; - 这两种符号表都直接嵌入在
App
二进制文件Mach-O
头中的__LINKEDIT
二进制段中
DWARF
截止现在咱们还没能看到诸如文件名、函数地点行数、溃散地点行数等符号化信息;这些信息在 DWARF
中都有供给,咱们在此具体评论一下 DWARF
;
相较于 nlist
符号表只保存函数部分信息,DWARF
几乎记载了函数的全部上下文信息;回顾 function starts
只在一个维度上供给偏移量信息;nlist
根据编码 nlist_64
结构体将调试信息升级到两个维度,即地址信息和函数称号;作为比较 DWARF
增加了第三个维度:联系信息;实践项目中函数不是孤立存在的,函数会被调用和在其内部调用其他函数,函数会有出参入参;经过记载这些函数的上下文联系信息;DWARF 会带咱们解锁符号化最牛逼的姿态;
当咱们剖析 DWARF
时,一般指的是引用剖析一个 dSYM bundle
,该 bundle
中存在由元数据组成的 plist
,还包括一个 DWARF
二进制文件;二进制文件中将 DWARF
的信息记载在 __DWARF
二进制段中;DWARF
在该二进制段中记载了咱们需求重视的三个数据流;具体而言三个数据流别离是 debug_info
, debug_abbrev
, debug_line
; debug_info
包括了原始数据,debug_abbrev
为原始数据进行了结构化处理,debug_line
包括了文件名和行号;除此之外 DWARF
还界说了需求评论的两种 vocabulary list
词汇表:compile unit
编译单元和 subprogram
子程序;后文会说到第三种词汇表 – 内联子程序
Compile Unit – 编译单元
编译单元表明了在项目中会被编译的单个源码文件;具体来说,在项目中的每个 swift
文件都会有一个编译单元与之对应;DWARF
为每一个编译单元赋予了一些特点,诸如文件名、模块称号、__TEXT segment
的函数占位部分等;main.swift
文件对应的编译单元在 debug_info
数据流中贮存了这些特点,如左侧所示;与之对应的,在 debug_addrev
数据流中包括了一个相关的条目,这些条目告知咱们这些值代表了什么,如右侧所示;咱们看到图中右侧包括了文件名、语言和一个 low/high
对,用来表述 __TEXT
segment
的规模
Subprogram – 子程序
子程序表明已被界说的函数;咱们现已在 nlist
符号表中找到过已界说的办法,但子程序还能够用来描绘静态办法和本地办法;子程序当然也有自己的称号和对应的 __TEXT
segment
地址开端规模
DWARF 联系树
编译单元和子程序之间的一个基本联系是,子程序是在编译单元中被界说的;DWARF
运用树来表述这种联系;编译单元在根节点上,子程序是根节点的孩子节点;这些子节点能够经过他们的地址规模而被检索到;
咱们能够经过 dwarfdump
指令行东西来验证上述 DWARF
的编译单元、子程序和联系树细节
首要咱们将查看到一个编译单元,这句之前说到的编译单元所带着的特点相吻合(文件名、语言、行数等),dwarfdump
东西结合了 debug_info
和 debug_abbrev
内容来展现 dSYMs
文件中的数据结构与内容
输出很长,咱们往下看,会看到一个子程序 subprogram
;它所占用的地址规模存在于该编译单元的地址规模内,而且能够看到办法名;之前说到过 DWARF
十分具体的描绘符号表和联系信息,咱们不会在深化探究 DWARF
的联系树 规划细节,但了解这些细节能够协助咱们了解符号化背面的逻辑;
继续往下看输出成果,会发现其间还包括参数信息,DWARF
持有一个自己的词汇表,来描绘参数的称号和类型;参数是子程序的一个子节点;下图中的输出,能够发现 numberofChoice
函数的参数 choices
的相关信息;
文件名与行数信息
此外,debug_line
数据流中存储了函数相关的文件名和具体行数;但 debug_line
数据流不是树状结构,相反的,该数据流界说了一个 line table program
行表程序,这个航标程序能够让链接后的文件地址映射到源码文件中的具体行数;咱们能够运用这个行表程序来查找文件地址相关的具体源码和行数;
综上,根据 debug_info
的树状结构和 debug_line
的行表程序,咱们能够得到一个下面的结构;经过遍历这棵树,咱们能够找到想要的文件地址;首要从编译单元开端,遍历其子节点,然后挑选出包括 debug_line
的子节点;
DWARF 与编译时函数内联优化
咱们能够运用 atos
指令行东西来完结上述操作,这次咱们省略 -i
flag
,能够看到输出成果少了许多,只剩下办法名、文件名和行数;这里的成果供给了行数,因而咱们能够断定咱们在运用 DWARF
来进行符号化;但除了文件名和行数,这个输出成果和 nlist
符号表的符号化成果没有太大差异;然后咱们再试一试给 atos
加上 -i
flag
,输出成果是下面第二张图,咱们能够比照这两个输出的差异,他们的指令只差了一个 -i
atos -o MagicNumbers.dSYM/Contents/Resources/DWARF/MagicNumbers -arch arm64 -l 0x10045c000 0x10045fb70
atos -o MagicNumbers.dSYM/Contents/Resources/DWARF/MagicNumbers -arch arm64 -l 0x10045c000 -i 0x10045fb70
咱们或许会猜,这 -i
意味着什么;事实上 atos
的 -i
意味着 inlined function
内联函数,内联化是一种编译器履行的惯例优化;具体而言,内联化便是在编译中把函数的完结代码直接替换函数被调用的代码;这样的替换操作能够让函数调用的代码和函数的界说代码都「消失了」;
在咱们的 Demo
中也便是运用 numberOfChoice()
的完结代码替换了调用代码;numberOfChoice()
调用代码不见了~
Inlined subroutines – 内联子程序
DWARF
运用内联子程序来表述这种编译时内联优化;这便是咱们要评论的第三种 vocabulary list
词汇表类型 :inlined subroutines
内联子程序;内联子程序是子程序的一种,所以他也是一种办法,一种被内联到另一个子程序的办法;所以内联函数在 DWARF
联系树中是子程序的一个子节点;这样的界说意味着会呈现递归联系;也便是说一个内联子程序能够有其他内联子程序作为子节点;
再次运用 dwarfdump
指令行东西,咱们能够来查看一下 DWARF
中的内联子程序;这些内联子程序被列为其他节点的子节点,而且有着与子程序相似的特点,诸如称号和地址;但是在DWARF
文件中,这些特点一般会经过一个公共节点来拜访,这种规划叫笼统源;假如存在一个特定函数有许多内联拷贝,则该函数的公共同享特点将存储在笼统源中,如此这些内联函数就不会被重复剩余的拷贝;内联子程序有一个独特的特点是 call site
调用方位;该特点表述了在源码中实践调用函数的方位,编译优化器会替换这些函数调用代码;例如,咱们在 main.swift
文件中第36行调用了 generateANumber()
,这使得需求在树中新增子节点来记载这个函数调用;
到这里,咱们对 DWARF
符号化有了更全面的了解,如下图所示,咱们对 App
的调用逻辑也有了更宽广的视角。了解内联函数的优化方法和细节是彻底符号化溃散日志的要害地点; -i
指令实践会要求 atos
符号化进程中考虑到上述内联函数;这些内联函数的信息相同在 Instruments
仓库中缺失;咱们在溃散日志和 Instruments
仓库中都需求 dSYM
文件,正是由于 dSYM
中精确地包括了上述三种类型的信息:编译单元、子程序和 DWARF
联系树;
从库和方针文件中获取 DWARF
除了 dSYM
文件中,还能够在静态库和方针文件中找到 DWARF
;也便是说即便没有 dSYM
文件,你依然能够从静态库或方针文件中链接的函数,来生成 DWARF
;这种状况下,你会找到调试符号表的 nlist
类型,这些本是能够被 strip
剥离的符号类型之一;但这些 nlist
类型并不直接包括 DWARF
,相反,他们直接把函数相关到其源码文件;假如一个库在构建中包括调试信息,此时,这些 nlist
条目能够给咱们供给 DWARF
的相关信息
上述类型的 nlist
条目能够经过 dsymutil -dump-debug-map
指令行东西来输出和具体查看;在此咱们列出了不同函数办法和他们的出处;这些地址信息能够被扫描并处理成 DWARF
文件中所需的信息;
小结 – DWARF
-
DWARF
是深度符号化数据的重要来历 -
DWARF
描绘了函数与文件之间的重要联系信息; -
DWARF
妥当处理了编译时内敛优化的问题; -
dSYM
文件和静态库能够都可包括DWARF
; - 实践中推荐运用
dSYM
获取DWARF
,由于从dSYM
中获取的DWARF
能够便利的在其他东西中运用,而且Xcode
许多内置东西也支持DWARF
;
开发东西与符号化实践
Xcode 编译设置 – Debug info format
- 针对本地开发装备主张设置为直接生成
DWARF
- 针对发布编译装备,请保证生成包括
DWARF
的dSYM
文件 - 提交至
App Store Connect
的App
,你能够在那下载到dSYM
- 即便运用了
bitcode
技能 ,你也能够从App Store Connect
下载到dSYM
文件
查找和承认 dSYM
文件
如下图所示,在本地 Mac
上能够接住 mdfind
指令行东西查看 dSYM
文件;这个字母数字组成的字符串是编译二进制产物的 UUID
,也是运转时 load
指令的仅有标识符;
你还能够经过 symbols -uuid
来查看 dSYM
文件的 UUID
;
在少量状况下,编译进程会生成一个无效的 DWARF
,你能够经过 draftdump -verify
指令来查验 DWARF
的有效性;假如这个查看指令输出任何过错,请直接经过 feedbackassistant.apple.com 来进行Developer Tool - 开发东西
的 bug
反应;
单个 DWARF
二进制文件巨细上线是 4GB
,假如上述校验中陈述超过 4GB
的过错,你能够考虑将项目的进行组件化拆分,以便每个组件会有一个较小的 dSYM
实践操作中,经过比较 dSYM
的 UUID
和溃散日志中 binary image
的 UUID
性来匹配两者;除了在溃散日志中查看 App
二进制镜像的 UUID
,你还能够经过 symbols
指令行东西来获取 UUID
,参照下图;实践符号化中,需求 dSYM
和溃散日志的 UUID
匹配;
其他符号化的细节
symbols
指令行东西还能够帮你查看你 App
编译产物中包括的可用调试信息;输出内容的方括号中的标签,告知了这些调试信息的来历;当你不知道在调试时运用哪些调试信息时,运用该指令能够看看有哪些调试信息可用;
假如你确信现已有可用 dSYM
文件了,但是仍旧未能将 Instruments
中的仓库信息符号化,请查看一下项目的 Entitlements
和代码签名装备;具体来说运用 codesign
指令行东西,你能够验证是否具有正确的代码签名装备;
一起,你还需求查看本地开发的 entitlement
中是否包括了 get-task-allow
项,该装备颁发 Instruments
这类东西在调试中履行对应 App
符号化的权利;一般来说,Xcode
默许主动会设置这个 get-task-allow
装备项;但 Instruments
不能符号化的时分,能够排查一下这个装备项;假如你发现 entitlement
中没有 get-task-allow
,能够查看保证 build-setting
-> code signing
-> code signing inject base entitlemens
的值为 true
,来处理该问题;
最后,关于运用 Universal 2
技能的 App
, 在运用文章中说到的指令行东西时,都能够指定架构,诸如 symbols
、otool
、dwarfdump
都有 -arch
的参数可供装备,如此能够只履行特定架构的相关操作;
总结
正如称号中的「符号化进阶」,用以下几个要害点来总结本 Session
- 符号化
UUID
和文件地址是一致且牢靠的方法来识别App
在运转时的问题,由于这两者不受ASLR Slide
偏移量的影响;UUID
和文件地址是运转时信息符号化要害的第一步 - 实践中,尽可能运用
dSYM
完结符号化;dSYM
以DWARF
的方式记载了最丰厚细节的调试信息,而且被Xcode
和Instruments
所良好支持 - 文中介绍了几款指令行符号化东西,诸如
otool
,vmmap
,nm
,symbols
,dwarfdump
,atos
;这些东西包括在Xcode Command line tool
中,供给了强壮的确诊和检视符号化进程与细节信息的才能;必要时,咱们能够将这些东西集成进自己的工作流;
假如你有爱好学习更多链接与符号化知识,我在此推荐两个WWDC18的Session :他们协助你了解 App 在发动时怎么运转起来,一个是Optimizing app startup time – 优化 App 发动速度,另一个是App startup time: past ,present, and future – App 发动的时间线:曩昔、现在和将来;
重视咱们
咱们是「老司机技能周报」,一个持续寻求精品 iOS 内容的技能大众号。欢迎重视。
重视有礼,重视【老司机技能周报】,回复「2021」,收取 2017/2018/2019/2020 内参
支持作者
在这里给咱们推荐一下 《WWDC21 内参》 这个专栏,一共有 102 篇关于 WWDC21 的内容,本文的内容也来历于此。假如对其他内容感爱好,欢迎戳链接阅读更多 ~
WWDC 内参 系列是由老司机牵头组织的精品原创内容系列。 现已做了几年了,口碑一直不错。 首要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实践开发经历、苹果文档和视频内容做二次创造。