作者:米广,有赞 iOS 开发,喜欢折腾,微信订阅号:剁手指北, bilibili频道:yz06276

审核:

五子棋,老司机技能周报修改,首要致力于研讨一站式机器学习平台 — MNN 工作台,咱们能够前往 www.mnn.zone 下载

Damonwong,iOS 开发,老司机技能周报修改,就职于淘系技能部

【老司机精选】iOS 符号化:基础与进阶

前语

符号化能协助咱们在定位 bug 、溃散和功能瓶颈时,从运转时日志与仓库找到底子的代码原因; 信任咱们了解 atos 或 dSYM 等常用符号化东西,但这些东西是怎么运作的? 本篇文章将围绕符号化的界说、原理、实践与技巧,带领咱们对符号化进一步深层次了解; 本篇文章是根据 Session 10211 – Symbolication: Beyond the basics 编撰,Session 的演讲者是Apple – 功能东西团队的 Alejandro Lucena 工程师

什么是符号化?

「将 App 运转时信息映射为源码」 长话短说便是将运转时信息转化为源码信息,符号化是一种机制,将咱们在设备运转时 App 的内存地址和相关的指令信息转化为源码文件中具体文件名、办法名、行数等; 能够了解为将运转时机器怎么看待处理咱们 App 的信息转化成咱们开发者怎么看待处理咱们的 App(源码)。 假如短少这层转化,哪怕只要几行的代码的 App,bug 定位也变得难以进行;

Demo

本文为了带领咱们了解符号化的原理,全文所用到的项目是一个简略的只要几行的Demo App,他全部代码如下:

【老司机精选】iOS 符号化:基础与进阶
demo 的逻辑很简略: randomValue() 能够生成值区间在1-100间的随机数 numberChoices() 能够生成一个包括 10 个上述随机数的数组 selectMagicNumber(choices: numbers) 能够从入参 numbers 数组中,取出一个指定下标的元素 generateMagicNumber() 按部履行上述操作,回来取出下标的元素 此处的 MAGIC_CHOICE 是一个随机值

日常溃散日志的符号化

第一次履行这个 App 就溃散了,查看生成的过错日志,里边没有很直观的信息,是一堆内存地址,我只能看到 App 在主线程上 crash 了;

【老司机精选】iOS 符号化:基础与进阶

我测验直接 debug 我的 App,但在履行中没有复现该问题,看来调试器也纷歧定能帮得了忙;屡次测验之后终于复现了,但程序溃散在汇编中,也没有直观的信息,汇编太硬核,搞不定。

【老司机精选】iOS 符号化:基础与进阶

上面的溃散日志和汇编仓库显然都不能直接处理问题,但在符号化的协助下,咱们能够不从这些原始内存地址中发掘过错; 信任咱们都知道在 Xcode Organizer 中载入 AppdSYM 文件,他会重新处理溃散日志,载入后咱们就能够得到下面这种可读的、能够取得调用信息、文件名、具体行数的溃散日志,溃散日志直接告知我,溃散时产生了数组越界拜访,十分直观;依据这些信息回溯到代码,咱们也容易发现随机值 MAGIC_CHOICE 容易导致,在拜访只要 10 个长度数组拜访时,产生数组越界;

【老司机精选】iOS 符号化:基础与进阶

运用 atos 指令行东西,咱们也能够得到上述信息

【老司机精选】iOS 符号化:基础与进阶

日常 Instruments 仓库的符号化

另一个符号化的比如是,在 Instruments 中进行功能优化时,检测提示该 App 会周期性的履行许多写入操作,呈现了周期性的高负荷区间和低负荷区间;但是默许右下角显现的仓库信息只能提示 App 正在写入文件,无论高负荷还是低负荷,都提示了相同仓库;

【老司机精选】iOS 符号化:基础与进阶
很明显这两个区间不会履行相同代码,这原因是由于当时的 Instruments 仓库是被部分符号化的,一般而言,在仓库中没有具体文件名和具体行数时,符号化是不彻底,此时咱们也能够手动在 Instruments 载入 dSYM 文件,载入后,咱们再查看高负荷区,明确提示有剩余的调试代码 addDebugLog() ,而在低负荷区没有该办法调用;dSYM 不只能够使只包括内存地址信息的溃散日志可读,还能够协助 Instruments 仓库信息更加有用,这些都能帮咱们找到问题背面的代码问题;
【老司机精选】iOS 符号化:基础与进阶

