使用 Clang 进行动态分析

本文档介绍如何使用 Clang 对 Python 及其库执行分析。除了执行分析之外,本文档还将介绍下载、构建和安装最新的 Clang/LLVM 组合(当前为 3.4)。

本文档不涵盖结果解读。有关结果解读的讨论,请参阅 Marshall Clow 的 使用 -fsanitize=undefined 测试 libc++。该博客文章详细探讨了 Clang 在 libc++ 中发现的问题。

什么是 Clang?

Clang 是 LLVM 编译器的 C、C++ 和 Objective C 前端。该前端提供对 LLVM 的优化器和代码生成器的访问权限。清理器或检查器是代码生成阶段的挂钩,用于对编译代码进行检测,以便标记可疑行为。

什么是清理器?

Clang 清理器是用于识别可疑和未定义行为的运行时检查器。检查在运行时使用实际运行时参数进行,因此将误报降至最低。

有许多可用的清理器,但应定期使用的两个清理器是地址清理器 (ASan) 和未定义行为清理器 (UBSan)。使用编译器选项 -fsanitize=address 调用 ASan,使用 -fsanitize=undefined 调用 UBSan。这些标志通过 CFLAGSCXXFLAGS 传递,有时通过 CCCXX 传递(除了编译器之外)。

可以在 控制代码生成 中找到清理器的完整列表。

注意

由于清理器在实际程序参数上在运行时操作,因此提供一组完整的正负自测非常重要。

Clang 及其清理器有优点(也有缺点)。它只是发现错误和提高代码质量的武器库中的一个工具。Clang 应与其他方法(包括代码审查、Valgrind、Coverity 等)配合使用。

Clang/LLVM 设置

本文档的这一部分介绍如何下载、构建和安装 Clang 和 LLVM。有三个组件需要下载和构建。它们是 LLVM 编译器、编译器前端和编译器运行时库。

在准备阶段,您应该创建一个临时目录。还要确保您使用的是 Python 2,而不是 Python 3。Python 3 将导致构建失败。

下载、构建和安装

执行以下操作以下载、构建和安装 Clang/LLVM 3.4。

# Download
wget https://llvm.net.cn/releases/3.4/llvm-3.4.src.tar.gz
wget https://llvm.net.cn/releases/3.4/clang-3.4.src.tar.gz
wget https://llvm.net.cn/releases/3.4/compiler-rt-3.4.src.tar.gz

# LLVM
tar xvf llvm-3.4.src.tar.gz
cd llvm-3.4/tools

# Clang Front End
tar xvf ../../clang-3.4.src.tar.gz
mv clang-3.4 clang

# Compiler RT
cd ../projects
tar xvf ../../compiler-rt-3.4.src.tar.gz
mv compiler-rt-3.4/ compiler-rt

# Build
cd ..
./configure --enable-optimized --prefix=/usr/local
make -j4
sudo make install

注意

如果您收到错误 'LibraryDependencies.inc' file not found,则确保您使用的是 Python 2,而不是 Python 3。如果您在切换到 Python 2 后遇到错误,请删除所有内容并重新开始。

在执行 make install 之后,编译器将安装在 /usr/local/bin 中,各种库将安装在 /usr/local/lib/clang/3.4/lib/linux/

$ ls /usr/local/lib/clang/3.4/lib/linux/
libclang_rt.asan-x86_64.a   libclang_rt.profile-x86_64.a
libclang_rt.dfsan-x86_64.a  libclang_rt.san-x86_64.a
libclang_rt.full-x86_64.a   libclang_rt.tsan-x86_64.a
libclang_rt.lsan-x86_64.a   libclang_rt.ubsan_cxx-x86_64.a
libclang_rt.msan-x86_64.a   libclang_rt.ubsan-x86_64.a

在 macOS 上,库安装在 /usr/local/lib/clang/3.3/lib/darwin/

$ ls /usr/local/lib/clang/3.3/lib/darwin/
libclang_rt.10.4.a                    libclang_rt.ios.a
libclang_rt.asan_osx.a                libclang_rt.osx.a
libclang_rt.asan_osx_dynamic.dylib    libclang_rt.profile_ios.a
libclang_rt.cc_kext.a                 libclang_rt.profile_osx.a
libclang_rt.cc_kext_ios5.a            libclang_rt.ubsan_osx.a
libclang_rt.eprintf.a

注意

