iOS疑难Crash的寄存器赋值追踪排查技术

我们会借助一些崩溃日志收集库来定位和排查线上的崩溃信息,但是有些崩溃堆栈所提供的信息有限又不是必现崩溃,很难直观排查出问题的所在。这里我给大家分享一个采用寄存器赋值追踪的技术来排查和分析崩溃日志的技巧。话不多说先看案例:

//符号化后的崩溃堆栈
Date/Time:       2021-03-25 04:35:38.211 +0800
OS Version:      iOS 10.3.2 (14F89)
Report Version:  104

Monitor Type:    Unix Signal
Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x00000000 at 0x00000001808f1014
Crashed Thread:  27


Pthread id: 1313098
Thread 27 Crashed:
0   libsystem_kernel.dylib          __pthread_kill + 8
1   libsystem_pthread.dylib         pthread_kill + 112
2   libsystem_c.dylib               abort + 140
3   libsystem_malloc.dylib          szone_error + 420
4   libsystem_malloc.dylib          free_list_checksum_botch.295 + 36
5   libsystem_malloc.dylib          tiny_free_list_remove_ptr + 288
6   libsystem_malloc.dylib          tiny_free_no_lock + 684
7   libsystem_malloc.dylib          free_tiny + 472
8   CoreFoundation                  _CFRelease + 1228
//只有这句信息可以参考
9   testApp                        __99-[XXX fn:queue:]_block_invoke + 384
10  libdispatch.dylib               _dispatch_call_block_and_release + 24
11  libdispatch.dylib               _dispatch_client_callout + 16
12  libdispatch.dylib               _dispatch_queue_serial_drain + 928
13  libdispatch.dylib               _dispatch_queue_invoke + 884
14  libdispatch.dylib               _dispatch_root_queue_drain + 540
15  libdispatch.dylib               _dispatch_worker_thread3 + 124
16  libsystem_pthread.dylib         _pthread_wqthread + 1096
//这是原始崩溃栈
Pthread id: 1313098
 Thread 27 Crashed:
0   libsystem_kernel.dylib          0x00000001808f1014 __pthread_kill + 8
1   libsystem_pthread.dylib         0x00000001809bb264 pthread_kill + 112
2   libsystem_c.dylib               0x00000001808659c4 abort + 140
3   libsystem_malloc.dylib          0x0000000180931828 szone_error + 420
4   libsystem_malloc.dylib          0x000000018093b74c 0x180924000 + 96076
5   libsystem_malloc.dylib          0x0000000180928994 0x180924000 + 18836
6   libsystem_malloc.dylib          0x000000018093ba00 0x180924000 + 96768
7   libsystem_malloc.dylib          0x000000018093c0c8 0x180924000 + 98504
8   CoreFoundation                  0x00000001818a701c 0x1817ca000 + 905244
9   testApp                         0x0000000103b9e5b8 __99-[XXX fn:queue:]_block_invoke + 384
10  libdispatch.dylib               0x00000001807ae9e0 0x1807ad000 + 6624
11  libdispatch.dylib               0x00000001807ae9a0 0x1807ad000 + 6560
12  libdispatch.dylib               0x00000001807bcad4 0x1807ad000 + 64212
13  libdispatch.dylib               0x00000001807b22cc 0x1807ad000 + 21196
14  libdispatch.dylib               0x00000001807bea50 0x1807ad000 + 72272
15  libdispatch.dylib               0x00000001807be7d0 0x1807ad000 + 71632
16  libsystem_pthread.dylib         0x00000001809b7100 _pthread_wqthread + 1096


Binary Images:
       0x1000b8000 -        0x108263fff +testApp arm64  <cb8cc0075ed4352b802c6c586b8a93d5> /var/containers/Bundle/Application/0A1F4541-8749-4F9D-B60A-813FFEE69CA6/testApp.app/testApp

从上面的崩溃信息大概可以看出这是一个GCD队列线程调用时发生了崩溃。其中崩溃的第9层显示是在一个[XXX fn:queue:] 方法内定义的block内部代码发生了崩溃,除此之外没有其它信息。崩溃方法的源代码定义如下:

//这是一个简化代码
@interface TestObj:NSObject

-(NSString*)testString;

-(NSInteger)length;

@end

//代码片段
-(void)fn:(TestObj*)testObj queue:(dispatch_queue_t)queue {
    dispatch_async(queue, ^{
        @autoreleasepool {
              if ([testObj length] != 0) {
                  NSString *suffix = [testObj testString];
                  const static int len = 4;
                  if (suffix.length > len) {
                      suffix = [suffix substringToIndex:len];
                  }
              }
        }
    });
}

从源代码来看确实是在方法-[XXX fn:queue:]内调用了一个dispatch_async,然后block也是定义在方法内。不过依然没办法定位到是哪行代码发生了崩溃, 同时也不是必现的线上崩溃。

所以要想查明原因需要到汇编代码级别定位崩溃原因!! 步骤如下:

  1. 先下载可执行文件到本地或者从CI发布部门获取可执行的app包并解压。

  2. 用系统自带的otool工具,进行代码的反汇编处理。下面的otool命令格式可以用来显示具体的函数或者方法的反汇编代码:

 otool "可执行文件路径"   -p "函数或者方法名" -V -t 

otool命令中 -p 后面跟的是方法名或者函数名或者符号名。 这里需要注意的时因为系统编译时可能会在函数名或者符号名前多增加一个下划线_ 因此在指定符号名时需要多增加一个_。 -V 是表明打印函数对应的汇编代码。 -t 是表明打印代码段中的代码。

