前言

  • 自从抖音团队共享了这篇 抖音研发实践:根据二进制文件重排的处理计划 APP发动速度提升超15% 发动优化文章后 , 二进制重排优化 pre-main 阶段的发动时间自此被咱们广为流传 .

  • 本篇文章首要叙述下二进制重排的原理 , ( 因为抖音团4 T o A # P M q队在上述文章中原理部分大多是点到即止 , 多数朋友看完并没有什么实践收成 ) . 然后将结合 clang 插桩的办法 来实践叙述和演练一下怎样处理抖音团1 C D ; J 2 }队留传下来的& I ~ 9 $ 0这一问题 :

    hook Objc_msgSend 无法处理的 纯swift , block , c++, } J U E N 办法 .

    来到达完美的二进制重排计划 .

( 本篇文章因为会从原理视点解说 , 有些现已比较了解的同学或许会觉得节奏偏烦琐 , 为了照料大部分同学 , 咱们自行根据目录跳过即[ T W可 . )

了解二进制重排之前 , 咱们需求了解一些前导常识 , 以及二进制重排是为了处理什么问题 .

虚拟内存与物理内存

在本篇文章里 , 笔者就不经过教科书或许大多数资料的办法来叙述这个概念了 . 咱们经过实践问题和其对应的处理办法来看这个技能 or 概念 .

在计算机范畴 , 任何一个技能 or 概念 ,y o { J ( z B 都是Y I !为了处理实践的问题而诞生的 .

在前期的计算机中 , 并没有虚拟内存的概念 , 任何运用被从磁盘中加载到运转内存中时 , 都是I 8 X k 7 _ r完好加载和按序排列的 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

那么因而 , 就会呈现两个问题 :

运用物理内存时留传的问题

  • 安全问题 : 因为在内存条中运用的都是实R Q . 4 ! P X在物理地址 , 并且内存= 6 M d Q条中各个运用进程都是按次序顺次排列的 . 那么在 进程1 中经过地址偏移就能够拜访到 其他进程 的内存 .
  • 功率问题 : 随着软件的开展 , 一个软件运转时需求占用的内存越来越多 , 但往往用户并不会用到这个运用的一切功用 , 形成很大的内存糟蹋, R o | a q 2 | , , 而后边翻开的进程往往需求排队等待 .

为了处理上述两个问题 , 虚拟内M F p h Q E F .存应运而生 .

虚拟内存工作原理

引用了虚拟u a i ? & x内存后 , 在咱们进程中以为自己有一大片连续的内存空间实践上是虚拟的 , 也便是说从 0x000000 ~ 0xffffffG g 0 C c 6 { o Q 咱们是都能够拜访的 . 可T y ^ d Q d是实践上这个I V .内存地址仅仅一个虚拟地址 , 而这个虚拟地址经过一张映射表映射后才干够获取到实在的物X ] z T b ] x H R理地址 .

什么意思呢 ?

  • 5 3 @ P & # 6 4践上咱们能够了解为 , 体系对实在物理内存拜访做了一层限制 , 只有被写到映射表中的地址才是被认或许够拜访的 .
  • 例如 , 虚拟地址 0x000000 ~ 0xffffffS } R n G个范围内的任意地址咱们都能够拜访 , 可是这个虚拟地址对应的实践物理地址是计算机来随机分配到内存页上的6 H ; .
  • 这儿说到了实践物理内存分页的概念 , 下面会具体叙述 .

或许咱们也有注意到 , 咱们在一个工程中获取的地址 , 一起在另一个工程中去拜访 , 并不能拜访到数据 , 其原理便是虚拟d 2 b * C r内存 .

整个虚拟内存的工作原理这儿用一张图6 V 1 u T Q # ) ~来展示 :

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

虚拟内存处理进程间安全问题原理

显着 , 引用虚拟内存后就不存在经过偏移能够拜访到其他进程的地址空间的问题了 .

因为每个进程的映射表是独自的 , 在你的进程中随意你怎样拜访 , 这些地址都是受映射表限制的 , & . 6 x 其实在物理地址永远在规则范围内 , 也就不存在经过偏移获取到其他进程的内存空间的问题了 .

并且实践上 , 每次运用被加载到内存中 , 实践分配的物理内存并不一定是固定或许连续的 , 这是因为内存分页以及懒加载以及 ASLRi X 6 所处理的安全问题 .

cw – r 8 t $pu 寻址进程

引进虚拟内存后 , cpu 在经过虚拟内存地址拜访数据的进程如下 :

  • 经过虚拟内存地址 , 找到对应进程的映射表 .
  • 经过映射表找到其对应的实在物理地址 , 从而找到数据 .

这个进程被称为 地址翻译 , 这个进程是由操作体系以及 cpuR 2 K Q c o集成的一个 硬件单元 Mv ( + + VMU 协同来完结的 .

那么安全问题处理了今后 , 功率问题怎样处理呢 ?

虚拟内存处理功率问题

刚刚说到虚拟内存和物理内存经过映射表进行映射 , 可是这个映射并不或许是一一对应的 , 那样就过分糟蹋内存了 . 为了处理功率问题 , 实践上实在物理内存是分页的 .m J Q B K 而映射表相同是以页为单位的 .

换句话说 , 映射表只会映射到一页 , 并不会映射到具体每一个地址 .

linux 体系M G 7 ) U a中 , 一页内存大小为 4KB ,r s 1 i ) Z 在不同渠道或许各有不同 .

  • Mac OS 体系中 , 一页为 4KB ,
  • iOS 体系中 , 一页为 16KB .

] H ~ ^ v %们能够运用 pagesize 指令直接检查 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

