基于ARM的iOS线上崩溃定位与修复实践

1. 背景

本篇文章完成于2023年初,公司内部信息已做脱敏处理

2022年10月中旬Apple针对iOS逐步推出了16.1系统,动态布局SDK的iOS端在稳定运行了很长时间之后,出现了大面积的"objc_release + 8"的内存访问异常崩溃(EXC_BAD_ACCESS)。随着16.1及其以后版本的占比越来越高,对应的崩溃量也出现逐步的攀升。

本文会结合线上崩溃日志以及线下复现的多种手段来定位问题并解决问题。

2. 现状

2.1 问题是什么

我们可以先来看一下上述崩溃的线程信息:

Incident Identifier: 9546113E-xxxx-xxxx-xxxx-C9F25815BFCA
CrashReporter Key:   addxxxxxxxxx
Hardware Model:      iPhone15,3
Process:         demoe [2161]
Path:            /private/var/containers/Bundle/Application/B286F1DB-xxxx-xxxx-9157-A99939E67462/demo.app/demo
Identifier:      com.test.demo
Version:         160490 (12.6.204)
Code Type:       ARM-64
Parent Process:  ? [1]

Date/Time:       2023-01-05 23:59:38.028 +0800
OS Version:      iOS 16.1.1 (20B101)
Report Version:  104

Monitor Type:    Mach Exception
Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000010
Crashed Thread:  26

Thread 26 Crashed:
0   libobjc.A.dylib                 objc_release + 8
1   demo                                -[SAKDynamicLayout createElement:] (SAKDynamicLayout.m:775)
2   demo                               -[SAKDynamicLayout buildElementFromMetaXMLNode:parentElement:forVars:] (SAKDynamicLayout.m:1003)
3   demo                               __70-[SAKDynamicLayout buildElementFromMetaXMLNode:parentElement:forVars:]_block_invoke.242 (SAKDynamicLayout.m:1328)
...

对应的代码如下:

-(SAKXMLElement *)createElement:(NSString *)name {
    SAKXMLElement *element = [[SAKXMLElement alloc] init];
    element.name = name;
    element.currentLayout = self;
    [element createRenderObject];
    return element;
}

崩溃信息和原始的代码对比看起来,没办法知道objc_release是发生在哪里。是发生在形参的retain对应的release、还是发生在SAKXMLElement的初始化方法里面(内联了)、抑或是发生在element的setName方法里面、又或者说是element本身的release呢?

从现有的崩溃日志简单分析是没办法分析出来的,没有办法知道崩溃发生的点,也就没办法去增加修复的兜底。

编译器最终生成的代码和程序员写的代码有出入,在崩溃日志上面没有直观地体现问题发生的位置以及对应的原因

怎么做呢?既然崩溃日志无法给出具体的错误信息,那么我们剖析ARM的寄存器与函数调用规范获取更加底层的信息。

再来看看崩溃发生时寄存器状态,一堆16进制的数据,粗看完全不知所云:

Thread 26 crashed with ARM-64 Thread State:
  cpsr: 0x0000000000001000     fp: 0x000000016ed984c0     lr: 0x00000001063bddcc     pc: 0x00000001c9d8b54c 
    sp: 0x000000016ed984a0     x0: 0x0000000000000010     x1: 0x0000000280d013c4    x10: 0x00000002270d0840 
   x11: 0x0000000000000072    x12: 0x00000000000001c3    x13: 0x00000001608abc20    x14: 0x0000000280d013c1 
   x15: 0x00000002270e4f08    x16: 0x00000001c9d8b544    x17: 0x000000022c169d30    x18: 0x0000000000000000 
   x19: 0x00000002812c8660     x2: 0x0000000000000000    x20: 0x0000000161210c20    x21: 0x0000000282f4b480 
   x22: 0x0000000281f99640    x23: 0x0000000000000000    x24: 0x0000000161210c20    x25: 0x00000002814e5fe0 
   x26: 0x0000000282f7f4e0    x27: 0x00000002812747b0    x28: 0x0000000000000001    x29: 0x000000016ed984c0 
    x3: 0x000000016ed97f9b     x4: 0x0000000000000000     x5: 0x0000000000000000     x6: 0x0000000000000072 
    x7: 0x0000000000000000     x8: 0x0000000000000010     x9: 0x001b3ac2b573008d 

