blob: 7bf9a9ee9f21097792355c48575c2a04163f1b1b [file] [log] [blame]
<html devsite><head>
<title>AddressSanitizer</title>
<meta name="project_path" value="/_project.yaml"/>
<meta name="book_path" value="/_book.yaml"/>
</head>
<body>
<!--
Copyright 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>AddressSanitizer (ASan) 是一种基于编译器的快速检测工具,用于检测原生代码中的内存错误。它与 Valgrind(Memcheck 工具)相差不大,但 ASan 具备以下独有特性:</p>
<ul>
<li>+ 会检测堆栈和全局对象是否有溢出
</li><li>- 不检测未初始化的读取和内存泄露
</li><li>+ 速度更快(与 Valgrind 的 20-100x 倍速设置相差 2-3 倍)
</li><li>+ 内存占用空间较少
</li></ul>
<p>本文档介绍了如何使用 AddressSanitizer 来编译和运行 Android 平台的各个组成部分。如果您希望利用 AddressSanitizer 编译独立的(即 SDK/NDK)应用,请改为参阅 <a href="https://github.com/google/sanitizers/wiki/AddressSanitizerOnAndroid">AddressSanitizerOnAndroid</a> 公共项目网站。</p>
<p>AddressSanitizer 包括一个编译器 (<code>external/clang</code>) 和一个运行时库 (<code>external/compiler-rt/lib/asan</code>)。</p>
<p class="note"><strong>注意</strong>:使用最新的 master 分支即可获得 <a href="#sanitize_target">SANITIZE_TARGET</a> 功能,并能够使用 AddressSanitizer 一次性编译整个 Android 平台。否则,您将只能使用 <code>LOCAL_SANITIZE</code></p>
<h2 id="building_with_clang">使用 Clang 编译</h2>
<p>要编译可通过 ASan 进行测试的二进制文件,第一步是要确保您的代码是使用 Clang 编译的。ASan 的 master 分支会默认执行这一步骤,因此您无需执行任何操作。如果您认为自己要测试的模块是使用 GCC 编译的,则可以向编译规则中添加 <code>LOCAL_CLANG:=true</code>,从而切换至 Clang。Clang 也许可以发现 GCC 遗漏的代码错误。</p>
<h2 id="building_executables_with_addresssanitizer">使用 AddressSanitizer 编译可执行文件</h2>
<p><code>LOCAL_SANITIZE:=address</code> 添加到可执行文件的编译规则中。</p>
<pre class="devsite-click-to-copy">
LOCAL_SANITIZE:=address
</pre>
<p>检测到错误时,ASan 会向标准输出和 <code>logcat</code> 发送一份详细报告,然后让相应进程崩溃。</p>
<h2 id="building_shared_libraries_with_addresssanitizer">使用 AddressSanitizer 编译共享库</h2>
<p>根据 ASan 的工作原理,不是通过 ASan 编译的可执行文件将无法使用通过 ASan 编译的库。</p>
<p class="note"><strong>注意</strong>:在运行时,如果将 ASan 库加载到错误的进程中,系统将会显示以 <code>_asan</code><code>_sanitizer</code> 开头的消息,以提示您有无法解析的符号。</p>
<p>要对多个可执行文件(并非所有这些可执行文件都是使用 ASan 编译的)使用的共享库进行测试,您需要该库的 2 个副本。为此,建议您针对相应的模块向 <code>Android.mk</code> 中添加以下内容:</p>
<pre class="devsite-click-to-copy">
LOCAL_SANITIZE:=address
LOCAL_MODULE_RELATIVE_PATH := asan
</pre>
<p>这样一来,系统会将库放置到 <code>/system/lib/asan</code>(而非 <code>/system/lib</code>)中。然后,使用以下方法运行您的可执行文件:<code>LD_LIBRARY_PATH=/system/lib/asan</code></p>
<p>对于系统守护程序,将以下内容添加到 <code>/init.rc</code><code>/init.$device$.rc</code> 的相应部分。</p>
<pre class="devsite-click-to-copy">
setenv LD_LIBRARY_PATH /system/lib/asan
</pre>
<p class="warning"><strong>警告</strong><code>LOCAL_MODULE_RELATIVE_PATH</code> 设置会将您的库<strong>移至</strong> <code>/system/lib/asan</code>,这意味着,如果从头开始重写并重新编译,将会导致 <code>/system/lib</code> 中缺少该库,并且生成的映像可能会无法启动。这是当前编译系统存在的一个令人遗憾的限制。请不要重写;而是执行 <code>make -j $N</code><code>adb
sync</code></p>
<p>通过读取 <code>/proc/$PID/maps</code>,验证相应进程使用的库是否来自 <code>/system/lib/asan</code>(如果此库存在)。如果不是,您可能需要停用 SELinux,如下所示:</p>
<pre class="devsite-click-to-copy">
<code class="devsite-terminal">adb root</code>
<code class="devsite-terminal">adb shell setenforce 0</code>
# restart the process with adb shell kill $PID
# if it is a system service, or may be adb shell stop; adb shell start.
</pre>
<h2 id="better_stack_traces">更出色的堆栈跟踪</h2>
<p>AddressSanitizer 使用基于帧指针的快速展开程序 (unwinder),根据程序中的每个内存分配和取消分配事件来记录堆栈跟踪信息。Android 的大部分组件都未使用帧指针进行编译。因此,您通常仅会获得 1 个或 2 个有意义的帧。要解决此问题,请使用 ASan(推荐)或以下选项重新编译库:</p>
<pre class="devsite-click-to-copy">
LOCAL_CFLAGS:=-fno-omit-frame-pointer
LOCAL_ARM_MODE:=arm
</pre>
<p>或者在进程环境中设置 <code>ASAN_OPTIONS=fast_unwind_on_malloc=0</code>。后者可能对 CPU 要求极高,具体取决于负载。</p>
<h2 id="symbolization">符号化</h2>
<p>最初,ASan 报告中包含对二进制文件和共享库中的偏移量的引用。您可以通过以下两种方法获取源文件和行信息:</p>
<ul>
<li>确保 <code>/system/bin</code> 中有 llvm-symbolizer 二进制文件。Llvm-symbolizer 根据 <code>third_party/llvm/tools/llvm-symbolizer</code> 中的源文件进行编译 </li><li>通过 <code>external/compiler-rt/lib/asan/scripts/symbolize.py</code> 脚本过滤报告。
</li></ul>
<p>由于可以使用主机上的符号化库,因此第二种方法可以提供更多数据(即 file:line 位置)。</p>
<h2 id="addresssanitizer_in_the_apps">在应用中使用 AddressSanitizer</h2>
<p>AddressSanitizer 无法检查 Java 代码,但可以检测 JNI 库中的错误。为此,您需要使用 ASan 编译可执行文件(在这种情况下是 <code>/system/bin/app_process(<em>32|64</em>)</code>)。这将在设备上的所有应用中同时启用 ASan,因而会给设备带来一些压力,但 2GB 内存的设备可以从容处理这种情况。</p>
<p><code>frameworks/base/cmds/app_process</code> 中的 app_process 编译规则添加常规的 <code>LOCAL_SANITIZE:=address</code>。暂时忽略同一个文件中的 <code>app_process__asan</code> 目标(如果在您阅读该文件时这个目标仍存在于其中)。在 <code>system/core/rootdir/init.zygote(<em>32|64</em>).rc</code> 中修改 Zygote 记录,以添加以下行:</p>
<pre class="devsite-click-to-copy">
setenv LD_LIBRARY_PATH /system/lib/asan:/system/lib
setenv ASAN_OPTIONS
allow_user_segv_handler=true
</pre>
<p>编译,然后依次执行以下命令:adb sync、fastboot flash boot、reboot。</p>
<h2 id="using_the_wrap_property">使用 wrap 属性</h2>
<p>上一部分中的方法将 AddressSanitizer 放置到了系统的每个应用中(实际上是放置到了 Zygote 进程的每个子项中)。您可以只通过 ASan 运行一个或少数几个应用,从而节省一些内存空间,但是应用启动速度会变慢。</p>
<p>为实现这一目标,您可以通过“wrap.”属性(与在 Valgrind 下运行应用时所用的属性相同)启动应用。下面是在 ASan 下运行 Gmail 应用的示例:</p>
<pre class="devsite-click-to-copy">
<code class="devsite-terminal">adb root</code>
<code class="devsite-terminal">adb shell setenforce 0 # disable SELinux</code>
<code class="devsite-terminal">adb shell setprop wrap.com.google.android.gm "asanwrapper"</code>
</pre>
<p>在此情况下,asanwrapper 会将 <code>/system/bin/app_process</code> 重写至(使用 AddressSanitizer 编译的)<code>/system/bin/asan/app_process</code>。此外,它还会在动态库搜索路径的开头处添加 <code>/system/lib/asan</code>。这样一来,通过 asanwrapper 运行应用时,系统会优先使用 <code>/system/lib/asan</code> 中可通过 ASan 进行测试的库,而非 <code>/system/lib</code> 中的普通库。</p>
<p>同样,如果发现错误,应用会崩溃,且系统会将报告记录到日志中。</p>
<h2 id="sanitize_target">SANITIZE_TARGET</h2>
<p>master 分支支持使用 AddressSanitizer 一次性编译整个 Android 平台。</p>
<p>在同一编译树中运行以下命令。</p>
<pre class="devsite-click-to-copy">
<code class="devsite-terminal">make -j42</code>
<code class="devsite-terminal">SANITIZE_TARGET=address make -j42</code>
</pre>
<p>在此模式下,<code>userdata.img</code> 中包含其他库,必须也刷写到设备上。请使用以下命令行:</p>
<pre class="devsite-terminal devsite-click-to-copy">
fastboot flash userdata &amp;&amp; fastboot flashall
</pre>
<p>写入时,现今的 Nexus 和 Pixel 设备会启动到该模式中的界面。</p>
<p>其工作原理是编译两组共享库:<code>/system/lib</code> 中的常规库(第一次 make 调用),<code>/data/asan/lib</code> 中可通过 ASan 进行测试的库(第二次 make 调用)。第二次编译出的可执行文件会覆盖第一次编译出的可执行文件。通过使用 PT_INTERP 中的“/system/bin/linker_asan”,可通过 ASan 进行测试的可执行文件会获得一个不同的库搜索路径(在该路径中,<code>/system/lib</code> 前面添加了 <code>/data/asan/lib</code>)。</p>
<p><code>$SANITIZE_TARGET</code> 的值变更时,编译系统会重写中间对象目录。这样一来,系统便会强制重新编译所有目标,同时保留 <code>/system/lib</code> 下已安装的二进制文件。</p>
<p>以下目标不能使用 ASan 进行编译:</p>
<ul>
<li>静态关联的可执行文件。
</li><li><code>LOCAL_CLANG:=false</code> 目标</li><li>不会针对 <code>SANITIZE_TARGET=address</code> 进行 ASan 操作的 <code>LOCAL_SANITIZE:=false</code>
</li></ul>
<p>在 SANITIZE_TARGET 编译中,系统会跳过此类可执行文件,且会将第一次 make 调用中编译的版本留在 <code>/system/bin</code> 中。</p>
<p>此类库只是未使用 ASan 进行编译,但它们仍然可包含来自其依赖的静态库的 ASan 代码。</p>
<h2 id="supporting_documentation">支持文档</h2>
<p><a href="https://github.com/google/sanitizers/wiki/AddressSanitizerOnAndroid">AddressSanitizerOnAndroid</a> 公共项目网站</p>
<p><a href="https://www.chromium.org/developers/testing/addresssanitizer">AddressSanitizer 和 Chromium</a></p>
<p><a href="https://github.com/google/sanitizers">其他 Google 排错程序</a></p>
</body></html>