那么为什么说内存分页就能够处理内存糟蹋的功率问题呢 ?

内存分页原理

假设当时有两个进程正在运转 , 其状况就如下图所示 :

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

( 上图中咱们也看出* s ? Z Z , 实践物理内存并不是连续以及某个进程完好S { : m 6 P S的 ) .

映射表左侧的 01 代表当时地址有没有在物理7 C 3 : Z ? o u M内存中 . 为什么这么说呢 ?

  • 当运用被加载到内存中时 , 并不会将整个运用加载到内存中 . 只a $ [ Z C y K Z =会放用到的那一部分 . 也便是懒加载的概念 , 换句话说便是运用运用多少 , 实践物理内存9 C _ R j就实践存储多少 .

  • 当运用拜访到某个地址 , 映射表中为 0 , 也便是说并没有被加载到物理内存中时 , 体p H X F ;系就t ` Q [ p L I会立刻阻塞整个进程 , 触发一个咱们所熟知的 缺页中断 - Page Fault .

  • 当一个缺页中断被触发 , 操作体系会从磁盘中从头读取这页数据到物理内存上 , 然后将映射表中虚拟内存指向对应 (o . I 3 x o V @ 假如当时内存已满 , 操作体系会经过置换页算法 找一页数据3 c l T U k L ^进行掩盖 , 这也是为什么开再多的运用也不会崩掉 , 可是之前开的运用再翻开时 , 就从头发动了的根本原因 ).

经过这种分页和掩盖机制 , 就完f K [ 7 I q 7 {美的处理了内存糟蹋和功率问题 .

可是此刻 , 又呈现了一个问题 .

问 : 当运用开发完结今后因为采用了虚拟内存 , 那么其间一个函数无论怎样运转 , 运转多少次 , 都会是虚拟内存中的固定地址 .

什么意思呢 ?

假设运用有一个函数 , 根据首地址偏移量{ z h 9 f u0x00a000 , 那o 9 # $ m h ] l _么虚拟地址从 0x000000 ~ 0xffffff , 根据这个 , 那么这个函数我无论怎样只需求经过 0x00a000 这个虚拟地址就能够拿到其实在完成地址 .

而这种机制就给了许多黑客可操作性的空间 , 他们能够很轻易的提早写好程序获取固定函数的完成进行修正 hoe Q P R Dok 操作 .

为了处理这个问题 , ASLR 应运而生 . 其原理便是 每次 虚拟地址在映射8 L 8 c h e实在地址之前 , 增加一个随机偏移值 , 以此来处理咱们刚刚所说到的这个问题 .

( Android 4.0 , Apple iOS4.3 , OS X Mountain Lion10.8 开端全民引进 ASLR 技能 , 而实践上自从引进 ASLR 后 , 黑客的门槛也自此被拉高 . 不再是人人都可做黑客的年代了 ) .

至此 , 有关物理内存 , 虚拟内存 , 内存分页的完好流程8 . : % z 2 x : (和原理 , 咱们现已叙述结束了 , 那么接下来来到要点 , 二进制重排 .

二进制重排

概述

在了解了内存分页会触发中断反常 Page Fault 会阻塞进程后 , 咱们就知道了这个问题是会对功能产生影# D ^ h ! Q +响的 .

实践上在 iOS 体系中 , 对于出产环境的运用 , 当产生缺页中断进行从头加载5 9 { v 7 r ]时 , iOS 体系还会对其做一次签名验证 . 因而 iOS 出产环! q t境的运用 page fault 所产生的耗时要更多 .

抖音& , u L Y s )团队共享的一个 Page Fault,开销在 0.6 ~ 0.8ms , 实践测验发现不同页会有所不同 , 也跟 cpu 负荷状况有关 , 在 0.1 ~ 1.0 ms 之间 。

当用户运用运用时 , 榜首个_ , n j 3 5 u直接印象便是发动 app 耗时 , 而恰巧因为发动时期有许多的类 , 分类 , 三方 等等需求加载和履行 , 多个 pageS ? I 8 ] ! ? . fault 所产生的的耗时往往是不能小觑的 . 这也是二进制重排进行发动优化的必要性 .

二进制重排优化原理

假设在发动时期咱们需求t – i / | H 7 m o调用两个函数 method1method4 . 函数编译在 mach-o 中的方位是根据 ld ( Xcode 的链接器) 的编译次序并非调用次序来的 . 因而很或许这两个函数分布在不同的内存页上 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

那么发动时 , page1page2 则都需求从无到有加载到物. G ( / ] D }6 Q N C E内存中 , 从c A [ N 1 # &而触发两X x 5 r 6page fault} l # t k 3 ) .4 * 4 | : n L S

而二进制重排的做法便是将 method1method4 放到一个内存页中 , 那么发动时则只需求加载 page1 即可 , 也便是只触发一次 page6 | | 0 = * E 4 fault , 到达优化意图 .

实践项目中的做法是将发动时需求调用的函数放到一起 ( 比方 前10页中 ) 以尽或许削减 page fault , 到达优化意[ u t x ( a v z e . 而这个做法就叫做 : 二进制重排 .3 w g p : 5

讲到这儿相信许多同学现已迫不及待的想要看看具体怎样二进制重排了 . 其实操作很简单 , 可是在操作之前咱们还需求知道这几点 :

  • 怎样检测 page fault : 首要咱们要想看到优化作用 ,x a r 2 * A 就应该知道怎样检查 page faulb ; _t6 2 @ , 以此来协助咱们检查优化S ~ N r m p } /前以及优化后的作用 .

  • 怎样重排二进制 .

  • 怎样检查自己重排成功了没有 ?

  • 怎样检测自己发动时刻需求调用的一切办法 .

    • hook objc_MsgSend ( 只能拿到 oc 以及 swift 加上 @objc dynamic 润饰后的办法 ) .
    • 静态扫描 macho 特定段和节里边所存储的符号以及函数数据 . (静态扫描s d : D , 首要用来获取 load 办法 , c++ 结构(有关 c++ 结构 , 参阅 从头整理B i v L dyld 加载流程 这篇文章有具体叙述和演示 ) .
    • claM o b 0 ] $ng 插桩: L X – ( 完美版别 , 完全拿到 swift , oc , c , block 悉数函数 )

内容许多 , 咱们一项一项来 .

怎样检查 page fault

提示 :

假如想检查实在 page fault 次数 , 应该将运用卸载 , 检查榜首次运用装置后的作用 , 或许先翻开4 Q I ^许多个其他运用 .

因为之前运转过 app , 运用其间一部G R F 9 z Z _分现已被加载到物理内存并做好映射% 8 B v ] 表映射 , 这时再发动就会少触发一部分缺页中断 , 并且杀掉运用再翻开也是如此 .

其实便是希望将物理内存中之前加载的掩盖i : 6 ( } d D v J/整理掉 , 削减差错 .

  • 1️⃣ : 翻开 Instruments , 挑选 System Trace .
    iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
  • 2️⃣ : 挑选真机 , 挑选工程h & q N x ; B 1 , 点击发动 ,K d ~ L W 当首个页面加载出来点击停止 .
    这儿注意 , 最} # w J D x好是将运用杀掉从头装置w 5 H { j _ , 因为冷热发动的界定其实因为进程的原因并不一定后台杀掉运用从头翻开便是冷发动 .

    iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
  • 3️⃣ : 等待分析完结 , 检查缺页次数
    • 后台杀掉重启运用
      iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
    • 榜首次装置发动运用
      iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

当然 , 你能够经过增加 DYLD_PRINT_STATISTICS 来检查 pre-main 阶段总m H O R ! ` 7 6 r耗时来b I o Z做一个旁边面辅证 .

咱们能够分别[ C } u ? 0 b ]测验以下几种状况 , 来w O P ~ t X y ; )深度了解冷发动 or 热发动以及物理内存分页掩盖的实践状况 .

  • 运用榜首次装置发动
  • 运用后台没有翻开时发a – g O 2 ?
  • 杀掉后台后从头发动
  • 不杀掉后台从头发动
  • 杀掉后台后多翻开一些其他运用再次发动