此时可执行程序以及映射的image如下:

Binary Images:
       0x1020c8000 -        0x10851bfff +demo arm64  <45fce1ae1xxxxxxfd9b41780ad58> /private/var/containers/Bundle/Application/B286F1DB-xxxx-xxxx-xxxx-A99939E67462/demo.app/demo
       0x10c43c000 -        0x10c443fff  PanGu arm64  <3c5c92f3e89c3c1dbc6ce40d93992f47> /private/var/containers/Bundle/Application/B286F1DB-3F78-4291-9157-A99939E67462/demo.app/Frameworks/PanGu.framework/PanGu
       0x10cdb0000 -        0x10cdbbfff  libobjc-trampolines.dylib arm64e  <3eb26cf9922139f583d40c8ae83d3424> /private/preboot/Cryptexes/OS/usr/lib/libobjc-trampolines.dylib
       0x1c9d88000 -        0x1c9dcbe1f  libobjc.A.dylib arm64e  <ab79707faf643ba588993b711c6cff5c> /usr/lib/libobjc.A.dylib
       0x1c9dcc000 -        0x1ca8acfff  MetalPerformanceShadersGraph arm64e  <b0605248e3443aca8f9c167c76255813> /System/Library/Frameworks/MetalPerformanceShadersGraph.framework/MetalPerformanceShadersGraph
       0x1ca8ad000 -        0x1cae15fff  libswiftCore.dylib arm64e  <f896d145e02539d6afd3bc0a2ad4f839> /usr/lib/swift/libswiftCore.dylib
       0x1cae16000 -        0x1cae47fff  CoreServicesInternal arm64e  <b3d3659c112d3305b1afd6119b6aed1e> /System/Library/PrivateFrameworks/CoreServicesInternal.framework/CoreServicesInternal
       0x1cae48000 -        0x1cb791fff  Foundation arm64e  <c431acb6fe043d28b6774de6e1c7d81f> /System/Library/Frameworks/Foundation.framework/Foundation
       0x1cb792000 -        0x1cb7d1fff  WebGPU arm64e  <1cc9f1f9198d3221ae9aa9e0413a5ec6> /System/Library/PrivateFrameworks/WebGPU.framework/WebGPU
       0x1cb7d2000 -        0x1cb987fff  Metal arm64e  <8f062125748839efafda57db8cd80033> /System/Library/Frameworks/Metal.framework/Metal
       0x1cb988000 -        0x1cbb7afff  CoreServices arm64e  <9b4971df95e5302b87290741b957daac> /System/Library/Frameworks/CoreServices.framework/CoreServices
       0x1d0a68000 -        0x1d0e4dfff  CoreFoundation arm64e  <5cdc5d9ae5063740b64ebb30867b4f1b> /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
       0x1d0e4e000 -        0x1d1be9fff  Network arm64e  <3bf445f9d58f3280b9c92feaf4daca6d> /System/Library/Frameworks/Network.framework/Network
       0x1d1bea000 -        0x1d1fb1fff  CFNetwork arm64e  <edb0559fc996327f9b3a6616e316f24d> /System/Library/Frameworks/CFNetwork.framework/CFNetwork
       0x1d1fb2000 -        0x1d20f5fff  CoreTelephony arm64e  <5da0a407b8723d8291ab16d63923fb47> /System/Library/Frameworks/CoreTelephony.framework/CoreTelephony

这里我摘录了几个关键image,下面我们需要以这些信息为输入并搭配lldb等工具来梳理其中的各个细节。不过进入具体的分析之前,我们需要一些前置的知识点。