本例的问题使用otool如下:

  otool   "/Users/apple/Downloads/Payload/testApp.app/testApp" -p "___99-[XXX fn:queue:]_block_invoke" -V -t 

汇编出来的局部代码如下:

/Users/apple/Downloads/Payload/testApp.app/testApp:
(__TEXT,__text) section
___99-[XXX fn:queue:]_block_invoke:
......... 省略部分代码
0000000103ae6554<+284>  mov x20, x0
0000000103ae6558<+288>  ldr x0, [x20, #x20]
0000000103ae655c<+292>  adrp    x8, 26695 ; 0x10a32d000
0000000103ae6560<+296>  ldr x1, [x8, #0x940] ; Objc selector ref: testString
0000000103ae6564<+300>  bl  0x107fac9e8 ; Objc message: -[x0 testString]
0000000103ae6568<+304>  mov x29, x29
0000000103ae656c<+308>  bl  0x107faca48 ; symbol stub for: _objc_retainAutoreleasedReturnValue
0000000103ae6570<+312>  mov x25, x0
0000000103ae6574<+316>  mov x0, x26
0000000103ae6578<+320>  bl  0x107faca18 ; symbol stub for: _objc_release
0000000103ae657c<+324>  mov x0, x25
0000000103ae6580<+328>  mov x1, x22
0000000103ae6584<+332>  bl  0x107fac9e8 ; Objc message: -[x0 testString]  //这里是otool的bug真实应该是 length方法。
0000000103ae6588<+336>  cmp x0, #0x5
0000000103ae658c<+340>  b.lo    0x103ae65bc
0000000103ae6590<+344>  adrp    x8, 26674 ; 0x10a318000
0000000103ae6594<+348>  ldr x1, [x8, #0xb30] ; Objc selector ref: substringToIndex:
0000000103ae6598<+352>  mov x0, x25
0000000103ae659c<+356>  mov w2, #0x4
0000000103ae65a0<+360>  bl  0x107fac9e8 ; Objc message: -[x0 substringToIndex:]
0000000103ae65a4<+364>  mov x29, x29
0000000103ae65a8<+368>  bl  0x107faca48 ; symbol stub for: _objc_retainAutoreleasedReturnValue
0000000103ae65ac<+372>  mov x22, x0
0000000103ae65b0<+376>  mov x0, x25
0000000103ae65b4<+380>  bl  0x107faca18 ; symbol stub for: _objc_release
0000000103ae65b8<+384>  mov x25, x22

从上述的崩溃信息可以看出崩溃的地址是0x0000000103b9e5b8。根据这个地址可以得出崩溃是在我们反汇编代码的倒数第二行。0x0000000103ae65b4<+380>

上述的两个地址都不同,结论是如何得出的?

  1. 线上的程序运行时程序时会在一个随机的基地址上加载,从崩溃的原始堆栈中最下面部分可以看到程序映像的基地址就是 0x1000b8000。因此:
0x0000000103b9e5b8 - 0x1000b8000 = 0x0000000103ae65b8
  1. 又因为一般程序崩溃的地址都有3个特征:
    a. 崩溃堆栈层级中的非顶层地址都是函数调用指令的下一条地址也就是LR的值,所以真实的崩溃指令处是第1步算出的结果再减去4也就是实际崩溃的地址是:0x0000000103ae65b4

    b. 如果崩溃信息出现在最顶层时,一般的崩溃指令都是带有内存访问的指令。假如崩溃是在第上面的第二条指令,也就是在ldr x0, [x20, #x20] 处时很大概率是访问的内存地址无效产生的崩溃。

    c . 如果崩溃信息出现在最顶层即无内存访问也无函数调用的指令时,这种崩溃一般是触发了brk断点指令,或者产生了其他一些无法可判断的原因了。前者比较好定位,后者就很难了。

从上面汇编代码倒数第二行<+384>处可以看出是一个对象调用了_objc_release进行释放时导致了崩溃。而_objc_release内部可能会调用_CFRelease方法,这也就是在上面崩溃信息堆栈中__99-[XXX fn:queue:]_block_invoke + 384的上面是_CFRelease函数了。

既然是因为对象调用_objc_release导致的内存释放异常。那么就需要继续追踪是哪个对象调用了_objc_release。

根据arm系统的函数调用ABI规则,以及从倒数第三行<+376>的汇编代码中可以看出是执行了一个 x0 = x25的操作,也就是x0对象是从x25赋值而来的。这时候我们就可以利用寄存器赋值追踪的技巧,继续往上查看x25又是在哪里被赋值。 往上的代码可以看出在<+312>处的指令执行了 x25 = x0的赋值操作,而x0的结果是上一条指令调用_objc_retainAutoreleasedReturnValue函数返回的结果。而_objc_retainAutoreleasedReturnValue的入参又是上一条指令<+300>处的[x0 testString] 方法返回的结果。到这里为止我就可以从源代码中推断出是[testObj testString] 返回的结果对象在释放时导致了崩溃了。

那接下来你就可以仔细查查源代码[testObj testString]的方法哪里有问题了。并最终定位出异常原因。

下面就是用寄存器追踪技术展示的追踪推导图:


寄存器追踪图

小贴士:在arm64位系统中,函数的第一个参数用x0寄存器保存,OC方法调用的对象也是用x0寄存器保存,函数和方法的返回结果也是用x0寄存器保存。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容