空指针访问的陷阱

背景

之前用AddressSanitizer工具验证阿里云短视频SDK的稳定性时出现了一个崩溃问题,报错堆栈在一个空指针对象访问其成员函数处,但是从整体代码执行流程分析发现其对象指针一直是空的,诡异的是不使用工具运行时无论如何都不会崩溃。

ASAN工具报错发生在如下行,此处mAudioRenderServicePtr为NULL。

this->OnStop(false, mAudioRenderServicePtr->GetAddr());

下面对正常运行和工具调试出现的结果一致进行分析,以及延伸的讨论一下关于空指针访问的一些陷阱。

分析

空指针即未指向任何对象,于是从语言层面来讲在访问空指针时应该一定发生崩溃,但在多函数调用及参数传递过程中,以及C++语言的封装特性、编译器是否优化的情况下,则不一定发生崩溃或者崩溃延迟。

先看看GetAddr这个成员函数的声明:

const MdfAddr& GetAddr(){ return mAddr; }

这里发现返回值为成员变量mAddr的常量引用,如下是反汇编对应的指令

    0xad0b9880 <+512>: ldr.w  r0, [r5, #0x124]
    0xad0b9884 <+516>: movs   r1, #0x0
    0xad0b9886 <+518>: add.w  r2, r0, #0x3c

从汇编指令看到,最后一行直接将this指针加上成员变量的偏移值0x3c并返回,也即返回的是mAddr的地址0x3c。

以及成员函数OnStop的函数声明

void OnStop(bool isAsync, const MdfAddr &srcAddr);

第二个参数实际使用的是MdfAddr的常量引用,因此参数会直接传递其对象地址0x3c,而不会访问0x3c指向的内存值,后面函数执行过程中也并未访问mAddr的成员变量,所以无工具运行时不会发生崩溃。

至于有工具时会发生崩溃,是因为ASAN会对所有地址访问进行监控,当出现异常时就会中断崩溃。

其它情况

  • 如上述的情况,如果我们在OnStop里实际访问了第二个参数里的成员变量,即实际访问了0x3c指针指向的内存,就会立即产生崩溃,比如OnStop的声明是这样

void OnStop(bool isAsync, MdfAddr srcAddr);

第二个参数会直接传递MdfAddr的值,因此会访问0x3c指向的实际内存,从而会立即导致崩溃,输出类似如下的信息

Build fingerprint: 'Xiaomi/cancro_wc_lte/cancro:6.0.1/MMB29M/8.9.13:user/release-keys'
02-21 16:43:27.589 290-290/? A/DEBUG: Revision: '0'
02-21 16:43:27.589 290-290/? A/DEBUG: ABI: 'arm'
02-21 16:43:27.589 290-290/? A/DEBUG: pid: 19854, tid: 19933, name: Thread-3246  >>> com.aliyun.aliyunvideosdkpro <<<
02-21 16:43:27.589 290-290/? A/DEBUG: signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
02-21 16:43:27.610 290-290/? A/DEBUG:     r0 00000000  r1 00004ddd  r2 00000006  r3 99c11978
02-21 16:43:27.610 290-290/? A/DEBUG:     r4 99c11980  r5 99c11930  r6 00000019  r7 0000010c
02-21 16:43:27.610 290-290/? A/DEBUG:     r8 0000000b  r9 b3472bd0  sl 00000000  fp 133822cc
02-21 16:43:27.610 290-290/? A/DEBUG:     ip 00000006  sp a8260188  lr b6816c69  pc b6819058  cpsr 400d0010
02-21 16:43:27.646 290-290/? A/DEBUG: backtrace:
02-21 16:43:27.646 290-290/? A/DEBUG:     #00 pc 00042058  /system/lib/libc.so (tgkill+12)
02-21 16:43:27.646 290-290/? A/DEBUG:     #01 pc 0003fc65  /system/lib/libc.so (pthread_kill+32)
02-21 16:43:27.646 290-290/? A/DEBUG:     #02 pc 0001c403  /system/lib/libc.so (raise+10)
02-21 16:43:27.646 290-290/? A/DEBUG:     #03 pc 000195b5  /system/lib/libc.so (__libc_android_abort+34)
02-21 16:43:27.646 290-290/? A/DEBUG:     #04 pc 00017508  /system/lib/libc.so (abort+4)
02-21 16:43:27.647 290-290/? A/DEBUG:     #05 pc 000a18b3  /system/lib/libclang_rt.asan-arm-android.so (_ZN11__sanitizer5AbortEv+40)
02-21 16:43:27.647 290-290/? A/DEBUG:     #06 pc 000a6449  /system/lib/libclang_rt.asan-arm-android.so (_ZN11__sanitizer3DieEv+60)
02-21 16:43:27.647 290-290/? A/DEBUG:     #07 pc 0008ffc0  /system/lib/libclang_rt.asan-arm-android.so (_ZN6__asan19ScopedInErrorReportD2Ev+352)
02-21 16:43:27.647 290-290/? A/DEBUG:     #08 pc 000900b8  /system/lib/libclang_rt.asan-arm-android.so (_ZN6__asan18ReportDeadlySignalEiRKN11__sanitizer13SignalContextE+160)
02-21 16:43:27.647 290-290/? A/DEBUG:     #09 pc 0008f0fc  /system/lib/libclang_rt.asan-arm-android.so (_ZN6__asan18AsanOnDeadlySignalEiPvS0_+188)
02-21 16:43:27.647 290-290/? A/DEBUG:     #10 pc 0036d2e1  /system/lib/libart.so (_ZN3art12FaultManager11HandleFaultEiP7siginfoPv+208)
02-21 16:43:27.647 290-290/? A/DEBUG:     #11 pc 0001756c  /system/lib/libc.so
  • 考虑另一种场景,如何实现的代码
class MyClass
{
public:
    int calcValue(int x, int y) const
    {
        return x + y;
    }
};

如果我们将一些实际未使用类成员变量的全局函数作为类的成员函数,那么在对象指针为NULL时访问这一类函数,则不会发生崩溃,因为在执行calcValue时this指针并不会用到。

总结

  • 如果直接访问空指针指向的内存,如内建类型,则会立即发生崩溃
  • 访问空指针对象指向的成员函数时,则不一定会发生崩溃
  • 使用引用或者常量引用类型的参数时,内存访问会延迟,内存访问异常也会延迟
上一篇:工作中遇到的C++语言基础和常见错误


下一篇:oracle 对只转发结果集的无效操作: last