Clang을 사용한 동적 분석

이 문서는 Clang을 사용하여 Python과 그 라이브러리를 분석하는 방법을 설명합니다.

이 문서는 분석 결과를 해석하는 방법은 다루지 않습니다. 결과 해석에 관한 논의는 Marshall Clow의 Testing libc++ with -fsanitize=undefined를 참조하십시오. 이 블로그 게시물은 Clang이 libc++에서 발견한 문제들을 자세히 살펴봅니다.

이 문서는 Clang을 중심으로 다루지만, 대부분의 기법은 일반적으로 GCC의 새니타이저에도 적용될 것입니다.

이 지침은 Linux에서 테스트되었지만, macOS에서도 동작할 것입니다. Windows용 지침은 완전하지 않습니다.

Clang이란 무엇입니까?

Clang은 LLVM 컴파일러를 위한 C, C++, Objective C 프런트엔드입니다. 프런트엔드는 LLVM의 최적화기와 코드 제너레이터에 대한 접근을 제공합니다. 새니타이저(또는 검사기)는 코드 생성 단계에 연결되는 훅으로, 컴파일된 코드를 계측하여 의심스러운 동작이 표시되도록 합니다.

새니타이저란 무엇입니까?

Clang 새니타이저는 의심스러운 동작과 정의되지 않은 동작을 식별하는 데 사용되는 런타임 검사기입니다. 검사는 실제 런타임 매개변수를 사용하여 런타임에 이루어지므로 거짓 양성이 최소한으로 유지됩니다.

사용할 수 있는 새니타이저는 여러 가지가 있지만, 정기적으로 사용해야 하는 두 가지는 Address Sanitizer(또는 ASan)와 Undefined Behavior Sanitizer(또는 UBSan)입니다. ASan은 컴파일러 옵션 -fsanitize=address로 호출되고, UBSan은 -fsanitize=undefined로 호출됩니다. 이 플래그들은 CFLAGS와 CXXFLAGS를 통해 전달되며, 때로는 (컴파일러와 함께) CC와 CXX를 통해 전달되기도 합니다.

새니타이저의 전체 목록은 Controlling Code Generation에서 확인할 수 있습니다.

참고

새니타이저는 런타임에 실제 프로그램 매개변수를 대상으로 동작하므로, 양성 및 음성 자체 테스트를 빠짐없이 갖추는 것이 중요합니다.

Clang과 그 새니타이저에는 장점(과 단점)이 있습니다. 이는 버그를 찾아내고 코드 품질을 높이기 위한 여러 도구 중 하나일 뿐입니다. Clang은 코드 리뷰, Valgrind 등 다른 방법을 보완하는 용도로 사용해야 합니다.

Clang/LLVM 설정

대부분의 플랫폼에서 미리 빌드된 Clang을 사용할 수 있습니다:

  • macOS에서는 Clang이 기본 컴파일러입니다.

  • 주요 Linux 배포판에서는 clang 패키지를 설치할 수 있습니다. 경우에 따라 llvm도 별도로 설치해야 하며, 그렇지 않으면 일부 도구를 사용할 수 없습니다.

  • Windows에서는 Visual Studio(Code 아님) 설치 프로그램에 “C++ clang tools for windows” 기능이 포함되어 있습니다.

clang을 소스에서 직접 빌드할 수도 있습니다. 자세한 내용은 clang 문서를 참조하십시오.

설치 프로그램이 필요한 구성 요소를 모두 설치하지 않는 경우가 가끔 있습니다. 예를 들어, scan-build를 실행하거나 scan-view로 결과를 살펴보고 싶을 수 있습니다. 이러한 경우에는 Clang을 소스에서 빌드한 후 tools/clang/tools에 있는 도구를 PATH에 있는 디렉터리로 복사하면 됩니다.

소스에서 빌드하는 또 다른 이유는 플랫폼의 배포 채널이 아직 최신 버전의 Clang/LLVM을 제공하지 않을 때 이를 얻기 위해서입니다. Clang/LLVM의 새 버전에는 새로운 새니타이저 검사가 도입됩니다.

Python 빌드 설정