符号化原理

既然符号化的东西能够协助咱们定位代码问题,你肯定会问,What ?why?为什么dSYM 能够协助符号化?How?dSYM怎么协助完结了符号化?dSYM是符号化的全部吗?除了溃散日志和Instruments ,别的当地还能载入 dSYM 吗?atos-o -i -l 各自有什么用处?Instruments 为什么未能直接供给彻底符号化的仓库?Xcode 编译设置对符号化有何影响?带着这些问题,让咱们深化探究一些符号化的原理。

【老司机精选】iOS 符号化:基础与进阶

为此咱们首要分解介绍符号化的两个进程: 第一步:从内存地址回溯到文件 第二步:复原运转时调试信息

【老司机精选】iOS 符号化:基础与进阶

第一步 – 与符号化相关的地址与转化

从内存地址回溯到文件地址,指的是将运转时随机的内存地址转化为磁盘上二进制文件中稳定可用的文件信息;正如内存地址有内存空间相同,二进制文件在磁盘上也有地址空间;但这两种地址空间不能直接转化,需求一种地址转化机制;

【老司机精选】iOS 符号化:基础与进阶

磁盘上的地址空间与二进制文件地址

磁盘地址空间的地址是编译时 Linker 链接器赋予二进制文件的地址;具体而言,linker 会把二进制代码分组,分组后的部分称为段 Segment,每个二进制段都包括了一些数据和特点,例如段的称号,巨细,地址等;举例来说,二进制文件中的 __TEXT 段会包括对应的办法和函数,__DATA 段会包括程序的大局状态,例如大局变量;每个段都被赋予了一个绝无仅有的开端地址,这种规划保证了段与段之间不会重叠;

【老司机精选】iOS 符号化:基础与进阶

具体而言 linker 会把段信息记载在可履行文件头部,作为 Mach-O 头的一部分;众所周知, Mach-O 是一种可履行文件和库的文件格局,Mach-O 头中包括许多与段的特点信息相关的 load 指令,操作系统内核经过读取这些 load 指令来把对应的二进制段加载入内存;假如 App 用到了 Universal2 打包技能,那每种架构都会有与之对应的 Mach-O 头和相关段信息;

【老司机精选】iOS 符号化:基础与进阶

上面讲了段信息和 load 指令,让咱们来结合开端的小 demo,实践查看一下相关的 load 指令;咱们能够经过 otool -l 来输出 load 指令信息,结合 grep(字符串挑选东西)能够过滤出 LC_SEGMENT_64load 指令,如下图所示;输出成果提示 __TEXT 段的开端方位为 vmaddr 所示地址,段的长度为 vmsize 所示字节巨细;

【老司机精选】iOS 符号化:基础与进阶

将二进制文件载入内存

由以上信息,咱们了解到 load 指令会包括载入的地址和巨细,那为什么内核实践经过 load 指令载入后,二进制段的内存地址和这个 linker 生成的地址纷歧致?下图中内存地址和 linkerABC 地址有啥联系?后文中会将 linker 生成的地址简称为 ABC

【老司机精选】iOS 符号化:基础与进阶

Address space layout randomization – 地址空间布局随机化技能

事实上,现在操作系统中都会有一种「地址空间布局随机化」技能,该技能是一种防备内存损坏缝隙被运用的核算机安全技能。ASLR 经过随机放置进程要害数据区域的地址空间来防止进犯者能牢靠地跳转到内存的特定方位来进犯拟定函数。现代操作系统一般都加设这一机制,以防备恶意程序对已知地址进行 Return-to-libc 进犯。简言之,内核在加载二进制段前,会初始化一个随机值,称为 ASLR Slide 「内存空间随机散布偏移量」,后文中会把该偏移量简称为 S;之后内核会将该偏移量 S 叠加到 linker 生成的 load 指令的地址 ABC上;因而,内核在履行 load 指令时,不会依照原始的 linker 地址直接载入到内存地址 ABC 中,而是载入到 A+SB+SC+S,咱们能够把这些实践的 load 加载地址称为 Load Address「加载地址」,后文中,Load Address 将简称为 L

【老司机精选】iOS 符号化:基础与进阶

经过了解 ASLR 技能,咱们弄明白了 linker addressload address 之间的差值是 ASLR Slide 随机内存地址散布偏移量;咱们能够得到该公式 ALSR Slide = Load Address - Linker Address, 简化为 S = L - A

