android 如何分析应用的内存(十一)——ASan

android 如何分析应用的内存(十一)

接下来是,heap的第五大板块——ASan(Address Sanitizer)和HWASan(Hardware Address Sanitizer)。可以将其称为:地址清理器

与其说是Heap板块,不如说是debug板块。

ASan是一个集成在编译器中的工具,因此只需要在编译的时候设置好Flag即可。而HWASan则可以认为是ASan的plus版本。HWASan比ASan有如下的优点:

  1. 更小的内存开销
  2. 还可以检测,返回之后的堆栈使用情况

注意:自2023年起,ASan不再支持,建议使用HWASan。

本文章先简单介绍一下原理,然后详细介绍ASan的使用,以及对输出结果的解析。在下一篇中介绍HWASan的使用

原理简述

ASan在编译和链接阶段,将一些特殊的检查代码和内存管理代码插入到程序中。当程序运行时,这些插入的代码将负责管理内存的分配和释放。

比如:当应用程序调用malloc时,实际上调用的是Asan提供的malloc版本。ASan版本的malloc除了基本的内存分配以外,还会做额外的动作如:在分配的内存周围加上一个特殊区域(Red zones)
用于检测内存越界问题;同时对分配的内存每8个字节一组,分配一个影子内存,记录该组的使用情况,这样可以精确的记录每个内存的使用情况。

除了上面的介绍的Red Zone和影子内存外,ASan还会使用,如下的技术:

  1. 内存填充:对已经释放的内存,填充特殊的字节,如果访问这些内存,就会触发相应的错误
  2. 内存泄漏:在程序退出时,会检测所有的内存,查看是否存在未释放的内存。

注意:ASan似乎和malloc debug功能一致。事实上,因为ASan可以在编译和链接阶段插入代码,它比malloc debug的动态检测更加丰富和齐全。当然ASan会有更大的开销。

将ASan加入编译

现在只需要修改相应的Flag即可。如下:

## 在编译的时候,启用ASan,并且不要省略栈帧(-fno-omit-frame-pointer),这对于打印可读的栈帧非常友好
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
## 在链接的时候,启用ASan
APP_LDFLAGS := -fsanitize=address

ASan对内存布局和寻址方式上面有一定的要求,如果是arm架构,需要明确指定,以arm模式编译,而不是thumb模式编译如下:

## 在每一个Android.mk中都需要添加如下的代码
LOCAL_ARM_MODE := arm

在这里插入图片描述

除了makefie以外,还可以使用Cmakefile如下:

## 设置编译选项,启用ASan功能,并且不省略栈帧(-fno-omit-frame-pointer)
target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
## 设置链接选项,启用ASan功能
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)

在这里插入图片描述

同样也需要设置arm模式编译,如下:

defaultConfig {
        externalNativeBuild {
            cmake {
                abiFilters "arm64-v8a"
                cmake {
                    arguments "-DANDROID_ARM_MODE=arm"
                }
            }
        }
    }

自定义应用的启动过程

因为ASan需要使用各种动态库,而这些动态库在Android设备默认是没有的。因此需要将这些动态库,放入Android设备中。

有两种方法将相应的动态库放入设备中:

  1. 自定义APP的启动过程,使用wrap.sh。
  2. 使用NDK提供的脚本。

方法一:使用wrap.sh

wrap.sh的详细介绍,参见:android 如何分析应用的内存(七)下面只做使用说明。

  • 在AndroidManifest.xml中添加android:debuggable=“true”
  • 在build.gradle中使用 useLegacyPackaging。见android 如何分析应用的内存(七)
  • 将 ASan 运行时库添加到应用模块的 jniLibs 中。
    ASan的运行库在:NDK目录/toolchains/llvm/prebuilt/host平台/lib64/clang/版本/lib/linux/
    分别为一下四个:
    1. libclang_rt.asan-aarch64-android.so;
    2. libclang_rt.asan-arm-android.so;
    3. libclang_rt.asan-i686-android.so;
    4. libclang_rt.asan-x86_64-android.so
  • 然后将如下内容添加到wrap.sh中
#!/system/bin/sh
## 获取当前脚本的路径,并将其赋值给HERE
HERE="$(cd "$(dirname "$0")" && pwd)"
## 定义几个环境变量,其中allow_user_segv_handler表示当程序运行出现问题时,内核发出的
## SIGSEGV信号,可以被处理,而不是简单的结束程序
export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
## ASAN_LIB为运行时库
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
## 执行程序
"$@"

