得物 iOS 函数调用栈及符号化调优实践|得物技术

二、BSBacktraceLogger 分析

当谈到在 iOS 上获取任意线程的堆栈信息,大部分文章都是在介绍如何进行栈回溯来还原调用堆栈,而 BSBacktraceLogger 是其中比较出名的一个工具。

BSBacktraceLogger 具体的实现原理分析本文不再赘述,感兴趣的读者可以自行搜索,网上有很多分析透彻的文章;在这里只说明一下 BSBacktraceLogger 在得物动态合规检测场景中不适用的原因:

  1. 使用 BSBacktraceLogger 拿到的堆栈是一堆内存地址,这样的堆栈传到合规监测平台没有任何意义。虽然 BSBacktraceLogger 提供了一种符号还原的方法,但 BSBacktraceLogger 的符号还原算法原理是通过解析 machO 文件,读取符号表,根据偏移量来判断应该是哪个函数。但这套算法的准确度一般,当符号表很大时,恢复速度慢,且占用大量的内存和 CPU 资源。

  2. 服务端恢复测试包的符号较为困难且耗时长。测试包出包频率高,得物每天测试包构建数量接近三位数,假设当前有 100 台测试机均安装了不同的测试包,每台设备上传 1000 条堆栈信息,即使通过一些方式进行去重,这个数量级的堆栈还原还是比恢复崩溃的堆栈大得多,毕竟崩溃是个小概率事件,而隐私 API 的调用是个必然事件。

  3. 测试包本身没有去除符号,无需再做一次离线的符号恢复,给服务端增加难度和工作量。

经过一系列测试,最终发现在得物中使用 BSBacktraceLogger 的方式,效率甚至不如直接使用 +[NSThread callStackSymbols]

既然 BSBacktraceLogger 不符合要求,那么深入研究一下系统 +[NSThread callStackSymbols] 方法的实现方式,或许会有收获。

三、callstackSymbols 分析

首先通过 lldb 看一下 +[NSThread callStackSymbols] 出自哪一个系统库,通过 image lookup 命令可以看到, +[NSThread callStackSymbols] 在系统的 Foundation.framework 中:

image.png

 callstackSymbols

进入 /Users/admin/Library/Developer/Xcode/iOS DeviceSupport/iPhone14,4 15.2 (19C57)/Symbols/System/Library/Frameworks/Foundation.framework/ 找到 Foundation 的二进制,通过逆向分析工具 IDA,可以直接定位到函数实现的伪代码:

image.png

 callstackSymbols 伪代码

伪代码的实现比较简单,能够看到 +[NSThread callStackSymbols] 内部主要调用 thread_stack_async_pcs 以及 _NSCallStackArray 的某个函数。静态分析由于缺乏符号,因此具体调用 _NSCallStackArray 的哪个函数无法知晓;但是可以通过动态调试,更加清晰的了解函数的运作方式。

启动 Xcode,运行 Demo 工程并给 +[NSThread callStackSymbols] 下断点,调试方式选择始终进入汇编模式:

image.png

 lldb 中的动态汇编代码

通过查看对应的汇编代码,可以定位到实际调用的函数在 51 行的 bl,通过动态调试可以看出,这里 0x180f19dd8 是 objc_msgSend 方法:

image.png

 IDA 中的静态分析结果

image.png

 0x180f19dd8

在 iOS 中 objc_msgSend 方法存在两个固定的参数,分别是 id 和 SEL ,一个代表函数的调用者,可能是个类,也可能是实例对象,另一个代表 Selector ,是需要调用的函数指针,这两个参数按照 ARM 汇编的传参顺序,一个放在 X0 寄存器,一个放在 X1 寄存器;这里通过静态分析可以得知,此处的 objc_msgSend ,第一个参数是类 _NSCallStackArray ,可以通过 po 命令打印验证,SEL 也可以一并输出结果:

image.png

 id 和 SEL 的动态调试结果

通过动态调试,可以看到此处调用的是 +[_NSCallStackArray arrayWithFrames:count:symbols:] ,使用 image lookup 命令查看函数的具体位置,发现该函数的实现也在 Foundation.framework 里:

image.png

 +[_NSCallStackArray arrayWithFrames:count:symbols:]

+[_NSCallStackArray arrayWithFrames:count:symbols:]

在 Foundation.framework 的静态分析结果中搜索,找到 +[_NSCallStackArray arrayWithFrames:count:symbols:] 函数的汇编,发现是一些内部的相关属性赋值操作:

image.png

 +[_NSCallStackArray arrayWithFrames:count:symbols:] 汇编

通过动态调试,查看在内存中运行时的具体赋值情况,可以看到 _NSCallStackArray 类中有很多属性,也可以知晓 +[_NSCallStackArray arrayWithFrames:count:symbols:] 方法三个参数类型分别是 void /unsigned long /BOOL