【老司机精选】iOS 符号化:基础与进阶

怎么获取实践的 Linker Address 和 Load Address

前面现已说到 otool 能够协助咱们查看二进制文件的 load 指令信息,从而得到 linker address (该地址也能够视为 file address 「文件地址」) 而取得运转时内存地址中的 Load Address,能够经过溃散日志中的 Binary Image 列表,Instruments 供给的仓库,或者经过 vmmap 指令行东西来获取;具体怎么运用 vmmap 在后文中会有解说

【老司机精选】iOS 符号化:基础与进阶

核算 ASLR Slide 随机内存偏移量

结合实践,咱们需求知道 ASLR Slide 随机内存偏移量,才能够从溃散日志和 Instruments 仓库中的内存地址,减去 ASLR Slide 而取得文件地址;因而需求先核算出 ASLR Slide,核算 ASLR Slide 一般以特定段(如 __TEXT)的 load addresslinker address 来相减得出,怎么获取这俩地址上面现已说了,结合实践咱们从溃散日志中获取了 __TEXT 二进制段的 load address0x10045c000 ; 经过 otool 我能够取得 __TEXT 二进制段的 linker address0x100000000 ;将这两者相减咱们就能够的得到 ASLR Slide = 0x45c000

【老司机精选】iOS 符号化:基础与进阶

有了 ASLR Slide ,咱们能够从溃散日志的运转时内存地址,换算出磁盘地址空间中的文件地址,如下图所示,咱们能够得到咱们 demo 中溃散的仓库的文件地址为 0x10003b70 ,有了文件地址,咱们能够用来查看源码,这个后续再说。咱们先继续探索一下其他核算 ASLR Slide 的姿态

【老司机精选】iOS 符号化:基础与进阶

如下图所示,otool 指令行东西能够用来查看溃散时产生问题的指令信息, 传入 -tV 能够输出汇编仓库;-arch arm64 是为了让 otool 正确处理 Universal 2 技能编译的产物;输出结构对应上述文件地址,显现此是 brk 指令,汇编中的 brk 一般代表着 App 呈现了异常或问题;

【老司机精选】iOS 符号化:基础与进阶

atos 指令行东西也能够帮咱们核算 ASLR Slideatos-o 指令会输出 file segment address-l 指令会输出 load address;

【老司机精选】iOS 符号化:基础与进阶

除了 atosotool ,还有 vmmap 指令行东西也能够协助咱们获取 load address ,咱们能够用 vmmap 来验证上面的核算成果, vmmap 输出溃散时 __TEXT segmentload address ,运用之前公式能够核算出本次运转的 ASLR Slide0x104d14000 ,将本次溃散日志中的 runtime address - ASLR Slide 得到了 file address0x100003b70 ,和之前核算的 file address 相同;

【老司机精选】iOS 符号化:基础与进阶

上述两次不同运转时, 不同溃散日志,不同的 ASLR Slide 能够得到同一个 file address ,这不是巧合;是由于内核每次运转的 ASLR Slide 都不同,因而不一起间,不同设备的溃散日志中所对应的内存地址会改变,但实践的 linkder address 是相同的;根据此,尽管内存地址每次改变,咱们依然能够定位到相同的 file address至此,咱们发现了一种机制,能让我在随机的运转时内存中,定位到咱们 App 源码等级所产生的的事;经过这种映射机制能够让咱们从运转时的仓库信息中,回溯到 App 源码中;

【老司机精选】iOS 符号化:基础与进阶

小结 – 从内存地址回溯到文件地址

以上内容便是「符号化两步走」中的第一步:从内存地址回溯到文件,总结一下该进程中的内容和东西

  1. App 和 库的二进制文件格局是 Mach-O ,其间 Mach-O 的头中存放了二进制段的相关信息和 load 指令,这些二进制段是 linker 创建的,其间包括了二进制段的地址信息 linker address
  2. otool -l 能够协助咱们输出 Mach-O 中指定二进制段的地址和特点信息,其间包括 linker address
  3. 溃散日志中的 binary image 列表中能够获取溃散产生时的 load address
  4. vmmap 也能够取得正在运转 Appload address
  5. ASLR Slide + Linker address = Load address
    【老司机精选】iOS 符号化:基础与进阶

第二步 – 剖析调试信息