下面是一个例子的截图
在这里插入图片描述

上面的例子我只放入了arm64-v8a的运行时库。同时其他的so库,为前面文章需要的so库。

注意:使用wrap.sh只能是大于等于 Android 8.1的设备

方法二:使用NDK中的脚本

使用如下的脚本:NDK目录/toolchains/llvm/prebuilt/host平台/lib64/clang/版本/bin/asan_device_setup.
将运行时库,push到设备中,该设备必须能够取得root权限。功能如同wrap.sh中一样。asan_device_setup的内部细节不在赘述,因为这里更加推荐wrap.sh的用法。事实上,asan_device_setup会修改相应的app_process,让它能够方便的加载一些预定义库。

注意:这种方法,并未得到官方的大力支持,因此部分设备可能存在错误

如果想要撤销对应的运行时库,可如下运行:

asan_device_setup --revert

检测是否成功

做如下代码验证:

auto *p1 = new int;
*(p1+4) = 2345678;

可在log中看到如下的错误
在这里插入图片描述

对输出进行解析

选取libtest_malloc.so加号后面的地址,传递给llvm-symbolizer或者addr2line.举例如下:

~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-symbolizer -e ./app/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libtest_malloc.so 0x289a0

Java_com_example_test_1malloc_MainActivity_stringFromJNI
/Users/biaowan/AndroidStudioProjects/Test_Malloc.old/app/src/main/cpp/native-lib.cpp:212:13

可以解析问题点在具体的文件和行数。

在这里插入图片描述

ASan的高级用法

  1. 忽略特定函数。
    在一些已知问题,或者耗时特别长的函数上,可以使用属性__attribute__((no_sanitize_address))来添加忽略。如下:
__attribute__((no_sanitize_address))
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_test_1malloc_MainActivity_stringFromJNI(JNIEnv* env,jobject /* this */){
    
    }

在这里插入图片描述

  1. 出现错误不中断,而是继续运行
  • 编译时添加-fsanitize-recover=address
  • 在wrap.sh中添加ASAN_OPTIONS=halt_on_error=0

在这里插入图片描述
在这里插入图片描述

  1. 查看ASAN_OPTIONS支持那些选项,添加ASAN_OPTIONS=help=1

可以看见如下选项

在这里插入图片描述
其中可以打开,new和delete不匹配的检测,malloc和demalloc不匹配的检查等

  1. 将错误保存在特定的路径下

添加ASAN_OPTIONS=log_path=/sdcard/asan

会将信息输出到/sdcard/asan.pid文件中

  1. 打开泄漏检测(待定)

ASAN_OPTIONS=detect_leaks=true

注意:实验未通过,没有很好的检查出内存泄漏

  1. 崩溃的时候,生成coredump

ASAN_OPTIONS=disable_coredump=0

注意:如果无法生成coredump,则需要检查,Android是否打开了coredump。因为每个平台的打开coredump 的步骤不同,因此具体平台,请参阅相应文档

  1. ASAN单步

ASan还可以和gdb和lldb联合使用。gdb和lldb的使用,见前面的章节。

如果想要gdb或lldb停留在ASan报告错误之前,可以在如下函数设置断点:

__asan::ReportGenericError

如果想要gdb或lldb停留在ASan报告错误之后,可以在如下函数设置断点:

__sanitizer::Die

如果想要gdb或lldb打印Asan描述的内存信息,可以调用下面的函数

__asan_describe_address(地址)

如下:(gdb例子)

(gdb) set overload-resolution off
(gdb) p __asan_describe_address(0x7ffff73c3f80)
0x7ffff73c3f80 is located 0 bytes inside of 10-byte region [0x7ffff73c3f80,0x7ffff73c3f8a)
freed by thread T0 here: 
...

第一句话是关闭重载函数的解析。第二句话是,调用__asan_describe_address函数,解析0x7ffff73c3f80地址。输出则为对应地址的描述信息

至此,Android的ASan介绍完毕,下一篇会介绍HWASan的使用,因为HWAsan需要编译AOSP,篇幅较长,敬请期待

猜你喜欢

转载自blog.csdn.net/xiaowanbiao123/article/details/131650409