目录

如何定位应用中Native层出现的空指针解引用CppCrash

问题现象

长稳测试的时候,偶现CppCrash崩溃,每次的崩溃栈不同,复现的场景不明确。查看多个崩溃日志都有出现SIGSEGV(SEGV_MAPERR)@0x0000000000000000 probably caused by NULL pointer dereference报错,其中之一的报错信息如下:

Generated by HiviewDFX@OpenHarmony
================================================================
Device info:HUAWEI Mate 60 Pro
Build info:ALN-AL00 5.0.0.150(SP8C00E150R4P30log)
Fingerprint:xxxxxxxxx
Module name:com.example.application
Version:2.8.5
VersionCode:30
PreInstalled:No
Foreground:No
Timestamp:2025-04-30 14:17:03.643
Pid:42378
Uid:20020210
Process name:com.example.application
Process life time:18446744073709405886s
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x0000000000000007  probably caused by NULL pointer dereference
Fault thread info:
Tid:42378, Name:m.example.application
#00 pc 0000000000135dc8 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
#01 pc 0000000000135d68 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
#02 pc 00000000001397c0 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
#03 pc 000000000012b0d8 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
#04 pc 000000000012aa78 /data/storage/el1/bundle/libs/arm64/libnet.so(doCallback(napi_env__*, napi_status, void*)+1584)(91a0f067f914ed44654e1191fccc295e60882cb0)
#05 pc 00000000000704f4 /system/lib64/platformsdk/libace_napi.z.so(NativeSafeAsyncWork::ProcessAsyncHandle()+596)(85e70f1f5cf0e81468ec141fc2f4922c)
#06 pc 0000000000016de4 /system/lib64/platformsdk/libuv.so(uv__async_io+352)(81fe2b78ed1df8447a6c4e51c5f644e2)
#07 pc 0000000000028840 /system/lib64/platformsdk/libuv.so(uv__io_poll+1016)(81fe2b78ed1df8447a6c4e51c5f644e2)
#08 pc 00000000000173cc /system/lib64/platformsdk/libuv.so(uv_run+408)(81fe2b78ed1df8447a6c4e51c5f644e2)
#09 pc 000000000007f250 /system/lib64/platformsdk/libruntime.z.so(OHOS::AbilityRuntime::OHOSLoopHandler::OnTriggered()+140)(17e438b05b67ccb9c7eb96e54ed833e5)
#10 pc 0000000000020430 /system/lib64/chipset-pub-sdk/libeventhandler.z.so(770d0cd78402a5ad1ced37fad50c5707)
#11 pc 000000000001bdb4 /system/lib64/chipset-pub-sdk/libeventhandler.z.so(OHOS::AppExecFwk::EventHandler::DistributeEvent(std::__h::unique_ptr<OHOS::AppExecFwk::InnerEvent, void (*)(OHOS::AppExecFwk::InnerEvent*)> const&)+1140)(770d0cd78402a5ad1ced37fad50c5707)

背景知识

CppCrash(进程崩溃)是指C/C++运行时崩溃。目前主要支持崩溃异常信号SIGBUS、SIGFPE、SIGSEGV、SIGSTKFLT、SIGSYS、SIGILL、SIGTRAP、SIGABRT的处理。可以通过FaultLogger模块获取崩溃日志。

崩溃日志报错中空指针解引用NULL pointer dereference表现形式一般为:

定位该类型问题一般尝试以下几种方法:

  1. 使用DevEco Studio中链接应用,尝试直接点击崩溃日志中的调用栈跳转到对应行号。在应用开发场景,对于应用自身的动态库生成的CppCrash堆栈可直接跳转到代码行处,支持Native栈帧和ArkTS栈帧,无需开发者自行进行解析行号操作。
  2. 对于未能解析跳转到对应行号的栈帧,可使用llvm-addr2line工具解析定位崩溃行号。注意在使用llvm-addr2line工具前需要将release编译的so带上符号表,同时将build-profile.json5中debugSymbol的strip字段设置为"false"。
  3. 使用ASan检测内存错误。使用DevEco Studio集成的ASan(Address-Sanitizer)来检测面向C/C++的地址报错,并通过FaultLog展示的错误堆栈详情及导致错误的代码行来进行问题定位。
  4. 结合业务检视代码。根据基于崩溃栈定位行号章节中介绍的三种方式获取到栈顶对应的行号后,回到代码中,检视上下文。