有了以上基础,咱们能够进一步评论符号化的第二步:剖析调试信息;调试信息一般包括了 file address 和源码之间的联系信息;Xcode 会在编译时生成这些联系信息并存放为 dSYM 文件,也能够把这些联系信息内置在二进制编译产物中;

【老司机精选】iOS 符号化:基础与进阶

这些调试信息有三种类型,每一种都供给了不同等级与 file address 相关的调试信息;

  1. Function starts
  2. Nlist symbol table
  3. DWARF

下图中展现了这三种东西别离供给了对应维度的调试信息

【老司机精选】iOS 符号化:基础与进阶

Function Starts

从上图中可知,function starts 相较于其他东西供给最少的信息,该东西只能供给函数对应的开端地址,具体而言,function starts 会供给函数的开端地址和其调用的地点的地址;但这其间不会告知你这调用地址里是否有其他函数,他只能告知你这里有个函数出问题 ;

【老司机精选】iOS 符号化:基础与进阶

function starts 经过编码 __LINKEDIT 二进制段中的 linker 地址列表来供给该功能; function starts 根据直接内置在 App 编译产物中,经过 mach-O 文件的 load 指令的 LC_FUNCTION_STARTS 来描绘 function starts

【老司机精选】iOS 符号化:基础与进阶

实践中,能够经过 symbols -onlyFuncStartsData 指令行东西来输出 function starts 相关信息,如下图所示,其间的 null 是由于 function starts 不供给函数称号,所以用 null 来做函数称号的占位符;

【老司机精选】iOS 符号化:基础与进阶

根据 function starts 咱们能够对未符号化的溃散日志进行处理,先从溃散日志的内存地址 0x10045fb70 减去之前核算好的 ASLR Slide 0x45c000得到 file address 0x100003b70; 然后结合 function starts 输出成果,咱们发现只要第一个地址 0x100003a68 小于咱们算出的 file address 0x100003b70,所以只要这第一个地址包括了过错产生的地址; 根据此咱们核算这两个地址之间偏移了 0x108,换算成十进制 是 264,也便是咱们 file address 与实践过错产生地址之间有 264 字节的偏移量;

【老司机精选】iOS 符号化:基础与进阶

至此 function starts 能够协助咱们了解溃散日志中的函数怎么被设置,修改了哪些寄存器;但由于 function starts 不供给函数名,咱们只能在初级的机器码层面来剖析这些过错日志,关于调试开发 App 来说挺有用,但关于剖析过错日志,咱们还需求其他东西;

Nlist symbols List – Nlist 符号表

nlist 是一个结构体,他具体结构如下图所示,nlist 符号表建立在 function starts 和一个编码后的 __LINKEDIT segment 的信息列表,当然 nlist 有自己的 load 指令;与 function starts 不同的是 nlist 不只是编码内存地址,他在其结构体中编码了更多信息; 如下图所示,nlist 结构体中包括了称号和其他几个特点,具体而言 nlist 的类型由 n_type 所决议

【老司机精选】iOS 符号化:基础与进阶

n_type 有三种类型是咱们符号化所感爱好的,这里咱们先着重聊一聊其间两种; 第一种是 direct symbole – 直接符号;直接符号相关的是在 App 和二方库中,包括了已被完好界说的办法和函数;直接符号在 nlist_64 结构体中存储了函数姓名和函数文件地址;

Nlist 直接符号

n_type 中的指定二进制位的值决议了该 nlist 的类型,具体而言,n_type 中的第二、三、四的二进制位为 1 时,表明该 nlist 类型为直接符号,这三个位的组合还被叫做 N_SECT

【老司机精选】iOS 符号化:基础与进阶

咱们能够经过 nm -defined-only —numberic-sort 指令行东西来查看 N_SECT;在这里 nm 遍历了 magicNumbers App 的拟定符号,并以地址次序罗列出来,具体参照下图中的输出;注意此处咱们还是用了 xcrun -swift-demangle 来解析 Swift mangling 后的函数称号;

【老司机精选】iOS 符号化:基础与进阶

上图所示,咱们现已能够从成果中取得了办法名 numberChoices()、类名 MagicNumbers、文件名 main;这是由于这些信息直接在 App 内界说; symbols 查看直接符号 和 nm 东西相似, symbols 指令行东西也供给查看 nlist 数据的办法,而且支持主动 demangle ,具体如下图