二进制重排具体怎样操作

说了这么多前导常识 , 总算要开端做二进制重排了 , 其实具体操作很简单 , Xcode 现已供给好这个机制 , 并且 libobjc 实践上也是用了二进制重排进行优化 .

参阅下图

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
  • 首要 , Xc; k L qode 是用的链接器叫做 ld , ld 有一个参数叫 Order File , 咱们能够经过这个参数装备一个 order 文件的途径 .
  • 在这个 order 文件中 , 将你需[ V y 0 8 P v求的符号按次序写在里边 .
  • 当工程 build 的时分 , Xcode 会读取这个文件 , 打的二进制包就会依照这个文件中的符号次序进行生成对应的 mach-O .
iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

二进制重排疑问 – 题外话 :

  • 1️⃣ : order 文件里 符号写错了或Z | ` . f Z许这个符号不存在会不会有问题 ?

    • 答 : ld 会忽略这些符号 , 实践上假如供给了 link 选项 -order_file_statisticsX Q j 7 $,会以 wd 5 1 t I f t )arning 的办法把这些V x b H K $没找到的符号打印在日) ( 8 z M y r志里。 .
  • 2️⃣ : 有部分同学或许会考虑这种办法会不会影响上架 ?

    • 答 : 首要 , objc 源码自己也在用这种办法 .
    • 二进制重排仅仅从头排列了所生成的 macho 中函数表与符号表的次序 .

怎样检查自己2 C = }工程的符号次序

重排前后咱们需求检查自己的符号次序有没有修正成功 , 这时分就用到了
Link Map .

Link Map 是编译期间产生的产物 , ( ld 的读取二进制文件次序默认是依照 Compiley * & Z 8 E SourcesGUI 里的次序 ) , 它记录了二进制文件的布局 . 经过设置 Write Link Map File 来设置输出与否 , 默认是 no .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

修正结束后 clean 一下 , 运转工程 , Productsshow in finder, 找到 macho 的上上层目录.p m ^ / N ?

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

按下图顺次找到最新的一个 .txt 文件并翻开.

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

这个文件中就存储了一切符号的次序 , 在 # Symbols: 部分 ( 前面的 .o 等内容忽略 , 这部分在笔者后续叙述 llvm 编译器华章会具体解说 ) .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

能够看到 ,– ! m % j 这个符号次序显着是h e B依照 Compile Sources 的文件次序来排列的 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

提示 :

上述文件中最左侧地址便是 实践代码地址而并非符号地` t n = , 因而咱们二进制重排并非仅仅% ~ . p Y V p %修正符号地址 , 而是运用符号次序 , 从头排列整个代码在文件的偏移地址 , 将发动需求加载的办法地址放到前面内存页中 , 以此到达削减 page fault 的次数从而完成时间上的优化 , 一定要清楚这一点 .

你能够运c 1 ! ? E !MachOView 检查排列前e c s 9 * I V ~ s后在 _text( 代码段 )6 v i V @ j u的源码次序来协助了解 .

实战演练

来到工程根目录 , 新建一个文件 touch lb.order . 随意挑选几个发动时就需求加载的办法 , 例如我这儿选了以下几个 .

-[LBOCTools lbCurrentPresentingVC]
+[e ( Q H e h k KLBOCTools lbGetCurrentTimes]
+) _ %[RSAEncryptor s F z N 4 wtripPublicKeyHeader:]

写到该文件8 M 7 g n中 , 保存 , 装备文件途径 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

从头运转 , 检查 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

能够看到 , 咱们所写的这三个办法现已被? p P放到最前面了 , 至此 , 生成的 macho 中距离首地址偏移量最小的代@ A ( 2 B码便是咱y @ {们所写的这三个办法r ] e , 假设这三个办法原本在不同的三页 , 那么咱们就现已优化掉了两个 page fault.

错误提示

有部分同学或许装备完运转会发现报错说can't open 这个 order file . 是因为文件格局的问题 . 不必运用 mac 自带的文本编辑 . 运~ m & * l o ) $用指令东西 touch 创建即可 .

获取发动加载一切m X @ y p S ` X 9函数的符号

讲到G 1 } a N t l _ G这 , 咱们就只差一个问题了 , 那便是怎样知道我的项目发动需求调用哪s ~ 7 y 5 2 i : s些办法 , 上述华章中咱们也有略微说到一点 .

  • hook objb D t K jc_MsgSend ( 只能拿到 oc 以及 swi] 2 +ft @objc dynamic 后的办P | 1 J $ n 8 T法 , 并且因为可变参数个数 , 需求用汇编来获取参数 .)
  • 静态扫描 macho 特定段和节里边所存储的符号以及函数数据 . (静态扫描 , 首要用来获取 load 办法 , c++ 结构(有关 c++ 结构 , 参阅 从头整理 dyldn B 4 z u q ! O } 加载流程 这篇文章有具体叙述和演示 ) .
  • clang 插桩 ( 完美版别 ,# f Z R S 4 完全拿到 swift , oc , c , block 悉数函数 ) .

前两种这儿咱们就不在赘述了 . 网S m R $ d Z上参阅资料也较多 , 并且完成作用也并不是完美状况 , 本文咱们来谈谈怎样经过编译期插桩的办法来 hook 获取一切的函数符号 .

cla? + ` [ mng 插桩

关于 clang 的插桩掩盖的官方文档如下 : clang 自带代码掩盖东西
文档中有具体概述 , 以及简略 Demo 演示 .

考虑

其实 clang 插桩首要有两个完成思路 , 一是自己编写 clang 插件 ( 自界说 clang 插件在后续底层篇 llvm 中会带着咱们来手写一个自己的插件 ) , 另外一个便是运用 clang 本身现已供给的一个东西 or 机制来完成咱们h y 8获取n D . 9 n O一切符号的需求 .
本文咱们就依照第二种思路来实践演练一下 .

原理探究

新建一个工g $ :程来测验和运用一下这个静态插桩代码掩盖东西的机制和原理 .
( 不想看这个进程的自行跳到静态插桩原理总结章节 )

依照文档指示来走 .

  • 首要 , 增加编译设置 .

直接搜索 Ov X / # _ther C Flags 来到 Apple Clang - Custom Compiler Flags 中 , 增加

-fsan* U v ritize-coverage=tracem v 0 P 8 v 0-pc-guard
  • 增加 hook 代码 .
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;  // Counter for the guards.
if (start == stop || *start) return;  // Initialize only once.
printf("Iv 1 2NIT: %p %pn", start, s_ { t _ : Ptop);
for (uint32_t *x{ ` 6 J $ = start- R h; x < stop; x++)
*x = ++N;  // Guards should start from 1.
}
voia Z ] p /d __sanitizer_cov_trace_pc_guard(uint32_t *guardu q v e S u e) {
if (!*guard) return;  // Duplicate the guard check.
void *PC = __builtin_return_address(0);
char PcDescr[1024];
//__sanitizep [ 7 } w m K wr_symbolize_pc(PC, "%p %F %L", PcDL h F (escrU _ } p u 9 T F s, sizeof(PcDescr));
printf("gua2 @ krd: %p %x] , o r PC %sn", guard, *guard, PcDescr);
}

笔者这儿是写在空工程的 ViewController.m 里的.

  • 运转工程 , 检查打印
    iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

代码命名 INIT 后边打印的两个w ] C D { n指针地址叫 startstop . 那x , n么咱们经过 lldb 来检查下从 startstop 这个内存地址里边所存储的到底是啥 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

发现存储的是从 114 这个序号 . 那么咱们来增加一个 oc 办法 .

- (void)testOCFunc{
}

再次运转检0 k ; Y J 3 Y p查 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

发现从 0e 变成了 0f . 也便是说存储的 114 这个序号变成了 1q = ; e @ T U15 .

那么咱们再增加一个 c 函数 , 一个 block , 和一个接触屏幕办法来看下 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

相同发现序号顺次增加到了m # , 18 个 , 那么咱们得到一K M T – = j = q个猜想 , 这个内存d N g区间保存的便是工程一切符号的个数 .

其次 , 咱们在接触屏幕办法调用了 c 函数 , c 函数中调用了 block .~ | } R 那么咱们点击屏幕 , 发现如下 :

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

发现咱们实践调用几个办法 , 就会打印几次 guard : .

实践上就相似咱们埋点计算所完成的作用 . 在接触办法增加一个断点检查汇编 :

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

经过汇编咱们发现 , 在每个函数调用的榜首句实践代码 ( 栈平衡与寄存器数据预备在* n l外 ) ,S Q 0 被增加进去t 8 p M K了一个 bl 调用到 __sanitizer_cov_trace_pc_guard 这个函数中来 .

而实践上这也是静] { y态插桩的原理和称号由来 .

静态插桩总结

静态插桩实践上是在编译期就在每一个函数内部二进制源数据+ 5 Z 5 { u : 2增加 hI z 0 P 4 4 -ook 代码 ( 咱们增加的 __sanitizer_cov_trace_pc_guard 函数 ) 来完成全局的办法 hook 的作用 .

疑问

或许有部分同学对我上述表述的原理总结有些疑问 .

究竟是直接修正二进制在每个函数内部都增加了调用 hook 函数这个汇编代码 , 还是仅仅相似于编译器在所生成的二进制文件增加了一个符号 , 然后在运转时假如有这个符号就会自动多做一步调用 hook 代码呢 ?

笔者这儿运用 hopper 来看下生成的 may y u ; ch-o 二进制文6 A M / Q _件 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

上述/ % p J +二进制源文件咱们就发现 , 的确是函数内部 一开端就增加了 调用额定办法的汇编代, & e q Q L码 . 这也是咱们为什么称其为 ” 静态插桩 “ .

讲到这i n S儿 , 原理咱们大体上了解了 , 那么到底怎样才干拿到函数的符号呢 ?

获取一切函数符号

先理一下思路 .

思路

咱们现在知道了 , 一切函数内部榜首步都会去调用 __sanitizer_cov_trace_pc_C 1 [ {guard 这个函数 . 那么了解汇编的同学或许就有# f 6 H o这么个想法 :

函数嵌套时 , 在跳转子函数时都会保存下一条指k U V 7 n令的地址在 X30 ( 又名 lr/ b e 寄存器) 里 .

例如 , A 函数中调用了 B 函数 , 在 arm 汇编中即 bl + 0x**** 指令 , 该指令会首要将下一条汇编指令的地址保存在 x30 寄存器中 ,
然后在跳转到 bl 后边传递的指定地址去履行 . ( 提示 : bl 能完成跳转到某个地址的汇编指令 , 其原理便是修正 pc 寄存器的值来指向到要跳转的地址 , 并且实践上 B 函数中也会对 xJ k B g29 / x30 寄存器的值做维护防i s M h 4止子函数又跳` H ; 0 T c h #转其他函数会掩盖掉 x30 的值 , 当然 , 叶子函数在外 . ) .

B 函数履行 ret 也便是回来指令时 , 就会去读取 x30 寄存器的地址 , 跳转曩昔 , 因而也就回到了上一层函数的下一步 .

这种思路来完成实践上是能够的 . 咱们所写的 __saniF v stizer_cov_trace_pc_guard 函数中的这一句代码 :

void *PC =Y I h N ^ __builtin_return_address(0);

它的作用其实便是去读取 x30 中所存储的要回来时下一条指令的地址 . 所以他称号叫做 __builtin_return_address . 换句话说 , 这个地址便是我当时这个函数履行结束后 , 要回来到哪里去 .

其实 , bt 函数调用栈也是这种思路来完成的 .

也便是说 , 咱们现在能够在 __sanitizer_cov_trace_t m G 0 rpc_guard 这个函数中 , 经过 __bq k G ;uiltin_return_address 数拿到原7 s . ` E a ^函数调用 __sanitizer_cov_i 2 J p $trace_pc_guard 这句汇编代码的下1 + 5 ] ! N $ U一条指令的地址 .

或许有点` M M o ^ @绕 , 画个图来整理一下流程 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

根据内I F [ # $存地址获取函数称号

拿到了函数内部一行代码的地址 , 怎样获取函数称号呢 ? 这儿笔者共享一下自己的思路 .

了解安全攻防 , 逆向的同学或许会清楚 . 咱们为了防止某些特定的办法被别人运用 fishh+ s ` s look hook 掉 , 会运用 dlopen 翻开动态库 , 拿到一个句. : l J g b柄 , 从而拿到函数的内存地址直接调用 .

是不是跟咱们这个流程有点相似 , 仅仅咱们g Q U D n好像是反过来的 . 其实反过来也是能够的 .

dlopen 相同 , 在 dlfcn.h 中有一个办法如下 :

typedef struct dl_info {
const chaL j b N 3r      *dli_fname;     /* 所在文件 */
void            *dli_fbase;     /* 文件地址 */
const char      *dli_sname;     /* 符号称号 */
void            *dli_saddr;     /* 函数起始地址 */
} Dl_info;
//这个函数能经过函数内部地址找到函数符号
int dladdr(const void *, Dl_infT b = ~ _ | J Q Mo *);

紧接着咱们来试验一下 , 先导入头文件#impors I r Z O [ k Qt <dlfcn.h> ,v = A= a ! ) ) a q后修正代码如下 :

voe K V [ [ ; V Gid __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;  // Duplicate the guard check.
void *PC = __builtid 5 zn_return_C ~ ! x j 5address(0);
Dl_info info;
dladdr(PC, &info);
printf("fname=%s nfbase=%p nsnamt z c =  T w ie=%G W , ^ , 2 n ` .snsaddr=%p n",info.dli_fname,ib L G n _nfo.dli_fbase,info.dli_sname,info.dli_saddr);
char PcDescr[1024];S D |
printf("guard: %p %x PCk E ? D - %sn", guard, *guard, PcDescr);
}

检查打印成果 :

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

总算看到咱们要找的符号了 .


收集符号

看到这儿 , 许多同学或许想的是 , 那马上到工程里去拿到我一切的符号 , 写到 order 文件里不就完事了吗 ?

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

为什么呢 ??

clang静态插桩 – 坑点1

→ : 多线程问题

这是一个多线程的问题 , 因2 Q 6 = } 7为你的项目各个办法必定有或许会在不同的函数履行 , 因而* b } M + D __saniti3 B A fzer_cov_trace_pc_guard 这个函数也有或许受多线v v – h d H D 0程影响 , 所以你当然不或许简简单单用一个数组来接纳一切的符号就搞定了 .

那办法有许多 , 笔者在这: ; ) 9 . } j儿共享一下自己的做法 :J , ~ U

考虑到这个办法会来特别屡次 , 运用锁会影响功能 ,
这儿运用苹果底层的原{ 4 L n ? [ V子行列 ( 底层实践上是个栈结构 , 运用行列结构 + 原子性来确保次序 ) 来完成 .

- (void)toucheH V gsBegan:(NSSet<UITouchb 6 j r T n O 1 *> *)touches withEvent:(UIEvent *)event{
//遍历出队
while (tru t , c D @ fue) {
//offsetof 便是针对某个结构体找到某个属性s 3  k [ N # p a相对这个结构体的偏移量
SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(S) c ; M ; 7 O P JymbolNode, next));
if (node == NULL) break;
Dl_info info;
dlC * 3 B 7 A baddr(node->pc, &info);
printf("%s n",info6 0 S F o ) 9.dli_sname);
}
}
//原子行列
static OSQueue9 1 ^ JHead symboList = OS_ATOMIC_QUEUE_INIT;
//界说符号结构体
typedef struct{
void * pc;
void * next;
}SymbolNode;
void __sanitizer_cov_trace_pc_guard(uint8 { W +32_t *guard) {
if (!*guard) return;  // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//入队
// offsetof 用在这儿是为了入队增加下一个节点找到 前一个节点next指针的方位
OSAtom: z , ]  [ FicEnqueue(&symboListZ 1 r P, node, offsetof(SymbolNode, neo Z : = g  pxt));
}

当你l L 4兴致冲冲开端考虑好多线程的处理办法写完之后 , 运转发现 :

死循环了 .

cD T ` # Plang静态插桩 – 坑点2

→ :
上述这种 clang 插桩的办法 , 会在V P w [# U V 3 1 d . = H环中相同刺进 hook 代码
.

当确认了咱们行列@ H s ] 6 y O 3 P入队和出队都是没问题的 , 你自己的/ { = m写法对应的保存和读取也是没问题的 , 咱们发现了这个坑点 , 这个会死循环 , 为什么4 4 K S呢 ?

这儿我就不带着咱们去分析汇编了 , 直接说定论 :

经过汇编会检查到 一个带有 while 循环的办法 , 会被静态加入屡次 __sa? H Anitizec n d e 0 p Hr_cov_trt ~ . / + & ^ace_pc_guard 调用 , 导致死循环.

→ : 处理计划

Other C Flags 修正为如下 :

-fsanitiz?  o & M 6 | ^ Fe-coverage=func,trace-pc-guard

代表进针对 func 进行 hook . 再次运转 .

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

又以为完事了x H ~ ? 还没有..

坑点3 : load 办法

→ : load7 8 b M & 5 t m 办法时 , __sanitizer_l _ ) p + } rcov_trace_pc_guard 函数的参数 guq G 0 T l rard 是 0.

上述打印并没有发! + k ? nload .

1 $ : j – # n 3 =理 : 屏蔽掉 __sanitizer_cov_trace_pc_guard 函数中的

if (!*guard) return;
iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

load 办法就有了 .

这儿也为咱们供给了一点启示:


假如咱们希望从某个函E H & t数之后/之前开端优化 , 经过一个全局静态变量 , 在特定的机遇修正其值 , 在 `__sanitizer_cov_trace_pc_guard` 这个函数中做好对应的处理即可 .

剩余细化工作

  • 假如你也是运用笔者这种多线程处理办法的话 , 因为用的先进后出原因 , 咱们要倒叙一下
  • 还需求做去重 .
  • order 文件d ^ 8 E K 9 @ i格局要求c 函数 , blockP W $ 9 Z ; 调用前面还需求加 _ , 下划线 .
  • 写入文件即可 .

笔者 demo 完好代码如下 :

#import "ViewContr 7 Lroller.h"
#import <dlfcnK 3 }.h>
#import <libkern/OSAtomic.h>
@interface ViewController ()
@end
@implementation ViewController
+ (void)load{
}
- (void)viewDidLoad {
[super viewDidLoad];
testCFunc();
[self testOCFunc];
}
- (void)testOCFunc{
NSLog(@"oc函数");
}
void testCFunc(){
LB! r b q PBlock();
}
void(^1 t B r % QLBBlock)(void) = ^(void){
N4 ; , [ . L WSLog(@"block");@ D -
};
void __sanitizer_cov_trace_pc_G ^ ?guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;  // Counter for the gh g I D e k I Juards.
if (startL 8 ( b ] == stop || *start) return;  // Initialize only once.* x l L i
printf("INIT: %p %f 6 jpn", start, stop);
for (uint3z  V T ] [ : A $2_t *x = start;/ K @ c e - v d N x < stM A .op; x++)
*x = ++N;` m _ q  // Guards should start from 1.
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
ND # bSMutableArray<NSString *> * symbolNames = [Nr r J h Y zSMutableArray array];
while (X 3 w m K t mtrue) {
/w d . ; /offsetof 便是针对某Y O u个结构体找到某个属性相对这U e m个结构体的偏移量
SymbolNode * node = OSAtomicDequeue(&a@ / = % j  , Q ;mp;symboList, offsetof(SymbolNq | l Z x & D 2 aode, next));
if (node == NULL) break;
Dl_info info;n 2 $ R W c z
dladdr(node-&gH B Z ( ( Qt;pc, &infoK S ^ @ });
NSString * name = @(info.dli_sname);
// 增加 _
BOOL isObjc = [name hasPrefiX 0 * d F ? ^ Ax:@"+["] || [name hasPrefix:@"-["];
NSSw K _ jtring * symbolName = isObjc ? name : [@"_" stringByA2 d B UppendingString:name];
//去重
if (![symbolNames contain0 S ? j T J S +sObject:symbolName]) {
[symH s = ? Y + J o bbolNames addObject:symbolName];
}
}
//取反
NSArraM q H @ A ~y * symbolAry = [[symbolNames reversea M ) 4 `ObjectEnumerator] allObjects];
NSLog(@"%@",symbolAry~ ] ?);
//将成果写入到文件
NSString * fuQ z 3 I | N WncString = [symbolAry componentsJoinedBb : f 5 & O D y syString:@"n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
NSData * fileContents = [funcString dataUsiH 7 . | /ngEncoding:NSUTF8StringEncoding];
BOu F s C E Q ^ y :OL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (result) {
NSLog(@"%@",8 X . = Nfile* n mPathR  { f c #);
}else{
NSLog(@"文件写入犯错");
}
}
//原子行列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//界说符号结构体
typedef struct{
void * pc;
void * next;
}SymbolNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *g6 E D b b Ouard) {
//if (!*guard) return;  // Duplicate the guard ch0 P jeck.
void *PC = __builtin_ret^ h a ^ Kurn_addressM S n L(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (Symbo4 W T $ _ 4 J $lNode){PC,NULL};
//入队
// offsetof 用在这儿是为了入队增加下一个节点找到 前一个节点next指针q # $ 7的方位
OSAtomic0 d D g m O N R Enqueue(&symboList, node, offsetof(SymbolNode, next));
}
@end

文件写入到了 tmp 途径下 , 运转 , 翻开手机下载检查 :

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

搞定 , 小伙伴们就能够立马去* u M优化自己的工程了 .

sw( } W ; g . i ~ /ift 工程 / 混编工程问题

经过如上办法适合纯 OC 工程获取符号办法N a B O G ] ( .

因为 swift 的编译器前端是自己的 swift 编译前端程序 , 因而装备稍有不= + = 0 # f C d同 .

搜索 Other Swift Flags , 增加两条装备即可 :

  • -saniu ( ; * I P ] E tize-coveraM f 7 N 0 C Pge=func
  • -sanitize=undefined

swift 类经过上述办法相同能够获取符号 .

优化后作用监测

在完全榜首次装置冷发动 , 确保相同的环境 , page fault 采样相同截取到榜首个可交互界面 , 运用w D E r ^ h重排优化前后作用如下 .

  • 优化前
    iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
  • 优化后
    iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

实践上 , 在出产环境中 , 因为 page fault 还需求签名验证 , 因而在分发环境下 , 优化作用其实] ] 6 E _更多 .

总结

本篇文章经过以实践碳素进程为基准 , 一步一步完成 clang 静态插桩到达二进制重排优化发动时间的完好流程 .

具体完成过程如下 :

  • 1️⃣ : 运用 clang 插桩获得发动时期需求加载的一切 函数/办法 , block , swiI # 7ft 办法以及 c++结构办法的符号 .
  • 2️⃣ : 经过 order file 机制完成二进制_ 4 ; % y d G H 7重排 .

如有疑问或许不同看法 , 欢迎留言交流7 / q | 6 [ * j ; .