您永远不必将库添加到项目中。Clang 会为您处理。如果您发现无法在 configure 期间通过 make 的隐式变量(CFLAGSCXXFLAGSCCCXXFLAGSLDFLAGS)传递 -fsanitize=XXX 标志,那么您应该在配置后修改 Makefile 以确保标志通过编译器传递。

安装程序有时不会安装所有需要的组件。例如,您可能想要运行 scan-build 或使用 scan-view 检查结果。您可以手动复制组件,方法是

sudo mkdir /usr/local/bin/scan-build
sudo cp -r llvm-3.4/tools/clang/tools/scan-build /usr/local/bin
sudo mkdir /usr/local/bin/scan-view
sudo cp -r llvm-3.4/tools/clang/tools/scan-view /usr/local/bin

注意

由于安装程序有时不会安装所有需要的组件,因此在您确信一切按预期工作之前,不要删除临时目录。如果缺少某个库,那么您应该在 Clang/LLVM 构建目录中搜索它。

Python 构建设置

本文档的这一部分涵盖了使用选项调用 Clang 和 LLVM,以便扫描程序使用其测试套件分析 Python。使用了两个检查器 - ASan 和 UBSan。

由于扫描程序是运行时检查器,因此最好尽可能多地进行正向和负向自测。自测永远不会嫌多。

总体思路是使用扫描程序标志进行编译和链接。在链接时,Clang 将包含所需的运行时库。但是,您不能使用 CFLAGSCXXFLAGS 通过编译器将选项传递给链接器,因为 BUILDPYTHON_testembed_freeze_importlib 的 makefile 规则不使用隐式变量。

作为没有链接器标志的解决方法,您可以通过编译器 - CCCXX 传递扫描程序选项。下面使用了通过编译器传递标志,但据报道通过 LDFLAGS 传递标志也可以正常工作。

构建 Python

首先,使用所需的扫描程序导出感兴趣的变量。可以同时指定两个扫描程序

# ASan
export CC="/usr/local/bin/clang -fsanitize=address"
export CXX="/usr/local/bin/clang++ -fsanitize=address -fno-sanitize=vptr"

# UBSan
export CC="/usr/local/bin/clang -fsanitize=undefined"
export CXX="/usr/local/bin/clang++ -fsanitize=undefined -fno-sanitize=vptr"

-fno-sanitize=vptr 删除了由于噪音而从 C++ 项目中移除的 UBSan 中的 vtable 检查。Python 不需要它,但您可能需要它用于其他 C++ 项目。

导出 CCCXX 后,正常地 configure

$ ./configure
checking build system type... x86_64-unknown-linux-gnu
checking host system type... x86_64-unknown-linux-gnu
checking for --enable-universalsdk... no
checking for --with-universal-archs... 32-bit
checking MACHDEP... linux
checking for --without-gcc... no
checking for gcc... /usr/local/bin/clang -fsanitize=undefined
checking whether the C compiler works... yes
...

接下来是标准 make(添加了格式以提高清晰度)

$ make
/usr/local/bin/clang -fsanitize=undefined -c -Wno-unused-result
    -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -I.
    -IInclude -I./Include -DPy_BUILD_CORE -o Modules/python.o
    ./Modules/python.c
/usr/local/bin/clang -fsanitize=undefined -c -Wno-unused-result
    -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -I.
    -IInclude -I./Include -DPy_BUILD_CORE -o Parser/acceler.o
    Parser/acceler.c
...

最后是 make test(添加了格式以提高清晰度)

Objects/longobject.c:39:42: runtime error: index -1 out of bounds
    for type 'PyLongObject [262]'
Objects/tupleobject.c:188:13: runtime error: member access within
    misaligned address 0x2b76be018078 for type 'PyGC_Head' (aka
    'union _gc_head'), which requires 16 byte alignment
    0x2b76be018078: note: pointer points here
    00 00 00 00  40 53 5a b6 76 2b 00 00  60 52 5a b6 ...
                 ^
...

如果您使用的是地址扫描程序,则通过 asan_symbolize.py 传递输出以获得良好跟踪非常重要。例如,从编译期间的问题 20953(添加了格式以提高清晰度)

$ make test 2>&1 | asan_symbolize.py
...

/usr/local/bin/clang -fsanitize=address -Xlinker -export-dynamic
    -o python Modules/python.o libpython3.3m.a -ldl -lutil
    /usr/local/ssl/lib/libssl.a /usr/local/ssl/lib/libcrypto.a -lm