【老司机精选】iOS 符号化:基础与进阶
以上两个办法,让咱们从溃散日志中的内存地址,相关到了源码中的具体函数称号,至此,溃散日志的符号化的信息丰厚程度更进一步; 至此,咱们经过 fuction starts 供给的函数进口偏移地址从 direct symbols 中匹配到一个函数进口,而且这个进口有姓名,把这些信息放在一起,咱们能够发现 crash 产生在 main 办法地址的 264字节偏移处;但 main 并不是溃散中仅有的函数,这表明咱们还有更多的信息有待发掘;例如咱们还没有弄清楚代码中的行数信息
【老司机精选】iOS 符号化:基础与进阶

咱们现已弄清 main 并不是仅有与溃散相关的函数,咱们还有更多的信息有待发掘;例如咱们还没取得文件的行数信息;而且在上述符号化中,部分函数被序列化,还有部分仓库和溃散日志信息没有被符号化

【老司机精选】iOS 符号化:基础与进阶

咱们在 Instruments 的仓库中遇到了相似的状况,一些函数名被符号化而可读,但部分仍是内存地址;产生这种现象的原因是,直接符号表中所包括的函数,只限于在链接时被直接链接的部分,动态库等运转时加载的二进制文件不被包括在内,这些未能符号化的办法便是跨模块从动态库中调用的办法;咱们需求其他手法了符号化这些调试信息;

【老司机精选】iOS 符号化:基础与进阶

这种直接符号表的逻辑,有助于削减编译产物体积;究竟换位考虑,假如把打包时全部相关函数信息都存入符号表,这种操作才有违常识;关于 FrameworksLibraries,咱们需求处理记载那些被调用的办法,而剥离没用到的;当然了假如把直接符号表里的主程序内的函数剥离,那符号表里啥也不剩了;

【老司机精选】iOS 符号化:基础与进阶

Xcode 编译设置对 nlist 直接符号的影响

Xcode 的编译设置中,strip 装备项有 strip linked productstrip stylestrip swift symbols 三个选项。这些编译设置的选项操控了 App 在编译链接进程中的剥离剩余符号表的逻辑;具体来讲,strip linked productYES 时,二进制文件中将依据 strip style 的值进行符号表剥离;举例来说,strip style 值为 all symbols 时,符号表中将履行最激进的剥离战略,终究符号表中只包括最核心的办法;Non globals 类型会剥离运用中不同模块中共同运用的直接符号,但会留下用于其他 APP 中的符号;Debugging symbols 则删除了第三种 nlist 类型的符号,这个后续评论 DWARF 时会讲到,但该类型的剥离会保存直接用到的符号。

【老司机精选】iOS 符号化:基础与进阶

【老司机精选】iOS 符号化:基础与进阶

举例来说,这里有一个界说了两个 public interface接口和一个 internal shared 完结的办法的 framework ,由于全部这些函数在链接环节中有用,他们都具有直接的符号项。

【老司机精选】iOS 符号化:基础与进阶

假如我依照 non globals 进行剥离,那只要两个 interface 会留下;由于同享完结的函数只在 framework 内运用,所以它不是大局的,从而也不会被放入符号表;

【老司机精选】iOS 符号化:基础与进阶
相似的假如是 all symbols 剥离战略不时,假如这两个 interface 有被 framework 外部所调用时,他们依然会被留下;
【老司机精选】iOS 符号化:基础与进阶

symbols —onlyNListData 会输出一些散布在直接符号之间 function starts 的条目;这些条目也表明了函数是存在于直接符号表中,亦或是现已被剥离了。你能够运用这些剥离设定,来完结你需求的符号表可见性;有了这些信息,咱们就能够确定什么时分需求直接符号表。在实践运用中,有时分咱们能符号化出函数名,但没有具体行数和文件名;或者符号化成果包括了办法名和办法开端地址,正如此处 frameworksymbols 指令的比如;

【老司机精选】iOS 符号化:基础与进阶

直接符号 – Indirect symbols

与直接符号相似,直接符号的 n_type 的第一位二进制位为 1 ,或称为 n_EXT

【老司机精选】iOS 符号化:基础与进阶

经过 nm -m -arch arm64 -undefined-only --numberic-sort MagicNumbers 输出直接符号的信息;这其间运用 —undefined-only 来替换 —defined-only ,该指令用于查看直接符号; -m ,这能够让你看到这些办法源自哪个 frameworklibraries。下面图中的输出成果提示 MagicNumbers App 依靠了 libSwiftCore 中的一系列 Swift 基础办法如 print()