문서의 이 부분에서는 테스트 스위트로 실행되는 Python을 새니타이저가 분석하는 데 필요한 옵션을 지정해 Clang과 LLVM을 호출하는 방법을 다룹니다.

Clang이 기본 컴파일러가 아니라면 컴파일러를 Clang으로 설정하십시오:

export CC="clang"

추가 새니타이저 옵션(Clang 문서에서 찾을 수 있습니다)을 사용하려면 CFLAGS 변수에 추가하십시오. 예를 들어, 검사 대상 프로세스가 첫 번째 실패 후 종료되도록 하고 싶을 수 있습니다:

export CFLAGS="-fno-sanitize-recover"

그런 다음, 관련 플래그와 함께 ./configure를 실행하십시오:

  • ASan: --with-address-sanitizer --without-pymalloc

  • UBsan: --with-undefined-behavior-sanitizer

--without-pymalloc 옵션이 반드시 필요한 것은 아니지만(이 옵션 없이도 테스트는 통과해야 합니다), pymalloc을 비활성화하면 ASan이 더 많은 버그를 찾아내는 데 도움이 됩니다(ASan은 pymalloc이 수행하는 개별 할당을 추적하지 않습니다).

두 새니타이저를 모두 지정해도 됩니다.

그런 다음, 평소처럼 make와 make test를 실행하십시오. 방금 컴파일된 Python이 빌드의 후반 단계에서 실행되므로 make 자체가 새니타이저 오류로 실패할 수도 있다는 점에 유의하십시오.

모든 코드에 새니타이저를 활성화하기 위한 빌드 설정

Python의 일부 구성 요소(예: _testembed, _freeze_importlib, test_cppext)는 configure가 설정한 변수를 사용하지 않을 수 있으며, 위 설정으로는 새니타이저 없이 컴파일됩니다.

우회 방법으로, 컴파일러인 CC(C용)와 CXX(C++용)를 통해 새니타이저 옵션을 전달할 수 있습니다. 아래에서는 이 방법을 사용합니다. LDFLAGS를 통해 옵션을 전달하는 방법도 동작한다고 보고되었습니다.

ASan의 경우 다음을 사용하십시오:

# ASan
export CC="clang -fsanitize=address"
export CXX="clang++ -fsanitize=address -fno-sanitize=vptr"

UBSan의 경우에는 다음을 사용하십시오:

# UBSan
export CC="clang -fsanitize=undefined"
export CXX="clang++ -fsanitize=undefined -fno-sanitize=vptr"

두 새니타이저를 모두 지정해도 됩니다.

그런 다음, 평소처럼 ./configure, make, 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 ...
                 ^
...

주소 새니타이저를 사용하는 경우, 쓸 만한 트레이스를 얻으려면 추가 도구가 필요합니다. 이 작업은 보통 llvm-symbolizer 도구를 통해 자동으로 이루어집니다. 이 도구가 PATH에 설치되어 있지 않다면 ASAN_SYMBOLIZER_PATH를 도구의 위치로 설정하거나, 테스트 출력을 Clang 배포판에 포함된 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가 설치되어 있지 않다면 Clang을 소스에서 빌드한 다음, Clang/LLVM 빌드 디렉터리에서 이 파일을 찾아 직접 사용하거나 PATH에 있는 디렉터리로 복사하십시오.

발견 사항 무시하기

Clang에서는 컴파일 시점에 특수한 무시 목록(ignorelist) 파일을 제공하여 특정 소스 수준에서 새니타이저 도구의 동작을 변경할 수 있습니다. 새니타이저는 관리되지 않는 라이브러리 코드에서 같은 문제가 수만 번 보고되더라도 문제가 발생한 모든 경우를 보고하므로 무시 목록이 필요합니다.

무시 목록은 -fsanitize-ignorelist=XXX로 지정합니다. 예를 들면 다음과 같습니다:

-fsanitize-ignorelist=my_ignorelist.txt

그러면 my_ignorelist.txt에는 다음과 같은 항목이 들어갑니다. 이 항목은 libc++의 ios 포매팅 함수에 있는 버그를 무시합니다:

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 special case list에서 볼 수 있습니다.