./python -E -S -m sysconfig --generate-posix-vars
=================================================================
==24064==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x619000004020 at pc 0x4ed4b2 bp 0x7fff80fff010 sp 0x7fff80fff008
READ of size 4 at 0x619000004020 thread T0
  #0 0x4ed4b1 in PyObject_Free Python-3.3.5/./Objects/obmalloc.c:987
  #1 0x7a2141 in code_dealloc Python-3.3.5/./Objects/codeobject.c:359
  #2 0x620c00 in PyImport_ImportFrozenModuleObject
       Python-3.3.5/./Python/import.c:1098
  #3 0x620d5c in PyImport_ImportFrozenModule
       Python-3.3.5/./Python/import.c:1114
  #4 0x63fd07 in import_init Python-3.3.5/./Python/pythonrun.c:206
  #5 0x63f636 in _Py_InitializeEx_Private
       Python-3.3.5/./Python/pythonrun.c:369
  #6 0x681d77 in Py_Main Python-3.3.5/./Modules/main.c:648
  #7 0x4e6894 in main Python-3.3.5/././Modules/python.c:62
  #8 0x2abf9a525eac in __libc_start_main
       /home/aurel32/eglibc/eglibc-2.13/csu/libc-start.c:244
  #9 0x4e664c in _start (Python-3.3.5/./python+0x4e664c)

AddressSanitizer can not describe address in more detail (wild
memory access suspected).
SUMMARY: AddressSanitizer: heap-buffer-overflow
  Python-3.3.5/./Objects/obmalloc.c:987 PyObject_Free
Shadow bytes around the buggy address:
  0x0c327fff87b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff87c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff87d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff87e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff87f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c327fff8800: fa fa fa fa[fa]fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff8810: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff8820: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff8830: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff8840: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c327fff8850: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:     fa
  Heap right redzone:    fb
  Freed heap region:     fd
  Stack left redzone:    f1
  Stack mid redzone:     f2
  Stack right redzone:   f3
  Stack partial redzone: f4
  Stack after return:    f5
  Stack use after scope: f8
  Global redzone:        f9
  Global init order:     f6
  Poisoned by user:      f7
  ASan internal:         fe
==24064==ABORTING
make: *** [pybuilddir.txt] Error 1

注意

asan_symbolize.py 应该在 make install 期间安装。如果未安装,请在 Clang/LLVM 构建目录中查找它并将其复制到 /usr/local/bin

黑名单(忽略)发现结果

Clang 允许您在编译时提供一个特殊的黑名单文件,从而更改扫描程序工具在特定源代码级别的行为。需要黑名单是因为它会报告问题的每个实例,即使该问题在非托管库代码中被报告了数万次。

您可以使用 -fsanitize-blacklist=XXX 指定黑名单。例如

-fsanitize-blacklist=my_blacklist.txt

my_blacklist.txt 然后将包含以下条目。该条目将忽略 libc++ios 格式化函数中的一个 bug

fun:_Ios_Fmtflags

以 Python 3.4.0 为例,audioop.c 会产生一些发现

./Modules/audioop.c:422:11: runtime error: left shift of negative value -1
./Modules/audioop.c:446:19: runtime error: left shift of negative value -1
./Modules/audioop.c:476:19: runtime error: left shift of negative value -1
./Modules/audioop.c:504:16: runtime error: left shift of negative value -1
./Modules/audioop.c:533:22: runtime error: left shift of negative value -128
./Modules/audioop.c:775:19: runtime error: left shift of negative value -70
./Modules/audioop.c:831:19: runtime error: left shift of negative value -70
./Modules/audioop.c:881:19: runtime error: left shift of negative value -1
./Modules/audioop.c:920:22: runtime error: left shift of negative value -70
./Modules/audioop.c:967:23: runtime error: left shift of negative value -70
./Modules/audioop.c:968:23: runtime error: left shift of negative value -70
...

其中一个感兴趣的函数是 audioop_getsample_impl(在第 422 行标记),黑名单条目将包括

fun:audioop_getsample_imp

或者,你可以使用以下命令忽略整个文件

src:Modules/audioop.c

遗憾的是,在运行 sanitizer 之前,你不知道要将什么列入黑名单。

文档可在 Sanitizer 特殊情况列表 中找到。