【老司机精选】iOS 符号化:基础与进阶

####小结 – Function starts 与 nlist 符号表 文章开头,咱们约定了要评论 function startsnlist 符号表DWARF 三种符号化东西;截止现在现已评论了前两种,在此回顾一下;

  • Function starts 能供给地址列表,短少办法名,能够协助核算溃散对应的文件地址偏移量;
  • Nlist 符号表把相关到一个地址的具体信息构成结构体存储,nlist 符号能供给函数称号,还能够描绘在 App 内界说的直接符号和在二方库中供给的直接符号;直接符号表一般保存与链接有关的函数,Xcode 项目设置中的 strip build style 会影响直接符号表中的内容;
  • 这两种符号表都直接嵌入在 App 二进制文件 Mach-O 头中的 __LINKEDIT 二进制段中
    【老司机精选】iOS 符号化:基础与进阶

DWARF

截止现在咱们还没能看到诸如文件名、函数地点行数、溃散地点行数等符号化信息;这些信息在 DWARF 中都有供给,咱们在此具体评论一下 DWARF ; 相较于 nlist 符号表只保存函数部分信息,DWARF 几乎记载了函数的全部上下文信息;回顾 function starts 只在一个维度上供给偏移量信息;nlist 根据编码 nlist_64 结构体将调试信息升级到两个维度,即地址信息和函数称号;作为比较 DWARF 增加了第三个维度:联系信息;实践项目中函数不是孤立存在的,函数会被调用和在其内部调用其他函数,函数会有出参入参;经过记载这些函数的上下文联系信息;DWARF 会带咱们解锁符号化最牛逼的姿态;

【老司机精选】iOS 符号化:基础与进阶

当咱们剖析 DWARF 时,一般指的是引用剖析一个 dSYM bundle,该 bundle 中存在由元数据组成的 plist,还包括一个 DWARF 二进制文件;二进制文件中将 DWARF 的信息记载在 __DWARF 二进制段中;DWARF 在该二进制段中记载了咱们需求重视的三个数据流;具体而言三个数据流别离是 debug_info, debug_abbrev, debug_linedebug_info 包括了原始数据,debug_abbrev 为原始数据进行了结构化处理,debug_line 包括了文件名和行号;除此之外 DWARF 还界说了需求评论的两种 vocabulary list 词汇表:compile unit 编译单元和 subprogram 子程序;后文会说到第三种词汇表 – 内联子程序

【老司机精选】iOS 符号化:基础与进阶

Compile Unit – 编译单元

编译单元表明了在项目中会被编译的单个源码文件;具体来说,在项目中的每个 swift 文件都会有一个编译单元与之对应;DWARF 为每一个编译单元赋予了一些特点,诸如文件名、模块称号、__TEXT segment 的函数占位部分等;main.swift 文件对应的编译单元在 debug_info 数据流中贮存了这些特点,如左侧所示;与之对应的,在 debug_addrev 数据流中包括了一个相关的条目,这些条目告知咱们这些值代表了什么,如右侧所示;咱们看到图中右侧包括了文件名、语言和一个 low/high 对,用来表述 __TEXT segment 的规模

【老司机精选】iOS 符号化:基础与进阶

Subprogram – 子程序

子程序表明已被界说的函数;咱们现已在 nlist 符号表中找到过已界说的办法,但子程序还能够用来描绘静态办法和本地办法;子程序当然也有自己的称号和对应的 __TEXT segment 地址开端规模

【老司机精选】iOS 符号化:基础与进阶

DWARF 联系树

编译单元和子程序之间的一个基本联系是,子程序是在编译单元中被界说的;DWARF 运用树来表述这种联系;编译单元在根节点上,子程序是根节点的孩子节点;这些子节点能够经过他们的地址规模而被检索到;

【老司机精选】iOS 符号化:基础与进阶

咱们能够经过 dwarfdump 指令行东西来验证上述 DWARF 的编译单元、子程序和联系树细节 首要咱们将查看到一个编译单元,这句之前说到的编译单元所带着的特点相吻合(文件名、语言、行数等),dwarfdump 东西结合了 debug_infodebug_abbrev 内容来展现 dSYMs 文件中的数据结构与内容

【老司机精选】iOS 符号化:基础与进阶

输出很长,咱们往下看,会看到一个子程序 subprogram;它所占用的地址规模存在于该编译单元的地址规模内,而且能够看到办法名;之前说到过 DWARF 十分具体的描绘符号表和联系信息,咱们不会在深化探究 DWARF 的联系树 规划细节,但了解这些细节能够协助咱们了解符号化背面的逻辑;