如果不需要阅读ARM的函数调用约定以及寄存器的使用规则,可以直接跳过这部分内容直接进进入到第3节。

2.2 汇编中数据与指令

在CPU里面直接和ALU交互的数据均是存储在寄存器中,而如何操作这些寄存器就是通过定义相关指令。在ARM64上面一共有31个通用寄存器

W是低32位寄存器的前缀,X是64位寄存器的前缀。LR和FP寄存器在下面的函数调用标准里面介绍。最下面的EL0~EL3是ARMv8的异常等级,我们应用程序处于EL0,操作系统处于EL1,内核等其他安全相关的处于更高的异常等级(这里说的异常等级可以理解为类似于Linux里面经常提到的特权级),不同的异常等级有不同的操作权限。

每一个异常等级都有对应的几个特殊寄存器:


  • XZR/WZR是零寄存器(所有针对该寄存器的读写都会得到0),针对ZR寄存器的读操作得到的都是0,针对ZR寄存器的写操作都是无意义被忽略的。
  • PC寄存器保留的是当前执行指令的地址;
  • SP寄存器通常用于子函数调用时,栈的边界(栈的基地址位于FP寄存器中);
  • PSR寄存器,状态寄存器在EL0级别下用的是CPSR,在其他异常等级下用的是对应的SPSR。我们后面在分析if语句的执行路径时会详细再看看;
  • ELR寄存器是用在切换异常等级返回之后的地址;

2.3 汇编中函数调用标准

下面的内容都是基于A64架构。看了上面的内容,我们需要的是明确在函数调用(也叫子过程调用)过程中,各个指令和寄存器的作用。我们要处理的任务包括有数据处理类的、内存处理流程控制类的(详细的指令可以在这里查询)

对于OC来说,大部分方法调用最终都会转变为基于objc_msgsend,该函数的原型如下:

objc_msgSend(void /* id self, SEL op, ... */ )

第一个参数指明当前方法调用的实力,第二参数指明当前调用方法名(对应为SEL),在此之后就是各种参数的传递。

参数寄存器:X0~X7作为参数传递和返回值保留的寄存器,超过部分将使用栈来进行传递;

  • X8(Indirect Result Location):间接结果寄存器,通常用于保留指针类型返回值;
  • FP(X29):保留着栈的基地址指针(在x86里面称作bp);
  • LR(X30):保留函数调用的返回地址指针(在x86里面是通过ip寄存器实现类似的效果);
  • SP(X31):保留栈顶地址;

我们看一个ARM官方文档提供的一个例子:

struct struct_A {
      int i0;
      int i1;
      double d0;
      double d1;
} AA;

struct struct_A foo(int i0, int i1, double d0, double d1) {
      struct struct_A A1;
      A1.i0 = i0;
      A1.i1 = i1;
      A1.d0 = d0;
      A1.d1 = d1;
      return A1;
}

 void bar() {
      AA = foo(0, 1, 1.0, 2.0);
    int a = 0;
   return ;
}

对应的汇编代码如下:

