Address Sanitizer

Android NDK hỗ trợ Address Sanitizer (còn gọi là ASan) kể từ API cấp 27 (Android O MR 1).

ASan là một công cụ dựa trên trình biên dịch giúp phát hiện các lỗi bộ nhớ trong mã gốc. ASan phát hiện:

  • Chặn tràn (overflow)/chặn trống (underflow) cho ngăn xếp (stack) và bộ nhớ khối xếp (heap)
  • Sử dụng bộ nhớ khối xếp sau khi giải phóng
  • Sử dụng ngăn xếp bên ngoài phạm vi
  • Giải phóng hai lần/giải phóng bộ nhớ chưa được cấp phát trước đó

Mức hao tổn CPU của ASan là khoảng 2x, mức hao tổn kích thước mã trong khoảng từ 50% đến 2x và mức hao tổn bộ nhớ khá lớn (tuỳ theo mô hình phân bổ, nhưng vào khoảng 2x).

Ứng dụng mẫu

Ứng dụng mẫu cho biết cách định cấu hình biến thể bản dựng cho ASan.

Tạo

Để tạo mã gốc (JNI) của ứng dụng bằng Address Sanitizer, hãy làm như sau:

ndk-build

Trong Application.mk:

APP_STL := c++_shared # Or system, or none.
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address

Đối với mỗi mô-đun trong Android.mk:

LOCAL_ARM_MODE := arm

CMake

Trong tệp build.gradle của mô-đun:

android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                // Can also use system or none as ANDROID_STL.
                arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
            }
        }
    }
}

Đối với mỗi mục tiêu trong CMakeLists.txt:

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

Chạy

Kể từ Android O MR1 (API cấp 27), một ứng dụng có thể cung cấp tập lệnh bao bọc môi trường shell có khả năng bao bọc hoặc thay thế quy trình của ứng dụng. Nhờ đó, một ứng dụng có thể gỡ lỗi có khả năng tuỳ chỉnh hoạt động khởi động ứng dụng, cho phép sử dụng ASan trên các thiết bị phát hành chính thức.

  1. Thêm android:debuggable vào tệp kê khai ứng dụng.
  2. Đặt useLegacyPackaging thành true trong tệp build.gradle của ứng dụng. Xem hướng dẫn về tập lệnh bao bọc môi trường shell để biết thêm thông tin.
  3. Thêm thư viện thời gian chạy ASan vào jniLibs của mô-đun ứng dụng.
  4. Thêm tệp wrap.sh có nội dung sau vào mỗi thư mục trong thư mục src/main/resources/lib.

    #!/system/bin/sh
    HERE="$(cd "$(dirname "$0")" && pwd)"
    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
    "$@"
    

Giả sử mô-đun ứng dụng của dự án có tên là app, cấu trúc thư mục cuối cùng phải bao gồm các thông tin sau:

<project root>
└── app
    └── src
        └── main
            ├── jniLibs
            │   ├── arm64-v8a
            │   │   └── libclang_rt.asan-aarch64-android.so
            │   ├── armeabi-v7a
            │   │   └── libclang_rt.asan-arm-android.so
            │   ├── x86
            │   │   └── libclang_rt.asan-i686-android.so
            │   └── x86_64
            │       └── libclang_rt.asan-x86_64-android.so
            └── resources
                └── lib
                    ├── arm64-v8a
                    │   └── wrap.sh
                    ├── armeabi-v7a
                    │   └── wrap.sh
                    ├── x86
                    │   └── wrap.sh
                    └── x86_64
                        └── wrap.sh

Dấu vết ngăn xếp

Address Sanitizer cần gỡ bỏ ngăn xếp trong mọi lệnh gọi malloc/realloc/free. Có hai cách:

  1. Bộ gỡ bỏ dựa trên con trỏ khung "fast" (nhanh). Bạn sẽ sử dụng công cụ này theo hướng dẫn trong phần xây dựng.

  2. Bộ gỡ bỏ CFI "slow" (chậm). Ở chế độ này, ASan sử dụng _Unwind_Backtrace. Phương thức này chỉ yêu cầu -funwind-tables (thường được bật theo mặc định).

Bộ gỡ bỏ nhanh là tuỳ chọn mặc định cho malloc/realloc/free. Bộ gỡ bỏ chậm là tuỳ chọn mặc định cho dấu vết ngăn xếp quan trọng. Bạn có thể bật bộ gỡ bỏ chậm cho tất cả dấu vết ngăn xếp bằng cách thêm fast_unwind_on_malloc=0 vào biến ASAN_OPTIONS trong wrap.sh.