Skip to main content
NRB Tech NRB Tech
← Back to Blog

Using Address Sanitizer (ASan) to Debug C/C++ Memory Issues in Android Applications

tutorial android cmake c asan

At NRB Tech, we sometimes use C or C++ libraries across multiple platforms, i.e. in the firmware, apps, and even on the server. This code sharing enables a solid foundation to be built which is well unit tested and works the same everywhere. It is very useful for data processing – encoding and decoding binary data formats for data transmission over Bluetooth, and cryptography – encrypting and decrypting data transmitted between devices.

However, this approach can cause issues. C is notorious for its unsafe memory management, which allows great flexibility, but can lead to memory corruption, data loss, data leakage, and crashes. Using best practices and unit testing code can mitigate some risk, but bugs can still occur, and debugging these issues can be tricky.

When developing for Android, Google have a number of tools available to help debug memory issues in NDK (Native Development Kit) C/C++ code. The approach recommended by Google is to use ASan, however the ASan documentation is currently (March ‘22, Android Studio 2021.1.1, Gradle 7, APIs 27+) missing crucial information to enable developers to debug these issues. In this article, we will provide a step-by-step guide to debug a memory issue. We have also created a working example project on GitHub.

From a stock Android project with CMake:

1. Set up the wrapper script and ASan libraries

To start your App with the address sanitizer library, your App needs to be run with a wrapper script. This is a script that is run on your test device and starts the application, modifying the environment variables and/or command arguments. As such, this script needs to be copied into your App at build time. Additionally, we need to copy the ASan libraries out of the NDK and into our project so they are copied into your App when building.

Both of these steps need to be done for 4 CPU architectures, so can be laborious. Instead of doing this by hand, we can use a script to set this up. Run this from the root of your project, modifying the NDK version if required.

#!/bin/bash
cd "$(dirname "$0")"

# This script sets up the required directories in app/src/sanitize.
# It can be run again if you change the ndk version.
ndkversion="24.0.8215888"

# store wrap.sh to variable
read -r -d '' script <<'EOT'
#!/system/bin/sh
HERE=$(cd "$(dirname "$0")" && pwd)

cmd=$1
shift

export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
ASAN_LIB=$(ls "$HERE"/libclang_rt.asan-*-android.so)
if [ -f "$HERE/libc++_shared.so" ]; then
  # Workaround for https://github.com/android-ndk/ndk/issues/988.
  export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"
else
  export LD_PRELOAD="$ASAN_LIB"
fi

os_version=$(getprop ro.build.version.sdk)

if [ "$os_version" -eq "27" ]; then
  cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@"
elif [ "$os_version" -eq "28" ]; then
  cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@"
else
  cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@"
fi

exec $cmd
EOT

# loop through all architectures and write/copy the required files
archs=( "arm64-v8a" "armeabi-v7a" "x86" "x86_64" )
asanarchs=( "aarch64" "arm" "i686" "x86_64" )

for i in "${!archs[@]}"; do
  mkdir -p app/src/sanitize/{jniLibs,resources/lib}/${archs[$i]}
  cp $ANDROID_HOME/ndk/$ndkversion/toolchains/llvm/prebuilt/*/lib64/clang/*/lib/linux/libclang_rt.asan-${asanarchs[$i]}-android.so app/src/sanitize/jniLibs/${archs[$i]}/
  echo "$script" > app/src/sanitize/resources/lib/${archs[$i]}/wrap.sh
done

2. Update CMakeLists.txt

In your CMakeLists.txt, add the following (replacing ${TARGET} as required with the name of your target):

if(SANITIZE)
    target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
    set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)
endif()

3. Update build.gradle

In your app’s build.gradle, to your android.buildTypes closure, add:

sanitize {
    initWith debug
    externalNativeBuild {
        cmake {
            arguments "-DANDROID_STL=c++_shared", "-DSANITIZE=TRUE"
        }
    }
}

4. Build and run

Sync Gradle and switch your active variant to “sanitize” in the “Build Variants” tab (bottom left).

5. Debug and analyze

Debug your App and reproduce the issue. You should see output from address sanitizer (filter by “wrap.sh”). In the stack traces you will see lines like:

#1 0x5e2cdaf76c  (/data/app/~~lTqqcXu2okmchd_nMeTm7w==/io.nrbtech.asanexample-spc73OiysYUjJ2984-N4kw==/lib/arm64/libasanexample.so+0x76c)

Where libasanexample.so is the name of the compiled C library and 0x76c represents the offset into the library the stack trace is pointing to.

6. Convert addresses to line numbers

We need to convert that address offset to a file and line number to understand where the problem occurred. The following script uses llvm-symbolizer in the NDK to do this:

#!/bin/bash
ndkversion="24.0.8215888"
libname="asanexample" # change to your library name

# default arch
arch="arm64-v8a"

POSITIONAL_ARGS=()

if [ $# -eq 0 ]; then
    echo "Pass an address as the last argument, use -a to provide architecture"
    exit 0
fi

while [[ $# -gt 0 ]]; do
    case $1 in
        -a|--arch)
            arch="$2"
            shift
            shift
            ;;
        *)
            POSITIONAL_ARGS+=("$1")
            shift
            ;;
    esac
done

set -- "${POSITIONAL_ARGS[@]}"

$ANDROID_HOME/ndk/$ndkversion/toolchains/llvm/prebuilt/*/bin/llvm-symbolizer \
    --obj=app/build/intermediates/cmake/sanitize/obj/$arch/lib$libname.so $1

Copy this to the root of your project and modify for your C library name and NDK version. You can then run this script like so:

./symbolize.sh 0x76c

The output contains the path to the line of code pointed to by the stack trace, enabling you to find where the issue occurred and figure out the problem.

7. Clean up

ASan can slow down your App, so be sure to switch back to the debug variant when you have resolved your issue.

Good luck resolving your own memory problems in Android Apps! If you need further help, feel free to reach out to NRB Tech!