【老司机精选】iOS 符号化:基础与进阶

继续往下看输出成果,会发现其间还包括参数信息,DWARF 持有一个自己的词汇表,来描绘参数的称号和类型;参数是子程序的一个子节点;下图中的输出,能够发现 numberofChoice 函数的参数 choices 的相关信息; 文件名与行数信息

【老司机精选】iOS 符号化:基础与进阶

此外,debug_line 数据流中存储了函数相关的文件名和具体行数;但 debug_line 数据流不是树状结构,相反的,该数据流界说了一个 line table program 行表程序,这个航标程序能够让链接后的文件地址映射到源码文件中的具体行数;咱们能够运用这个行表程序来查找文件地址相关的具体源码和行数;

【老司机精选】iOS 符号化:基础与进阶

综上,根据 debug_info 的树状结构和 debug_line 的行表程序,咱们能够得到一个下面的结构;经过遍历这棵树,咱们能够找到想要的文件地址;首要从编译单元开端,遍历其子节点,然后挑选出包括 debug_line 的子节点;

【老司机精选】iOS 符号化:基础与进阶

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

【老司机精选】iOS 符号化:基础与进阶

咱们或许会猜,这 -i 意味着什么;事实上 atos-i 意味着 inlined function 内联函数,内联化是一种编译器履行的惯例优化;具体而言,内联化便是在编译中把函数的完结代码直接替换函数被调用的代码;这样的替换操作能够让函数调用的代码和函数的界说代码都「消失了」; 在咱们的 Demo 中也便是运用 numberOfChoice() 的完结代码替换了调用代码;numberOfChoice() 调用代码不见了~

【老司机精选】iOS 符号化:基础与进阶

Inlined subroutines – 内联子程序

DWARF 运用内联子程序来表述这种编译时内联优化;这便是咱们要评论的第三种 vocabulary list 词汇表类型 :inlined subroutines 内联子程序;内联子程序是子程序的一种,所以他也是一种办法,一种被内联到另一个子程序的办法;所以内联函数在 DWARF 联系树中是子程序的一个子节点;这样的界说意味着会呈现递归联系;也便是说一个内联子程序能够有其他内联子程序作为子节点;

【老司机精选】iOS 符号化:基础与进阶
再次运用 dwarfdump 指令行东西,咱们能够来查看一下 DWARF 中的内联子程序;这些内联子程序被列为其他节点的子节点,而且有着与子程序相似的特点,诸如称号和地址;但是在DWARF 文件中,这些特点一般会经过一个公共节点来拜访,这种规划叫笼统源;假如存在一个特定函数有许多内联拷贝,则该函数的公共同享特点将存储在笼统源中,如此这些内联函数就不会被重复剩余的拷贝;内联子程序有一个独特的特点是 call site 调用方位;该特点表述了在源码中实践调用函数的方位,编译优化器会替换这些函数调用代码;例如,咱们在 main.swift 文件中第36行调用了 generateANumber() ,这使得需求在树中新增子节点来记载这个函数调用;

【老司机精选】iOS 符号化:基础与进阶

到这里,咱们对 DWARF 符号化有了更全面的了解,如下图所示,咱们对 App 的调用逻辑也有了更宽广的视角。了解内联函数的优化方法和细节是彻底符号化溃散日志的要害地点; -i 指令实践会要求 atos 符号化进程中考虑到上述内联函数;这些内联函数的信息相同在 Instruments 仓库中缺失;咱们在溃散日志和 Instruments 仓库中都需求 dSYM 文件,正是由于 dSYM 中精确地包括了上述三种类型的信息:编译单元、子程序和 DWARF 联系树;

【老司机精选】iOS 符号化:基础与进阶

从库和方针文件中获取 DWARF

除了 dSYM 文件中,还能够在静态库和方针文件中找到 DWARF;也便是说即便没有 dSYM 文件,你依然能够从静态库或方针文件中链接的函数,来生成 DWARF;这种状况下,你会找到调试符号表的 nlist 类型,这些本是能够被 strip 剥离的符号类型之一;但这些 nlist 类型并不直接包括 DWARF,相反,他们直接把函数相关到其源码文件;假如一个库在构建中包括调试信息,此时,这些 nlist 条目能够给咱们供给 DWARF 的相关信息