foo//
      SUB SP, SP, #0x30
      STR W0, [SP, #0x2C]
      STR W1, [SP, #0x28]
      STR D0, [SP, #0x20]
      STR D1, [SP, #0x18]
      LDR W0, [SP, #0x2C]
      STR W0, [SP, #0]
      LDR W0, [SP, #0x28]
      STR W0, [SP, #4]
      LDR W0, [SP, #0x20]
      STR W0, [SP, #8]
      LDR W0, [SP, #0x18]
      STR W0, [SP, #10]
      LDR X9, [SP, #0x0]  <------------------------------------------------------------------------ 开始赋值结构体值
      STR X9, [X8, #0]
      LDR X9, [SP, #8]
      STR X9, [X8, #8]
      LDR X9, [SP, #0x10]
      STR X9, [X8, #0x10] <------------------------------------------------------------------------ 结束赋值结构体值
      ADD SP, SP, #0x30
      RET

  bar//
      STP X29, X30, [SP, #0x10]!
      MOV X29, SP
      SUB SP, SP, #0x20
      ADD X8, SP, #8
      
      MOV W0, WZR 
      ORR W1, WZR, #1
      FMOV D0, #1.00000000
      FMOV D1, #2.00000000
  
      BL foo:
      ADRP X8, {PC}, 0x78
      ADD X8, X8, #0
      LDR X9, [SP, #8]
      STR X9, [X8, #0]
      LDR X9, [SP, #0x10]
      STR X9, [X8, #8]
      LDR X9, [SP, #0x18]
      STR X9, [X8, #0x10]
      MOV SP, X29
      LDP X20, X30, [SP], #0x10
      RET

下图是函数调用关系:


2.4 内联汇编

在后面的调试中,我们需要使用一点简单的汇编代码,仅仅是为了断点使用。为了文章的完整性这里还是简单提及一下。通用基本格式:

__asm__/asm [volatile] (code); /* Basic inline assembly syntax */

扩展格式:

/* Extended inline assembly syntax */
__asm__/asm [volatile] (
  code_template
    : outputs
   [: inputs
   [: clobber_list]]
);

这里提到的volatile和我们平时变量声明的时候含义有点差异,它的意思避免编译器优化这部分(比如删除掉),这么做的目的是为了我们的代码能够正常保留并执行。一个完整的例子如下:

int res = 0;
__asm ("ADD %[result], %[input_i], %[input_j]"
    : [result] "=r" (res)
    : [input_i] "r" (i), [input_j] "r" (j)
);

在输入和输出的列表里面,针对每个变量都有一个约束符号(比如r):

  • 修饰符:这些修饰符不能使用于输入的内容里面,只能用于输出的约束。比如这里的=符号,表示写操作;
  • 约束符:约束当前操作时针对寄存器的,比如这里的r表示的是,针对寄存器的操作;

详细内容见 Writing inline assembly codeConstraint codes for AArch64 state

3. 定位

进入正题,下面来看一下我们如何一步一步的找到被隐藏起来的崩溃原因。

3.1 线上日志分析

在本节我们会根据线上信息,并搭配上面提到的ARM知识来分析线上崩溃日志,看看能否找到问题的蛛丝马迹。本节会用到otool、MachOView以及LLDB等工具来协助排查问题。

用到的工具主要是基于LLDB,使用lldb还原线上崩溃发生时对应的符号(参考LLVM Symbolication):

(lldb) target create -no-dependents --arch arm64 demo
Current executable set to 'demo' (arm64).
(lldb) target modules load --file imeituan __TEXT 0x1020c8000 /// 崩溃发生时imeituan的起始地址是0x1020c8000
(lldb) image lookup --address 0x00000001063bddcc /// 查看发生崩溃时的lr

如果能够拿到对应的可执行文件,我们可以使用otool拿到反编译的汇编代码:

otool -tV -arch arm64 imeituan -> ~/Downloads/imeituan_text /// 也可以直接用MachOView,这里输出到文本中便于搜索

拿到了反编译的汇编代码之后,我们可以根据上面提到的LR寄存器的值,与imeituan的image的起始地址计算出当前出现的代码所在:

address=ASLR+offset

比如我们要关注LR寄存器里面保留的值(0x00000001063bddcc),因此这里的偏移量为0x42F5DCC(0x00000001063bddcc - 0x1020c8000),接着我们根据这个偏移量获取到当前LR指向的汇编代码为:



由于LR是子过程调用发生时,用来保留子过程返回时的汇编地址的。因此这里实际上是发生了指令跳转:

bl0x105c52a84

/// 0x105c52a84
0000000105c52a84movx2, x19
0000000105c52a88b0x10612fc00

0x10612fc00的地址已经不包含在imeituan的__TEXT里面,所以我们需要加载其他的image:

target create --arch arm64 demo

使用lldb查看其地址信息:

lldb) image lookup -a 0x10612fc00 -v
      Address: imeituan[0x000000010612fc00] (imeituan.__TEXT.__objc_stubs + 3761088)
      Summary: 
       Module: file = "/Users/xxxx/Downloads/demo-1-160490.20221228170145/Payload/demo.app/demo", arch = "arm64"

发现其是处于__TEXT.__objc_stubs,也就是说是一个桩调用,此时会跳转到stub_helper,等到正常调用发生之后才会修正为真实的函数地址。我们看看对应地址的反汇编代码:

000000010612fc00  adrpx1, 0x1080ee000
000000010612fc04  ldrx1, [x1, 0x268]
000000010612fc08  adrpx16, 0x106455000
000000010612fc0c  ldrx16, [x16, 0xb40]
000000010612fc10  brx16
000000010612fc14  brk0x1
000000010612fc18  brk0x1
000000010612fc1c  brk0x1

这里br指令是跳转到对应寄存器保留的地址,0x106455000+0xb40 = 0x106455B40。对应地址处于 __DATA.__got section内部对应的符号为:_objc_msgsend


这里也可以用otool -s __DATA __got来读取数据,但是需要我们自己去将符号表(对于got来说它在符号表中Type为N_UNDF,因此使用"nm -u imeituan")的下标和__got的地址下标关联起来才能知道当前下标是指向的哪个符号,这并没有MackOView省事儿,因此这里的内容是基于MachOView解析之后的截图。

现在来看看x1寄存器里面保存的值,0x1080ee000+0x268 = 0x1080EE268。对应地址处于__DATA.__objc_selrefs内部对应的值为:"setName:"

因此很明显,发生异常时是发生在对setName的调用上面。

再回到上面的寄存器值,我们看到目前pc的值为0x00000001c9d8b54c,对应到images查看处于libobjc.A.dylib内部。我们同样根据ASLR算出其偏移量为:0x354C。

由于这个偏移量是在libobjc.A.dylib里面,所以我们需要知道当前libobjc.A.dylib的起始地址:

(lldb) image list | grep libobjc.A.dylib
[  0] D6ECFB73-0CA2-3A21-A3A9-19E450D3B49C 0x00000001800b8000 /Users/xxxx/Library/Developer/Xcode/iOS DeviceSupport/16.2 (20C65) arm64e/Symbols/usr/lib/libobjc.A.dylib

看到它的起始地址是0x00000001800b8000,加上上面我们得到的偏移量。因此最终的地址为:

(lldb) image lookup -a 0x00000001800b8000+0x354C libobjc.A.dylib
      Address: libobjc.A.dylib[0x00000001800bb54c] (libobjc.A.dylib.__TEXT.__text + 8524)
      Summary: libobjc.A.dylib`objc_release + 8

可以看到发生崩溃时正处于objc_release + 8,和我们在崩溃日志里面看到的内容是一致的,并得到了补充信息:

崩溃发生是由于应用层调用了setName方法,并且是崩溃在libobjc.A.dylib的objc_release + 8

3.2 线下代码调试

线上的日志分析更多是来自于推断,线下代码我们可以编译器的优化等级(Optimization Level)更改为-Os尽可能地还原线上代码的执行路径。在本节主要使用的工具是lldb搭配简单的汇编代码分析(分析代码可能的执行路径)以佐证我们根据线上日志推断出来的结果。

崩溃的代码是调用了release触发的,因此我们想要类似的代码调用的话。是需要基于汇编来进行DEBUG的:

为了能够比较好的在汇编里面 做标记,我们可以插入一个空指令:

-(SAKXMLElement *)createElement:(NSString *)name {
    __asm__("nop\n");
    SAKXMLElement *element = [[SAKXMLElement alloc] init];
    if (name) {
        element.name = name;
    }
    if (self) {
        element.currentLayout = self;
    }
    [element createRenderObject];
    __asm__("nop\n");
    return element;
}

在空指令这里下一个断点。接着我们需要查看是哪里调用了objc_release,由于这个函数是系统函数,调用的地方繁多,为了能够尽可能的减少其他调用对我们的影响。我们可以限制在指定的线程加这个断点:

(lldb) thread info
thread #15: tid = 0x1d3c2b, 0x0000000103219f88 SAKFlexboxLibrary_Example`-[SAKDynamicLayout createElement:](self=0x000000010475ca70, _cmd="createElement:", name=@"Var") at SAKDynamicLayout.m:771:5, queue = 'NSOperationQueue 0x104759f10 (QOS: USER_INITIATED)', stop reason = breakpoint 5.1
/// 对指定线程添加断点
(lldb) br set -n "objc_release" -t 0x1d3c2b
Breakpoint 11: where = APFS`objc_release, address = 0x00000001e318505c

继续执行,我们可以看到具体是哪些调用触发了objc_release+8:

libobjc.A.dylib`objc_release:
->  0x185647544 <+0>:   ands   x0, x0, x0
    0x185647548 <+4>:   b.le   0x185647534               ; objc_retain_x28 + 68
    0x18564754c <+8>:   ldr    x16, [x0]
    0x185647550 <+12>:  and    x2, x16, #0xffffffff8
    0x185647554 <+16>:  ldr    x17, [x2, #0x20]
    0x185647558 <+20>:  tbz    w17, #0x2, 0x1856475b8    ; <+116>
    0x18564755c <+24>:  tbz    w16, #0x0, 0x1856475d4    ; <+144>

这里objc_release+4有个b.le的指令,它是属于b.cond这一类的(Conditional execution in A64 code):

In the A64 instruction set, there are a few instructions that are truly conditional. Truly conditional means that when the condition is false, the instruction advances the program counter but has no other effect.
The conditional branch, B.cond is a truly conditional instruction. The condition code is appended to the instruction with a '.' delimiter, for example B.EQ.

此时查看cpsr寄存器的值为0x00001000:

对于是否命中命中LE的条件,我们可以在Condition code suffixes and related flags对比来看(Z被设置,或者说N和V不一样):

此时条件为false的时候就会向下执行到objc_release+8。

通过增加objc_release的符号断点以及查看CPSR的值,我们可以发现触发了objc_release的地方包含了:

  • 形参的retain对应的release;
  • SAKXMLElement的初始化方法;
  • element的setName方法;

通过对比之后发现第3种是符合线上崩溃堆栈的。

通过线上日志分析和线下代码调试崩溃位置均是setName方法内坏内存访问(该set方法由编译器默认实现)

4. 复现与修复

在前面我们通过线上和线下都将崩溃的地方指向了setName方法,我们基本上可以确定是在set方法内部调用导致的异常内存访问。那么我们能复现这种崩溃吗?

4.1 堆栈复现

本节我们会在已确定发生错误的地方,人为地构造坏内存访问,以复现线上相同的崩溃。因此我们修改方法:

-(SAKXMLElement *)createElement:(NSString *)name {
    __asm__("nop\n");
    SAKXMLElement *element = [[SAKXMLElement alloc] init];
    void *ptr = (__bridge void *)(element);
    void *nameptr = (ptr + 0x10);
    memset(nameptr, 0x10, 1);
    if (name) {
        element.name = name;
    }
    if (self) {
        element.currentLayout = self;
    }
    [element createRenderObject];
    __asm__("nop\n");
    return element;
}

修改的大致含义如下:



name属性的定义如下:

@property(nonatomic, copy) NSString *name;

因此对于它的set方法最终会调用c函数objc_setProperty_nonatomic_copy:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    }
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    }
    objc_release(oldValue);
}

因此我们在init方法之后,setName之前。我们给偏移地址为0x10的地址上设置一个非法的内核地址(name的成员变量在该类的结构里面偏移为0x10):

SAKXMLElement *element = [[SAKXMLElement alloc] init];
void *ptr = (__bridge void *)(element);
void *nameptr = (ptr + 0x10);
memset(nameptr, 0x10, 1);

我们可以通过lldb查看更改前后内存里面的值:

(lldb) x/64b ptr
0x10889e8a0: 0x21 0x33 0xdd 0x05 0x01 0x00 0x00 0x01
0x10889e8a8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

更改之后:

(lldb) x/64b ptr
0x10889e8a0: 0x21 0x33 0xdd 0x05 0x01 0x00 0x00 0x01
0x10889e8a8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b0: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

可以看到地址0x10889e8a8之后8字节的数据为0x10。这时候我们继续执行,得到如下结果:

4.2 问题修复

当然上面复现是我们人为去修改对应内存地址上的数据,但是线上不会有人人为这么做的。那怎么解释呢?看了一下iOS16.1 release note,里面针对Memory Allocation有如下的改动:

通俗一点来说就是有两种场景:

释放内存后执行读操作:即我们在使用指针读取某一指定内存之后,触发了free操作。我们所观测到的内存会是0;

释放内存后执行写操作:即某一个指针指向的内存地址被释放了,在我们调用calloc的之前执行了写操作,那么我们此时生成新的指针指向的内存可能是非0的;

这个第二点就很好的解释了为什么name会有一个非0的初值,即在美团里面可能其他场景针对这块儿内存在释放后执行了写的操作。

如果此时我们调用setName方法,在释放旧值的时候就有可能出现bad_access的问题了。

解决的方案:

我们在SAKXMLElement的初始化方法里面,将对应的值显式赋值为0即可

涨点姿势

如果我们遇到了其他类型ARM汇编的问题了呢?授人以鱼不如授人以渔,简单介绍一下我在ARM开发者里面如何淘我需要的文档的。

ARM针对不同用途而划分出来的不同系列,对于不同的从业人员在查询相关文档时可以做到按需查找(比如基于移动端的开发人员,我们需要查看哪一系列的文档)。下表是ARM的一个架构演进(具体见ARM Cortex-A Series Programmer's Guide for ARMv8-A),每个架构对应了一系列的处理器

指令集架构 处理器家族
ARMv1 ARM1
ARMv2 ARM2ARM3
ARMv3 ARM6、ARM7
ARMv4 StrongARMARM7TDMIARM9TDMI
ARMv5 ARM7EJARM9EARM10EXScale
ARMv6 ARM11ARM Cortex-M
ARMv7 ARM Cortex-AARM Cortex-MARM Cortex-R
ARMv8 Cortex-A35、Cortex-A50系列[18]、Cortex-A70系列、Cortex-X1
ARMv9 Cortex-A510Cortex-A710Cortex-A715Cortex-X2Cortex-X3ARM Neoverse N2

从ARMv7开始,Arm就开始针对不同的领域进行了处理器细分:

  • Cortex-A(Application Processor cores):面向性能密集型系统的应用处理器内核,我们关心的智能手机则属于这个系列;
  • Cortex-R(Real Time Application cores):面向实时应用的高性能内核;
  • Cortex-M(Microcontroller Cores):面向各类嵌入式应用的微控制器内核;

因此后续我们在找相关文档的时候查看Cortex-A系列的即可,但是指令集架构在不断演进。我们目前主要是基于ArmV8-Cortex-A,以Cortex-A57处理器为例:

包含有4个核心(0~3),其内部包含有处理器核心部分、NEON(用于处理SIMD,单指令多数据,一般用于类似矩阵等有规律的数据,能够减少处理器和内存的数据交互次数)、L1的指令和数据缓存等等。

对于ArmV8来说,他相对于v7的变化如下:

其中最重要的就是引入了64位的运行模式,因此在v8上面目前支持两种执行状态:

  • AArch32:也简称为A32
  • AArch64:也简称为A64,在macOS或者其他类Unix的系统也会叫做ARM64。

希望对大家有所帮助。

参考文献

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