image.png

分析到这里,笔者猜测该函数的作用应该是将内存地址符号化,可以动态调试验证这一点,首先输出正常符号化后的堆栈信息:

image.png

 正常输出

根据 ARM 汇编的传参规则,通过修改 X4 寄存器,可以将 +[_NSCallStackArray arrayWithFrames:count:symbols:] 中第三个参数的 BOOL 从 1 改为 0:

image.png

 修改为 0

再次查看,输出的是未符号化之前的函数堆栈:

image.png

分析到这里,针对 +[_NSCallStackArray arrayWithFrames:count:symbols:] 做一个总结:

+[_NSCallStackArray arrayWithFrames:count:symbols:] 方法三个参数类型分别是 void **/unsigned long /BOOL ,函数的作用是将函数地址符号化,通过修改第三个参数 symbols,可以控制是否进行符号化操作。

thread_stack_async_pcs

+[_NSCallStackArray arrayWithFrames:count:symbols:] 分析完成,继续查看 thread_stack_async_pcs 的实现。thread_stack_async_pcs 通过 image lookup 命令可以看到出自 libsystem_c.dylib:

image.png

 thread_stack_async_pcs

使用 IDA 继续静态分析 libsystem_c.dylib,thread_stack_async_pcs 内部直接调用 **__thread_stack_pcs** 函数,没有其他任何操作:

image.png

 hread_stack_async_pcs 伪代码

查看 __thread_stack_pcs 函数的汇编代码,发现开头有三个函数没有符号,需要通过动态调试来查看:

image.png

 __thread_stack_pcs 汇编

给 __thread_stack_pcs 下断点,可以看到三个没有符号的函数是 pthread_self 、pthread_get_stackaddr_np 、pthread_get_stacksize_np

image.png

image.png

image.png

通过动态调试和静态分析相结合,现在可以拿到 __thread_stack_pcs 中函数符号化之后的伪代码:

__int64 __fastcall __thread_stack_pcs(__int64 a1, int a2, _DWORD *a3, int a4, unsigned __int64 a5, unsigned __int8 a6)
{
  v12 = pthread_self();
  v13 = pthread_get_stackaddr_np();
  v14 = pthread_get_stacksize_np(v12);
  *a3 = 0;
  v15 = pthread_stack_frame_decode_np(vars0, 0LL);
  v16 = 0LL;
  if ( ((unsigned int)vars0 & 1) == 0 )
  {
    v17 = v13 - v14;
    if ( (unsigned __int64)vars0 >= v17 )
    {
      v18 = v15;
      v28 = a6;
      v19 = (__int64 *)((char *)vars0 + v13 - v15);
      if ( vars0 <= v19 )
      {
        v20 = vars0;
        if ( !a5 )
          goto LABEL_7;
LABEL_5:
        if ( v18 > a5 && a5 != 0 )
        {
LABEL_15:
          if ( a2 )
          {
            v16 = 0LL;
            v21 = 1 - a2;
            StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 3));
            while ( 1 )
            {
              if ( (unsigned __int64)*v20 >> 60 == 1 )
              {
                v23 = *(_QWORD *)(StatusReg + 824);
                if ( v23 )
                {
                  v24 = *(unsigned int *)(v23 + 36);
                  v16 = (_DWORD)v24 ? 1LL : (unsigned int)v16;
                  if ( !(((_DWORD)v24 == 0) | (v28 ^ 1) & 1) )
                    break;
                }
              }
              v25 = pthread_stack_frame_decode_np(v20, &v29);
              v26 = (unsigned int)*a3;
              *(_QWORD *)(a1 + 8 * v26) = v29;
              *a3 = v26 + 1;
              if ( (unsigned __int64)v20 < v25 && (v25 & 1) == 0 && v17 <= v25 && (unsigned __int64)v19 >= v25 )
              {
                ++v21;
                v20 = (__int64 *)v25;
                if ( v21 != 1 )
                  continue;
              }
              return v16;
            }
            __thread_stack_async_pcs(a1, (unsigned int)-v21, a3, v20);
            return v24;
          }
          else
          {
            return 0LL;
          }
        }
        else
        {
          while ( 1 )
          {
            v16 = 0LL;
            if ( (unsigned __int64)v20 >= v18 || (v18 & 1) != 0 || v17 > v18 || (unsigned __int64)v19 < v18 )
              break;
            v20 = (__int64 *)v18;
            v18 = pthread_stack_frame_decode_np(v18, 0LL);
            if ( a5 )
              goto LABEL_5;
LABEL_7:
            if ( !a4 )
              goto LABEL_15;
            --a4;
          }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值