Skip to main content
A native crash on Android (C or C++ code, built with the NDK) reaches Apsio as the frames of the system’s crash report: each frame names a library, an offset into it and the library’s GNU build id. Apsio turns libshop.so + 0x398 into cart_total in shop.c:13, inlined frames included, with that library’s debug information. See Android native code for what is symbolicated. Native crash reports come from Android 12 and later. The stack of a native crash on older versions is not available to apps.

With the Gradle plugin

The Apsio Gradle plugin (io.apsio.gradle) does it on every release build. Its nativeSymbols option is on by default:
  • Release build types get ndk.debugSymbolLevel = "FULL" unless they set it, so AGP keeps each library’s debug information in a separate .so.dbg file. The libraries in the app are still stripped.
  • After the build, the plugin uploads those files with apsio upload ndk, under the build id it writes into the manifest, next to the R8 mapping. It uses the token in APSIO_TOKEN, as for the mapping.

With the CLI

From a build script or CI, after the build:
  • The directory (here native-debug-symbols) holds one subdirectory per ABI, arm64-v8a, armeabi-v7a, x86, x86_64 and riscv64, with the libraries’ ELF files that have debug information: the .so.dbg files of debugSymbolLevel = "FULL" (AGP also zips them, in this layout, into app/build/outputs/native-debug-symbols/<variant>/native-debug-symbols.zip for Google Play: unzip it and pass the folder), or unstripped .so files.
  • Each file is uploaded under its GNU build id, the id native crashes carry, with its ABI and the library’s name. A file that is not ELF, has no build id, has no symbols (a stripped library) or belongs to another ABI is skipped with a warning.
  • --build-id is the build id in the app’s manifest, the one its R8 mapping uses (see Give the build an id). Apsio records which libraries that build ships.
  • A library Apsio already has is not sent again: a library you did not change keeps its build id across builds. --force uploads everything again. --dry-run checks the files without a token or an upload, and --json prints a report.
  • The command succeeds when every library was uploaded or Apsio had it already. When any upload fails it exits with an error (4 when the server rejected the files, 1 for network or server errors), after uploading the others, so a build that treats upload errors as failures stops.
Link your libraries with a build id: the NDK’s toolchain does it by default (-Wl,--build-id). A library without one cannot be matched to its crashes.

When a native crash stays unsymbolicated

  • The build’s debug files were never uploaded, or come from another build: a rebuild of the same code can produce other build ids.
  • The library was linked without a build id.
  • The frame is in a library of the system (libc.so, libart.so): those keep the function name the device gives and are never listed as missing.
The release’s missing symbols list your libraries that crashes used and that have no upload, by the debug id made from their build id. Crashes that arrived before the upload are symbolicated when it lands, and grouped again by function names.