【老司机精选】iOS 符号化:基础与进阶

上述类型的 nlist 条目能够经过 dsymutil -dump-debug-map 指令行东西来输出和具体查看;在此咱们列出了不同函数办法和他们的出处;这些地址信息能够被扫描并处理成 DWARF 文件中所需的信息;

【老司机精选】iOS 符号化:基础与进阶

小结 – DWARF

  • DWARF 是深度符号化数据的重要来历
  • DWARF 描绘了函数与文件之间的重要联系信息;
  • DWARF 妥当处理了编译时内敛优化的问题;
  • dSYM 文件和静态库能够都可包括 DWARF
  • 实践中推荐运用 dSYM 获取 DWARF,由于从 dSYM 中获取的 DWARF 能够便利的在其他东西中运用,而且 Xcode 许多内置东西也支持 DWARF
    【老司机精选】iOS 符号化:基础与进阶

开发东西与符号化实践

Xcode 编译设置 – Debug info format

  • 针对本地开发装备主张设置为直接生成 DWARF
  • 针对发布编译装备,请保证生成包括 DWARFdSYM 文件
  • 提交至 App Store ConnectApp,你能够在那下载到 dSYM
  • 即便运用了 bitcode 技能 ,你也能够从 App Store Connect 下载到 dSYM 文件
    【老司机精选】iOS 符号化:基础与进阶

查找和承认 dSYM 文件

如下图所示,在本地 Mac 上能够接住 mdfind 指令行东西查看 dSYM 文件;这个字母数字组成的字符串是编译二进制产物的 UUID,也是运转时 load 指令的仅有标识符; 你还能够经过 symbols -uuid 来查看 dSYM 文件的 UUID;

【老司机精选】iOS 符号化:基础与进阶

在少量状况下,编译进程会生成一个无效的 DWARF,你能够经过 draftdump -verify 指令来查验 DWARF 的有效性;假如这个查看指令输出任何过错,请直接经过 feedbackassistant.apple.com 来进行Developer Tool - 开发东西bug 反应;

【老司机精选】iOS 符号化:基础与进阶

单个 DWARF 二进制文件巨细上线是 4GB,假如上述校验中陈述超过 4GB 的过错,你能够考虑将项目的进行组件化拆分,以便每个组件会有一个较小的 dSYM

实践操作中,经过比较 dSYMUUID 和溃散日志中 binary imageUUID 性来匹配两者;除了在溃散日志中查看 App 二进制镜像的 UUID ,你还能够经过 symbols 指令行东西来获取 UUID,参照下图;实践符号化中,需求 dSYM 和溃散日志的 UUID 匹配;

【老司机精选】iOS 符号化:基础与进阶

其他符号化的细节

symbols 指令行东西还能够帮你查看你 App 编译产物中包括的可用调试信息;输出内容的方括号中的标签,告知了这些调试信息的来历;当你不知道在调试时运用哪些调试信息时,运用该指令能够看看有哪些调试信息可用;

【老司机精选】iOS 符号化:基础与进阶

假如你确信现已有可用 dSYM 文件了,但是仍旧未能将 Instruments 中的仓库信息符号化,请查看一下项目的 Entitlements 和代码签名装备;具体来说运用 codesign 指令行东西,你能够验证是否具有正确的代码签名装备;

【老司机精选】iOS 符号化:基础与进阶

一起,你还需求查看本地开发的 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 ,来处理该问题;

【老司机精选】iOS 符号化:基础与进阶

最后,关于运用 Universal 2 技能的 App, 在运用文章中说到的指令行东西时,都能够指定架构,诸如 symbolsotooldwarfdump 都有 -arch 的参数可供装备,如此能够只履行特定架构的相关操作;

【老司机精选】iOS 符号化:基础与进阶

总结

正如称号中的「符号化进阶」,用以下几个要害点来总结本 Session

  • 符号化 UUID 和文件地址是一致且牢靠的方法来识别 App 在运转时的问题,由于这两者不受 ASLR Slide 偏移量的影响;UUID 和文件地址是运转时信息符号化要害的第一步
  • 实践中,尽可能运用 dSYM 完结符号化;dSYMDWARF 的方式记载了最丰厚细节的调试信息,而且被 XcodeInstruments 所良好支持
  • 文中介绍了几款指令行符号化东西,诸如 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 开发者,结合自己的实践开发经历、苹果文档和视频内容做二次创造。