问题定位

  1. 由于出现问题的场景不明确,所以首先通过崩溃栈日志进行规律总结。经过查看多次的崩溃栈,发现基本都是在回调函数中出现的崩溃,比如以下的libnet.so中doCallback函数,于是怀疑问题出在各回调函数中的共性问题。
    pc 0000000000135dc8 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
    #01 pc 0000000000135d68 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
    #02 pc 00000000001397c0 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
    #03 pc 000000000012b0d8 /data/storage/el1/bundle/libs/arm64/libnet.so(91a0f067f914ed44654e1191fccc295e60882cb0)
    #04 pc 000000000012aa78 /data/storage/el1/bundle/libs/arm64/libnet.so(doCallback(napi_env__*, napi_status, void*)+1584)(91a0f067f914ed44654e1191fccc295e60882cb0)
  2. 以doCallback回调作为突破口,尝试通过添加空指针保护,发现还是会偶现该问题。于是怀疑是由于doCallback函数在执行过程中被释放了使用的指针内存资源。
    void doCallback(napi_env env, napi_status status, void *data) {
        if (env == nullptr || data == nullptr) {
            LOGE("Invalid env or data");
            return;
        }
        // ...
    }
  3. 使用llvm-addr2line(或者堆栈轨迹分析)对出现崩溃的libnet.so尝试解析出文件名和行号。
    • 首先将libnet模块的build-profile.json5里面buildOption/externalNativeOptions中配置参数:"arguments": "-DCMAKE_BUILD_TYPE=RelWithDebInfo"。
      {
        "apiType": "stageMode",
        "buildOption": {
          "externalNativeOptions": {
            "path": "./src/main/cpp/CMakeLists.txt",
            "arguments": ["-DCMAKE_BUILD_TYPE=RelWithDebInfo"],
            "cppFlags": "",
          }
        },
        // ...
      }
    • 然后将libnet模块的build-profile.json5里面buildOptionSet/nativeLib/debugSymbol/strip设置为"false"。(注意下方示例中的name为"debug"包,如需"release"包带上符号表则需要在"name"为"release"的构建配置中修改)。
      "buildOptionSet": [
          {
            "name": "debug",
            "arkOptions": {
              "obfuscation": {
                "ruleOptions": {
                  "enable": false,
                  "files": [
                    "./obfuscation-rules.txt"
                  ]
                }
              }
            },
            "nativeLib": {
              "debugSymbol": {
                "strip": false,
                "exclude": []
              }
            }
          },
        ],
    • 编译出libnet.so后,确认是否已带上符号表:
      • 在Linux中可使用file libnet.so命令,查看最后是否有not stripped字段,带了则表示编译出的libnet.so带上了符号表。
        [root@ncn4b-partnervectorservice-33-83-97 arm64-v8a]# file libnet.so 
        libnet.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=9d075a569e007bc6e4ea25f2f96be548724b4aef, with debug_info, not stripped
      • 在windows中可使用llvm-objdump --headers libnet.so(llvm-objdump工具在DevEco Studio安装的时候会自带),查看是否有debug字段,有则说明带上了符号表,没有则说明无符号表:

    • 使用编译出来的带符号表的libnet.so使用llvm-addr2line进行崩溃行号定位:
      llvm-addr2line -Cipe LocalPath/libnet.so 000000000012aa78
    • 得到出问题的行信息,定位到崩溃在napi_call_function函数这行。
      napi_call_function(env, nullptr, callback, 1, &returnValue, &tempValue);

    说明是napi_call_function在调用ArkTS函数指针的时候,回调函数指针可能被其他线程释放了才导致的踩空。

  4. 通过代码分析发现问题所在ArkTS层往Native层传递函数参数的时候,需要依赖的回调函数指针必须是有效的。

    但是从上层传下来的函数制造者却没有创建强引用napi_ref保留。所以当GC回收了ArkTS的回调函数资源后,使用该ArkTS回调函数可能在Native执行napi_call_function前就已经是空指针了,最后导致空指针解引用。

分析结论

修改建议

完整示例参考如下: