회고록 블로그

[C언어 공부] sanitizer 공부하기 본문

3. Computer Science 공부/리눅스 및 유닉스

[C언어 공부] sanitizer 공부하기

김간장 2022. 4. 3. 01:32

※ 필자는 초초초초초보자입니다. 틀린 내용은 언제든지 피드백 부탁드립니다.

 

 

개요


리눅스에서 C 소스코드를 컴파일 하기 위해서 필요한 것 중 하나가 바로 gcc(다른 컴파일러도 있다)이다.

GCC는 GNU Compiler Collection의 약자로, GNU 컴파일러 모음이라고 한다.

 

https://gcc.gnu.org/ 에서 말하기를 GCC는 C, C++, Objective-C, Fortran, Ada, Go, D과 같은 언어의 라이브러리가 포함되어 있다고 한다.

GCC는 정말 감사하게도 여러 유용한 옵션을 제공하는데,

오늘은 그 중 하나인 sanitizer에 대해서 공부를 해보려고 한다.

 

 

Sanitizer?


한글로 그대로 발음해보면 새니타이저 정도가 될 것 같다.

일단 sanitizer에 대해서 공부를 해보자.

 

GCC에는 Program Instrumentation 옵션들이 있다.

위키백과에 의하면 컴퓨터 프로그래밍에서 instrumentation은 오류를 진단하거나, 추적 정보 를 쓰기 위해 제품의 성능 정보를 모니터하거나 측정하는 기능을 가리킨다고 한다.

 

GCC의 Program Instrumentation 옵션은 런타임 시 instrumentation를 제어하는 옵션이다.

이 옵션을 사용하는 이유는 1) 프로그램의 핫스팟(다른 것들보다 더 많이 실행되는 프로그램 명령)을 찾거나, 코드의 커버리지를 분석하거나, 프로필 기반 최적화에 사용하기 위해서 프로파일링 통계를 수집할 때 사용한다.

혹은 2) 런타임 체크를 추가해서 프로그래밍 에러를 감지하기도 한다. (유효하지 않은 포인터 역참조, 배열의 범위를 넘어가는 접근, 스택 스매싱과 같은 용납할 수 없는(?) 공격 등)

 

위키백과에 의하면 컴퓨터에서의 프로파일링(profiling) 또는 성능 분석은 프로그램의 시간 복잡도, 공간 복잡도, 함수 호출의 주기와 빈도 등을 측정하는 동적 프로그램 분석의 한 형태라고 한다.

 

참고자료 : https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

 

그리고 바로 이 Program Instrumentation 옵션 중 하나가 sanitizer이다.

 

Microsoft의 C++, C 및 어셈블러 공식 문서에 의하면 sanitizer는 결함 보고, 분석 및 방지를 위해 사용한다고 한다.

즉, Program Instrumentation의 옵션 중 하나 답게

런타임에서 발생하는 결함을 찾기 위해 사용하는 옵션인 것이다.

 

❖ gcc의 옵션으로 sanitizer가 있는건 맞지만, 반드시 gcc에서만 사용할 수 있는 것은 아니다.

clang 등에서도 사용할 수 있다.

아래에서 sanitizer 사용법을 읽다보면 알겠지만,

[sanitizer] 옵션을 사용한다는 것은 AddressSanitizer, UndefinedBehaviorSanitizer 등을 활성화 한다는 의미이다.

또한 여기에서 말하는 AddressSanitizer, UndefinedBehaviorSanitizer 등은 구글에서 개발한 도구이다.

따라서, gcc에만 국한되어 사용할 수 있는 도구라고 오해해서는 안된다. 

 

https://www.jetbrains.com/help/clion/google-sanitizers.html

 

Google sanitizers | CLion

 

www.jetbrains.com

 

 

Sanitizer의 세부 내용


위의 내용만 봐도 sanitizer는 막강한 옵션이라는 것을 알 수 있다.

그래서 그런지 이 옵션은 다양하게 사용될 수 있다.

아래는 gcc.gnu.org 에서 확인한 일부 옵션이다.

-fsanitizer=address
-fsanitizer=kernel-address
-fsanitize=hwaddress
-fsanitizer=kernel-hwaddress
-fsanitizer=pointer-compare
-fsanitizer=pointer-subtract
-fsanitizer=shadow-call-stack
-fsanitizer=thread
-fsanitizer=leak
-fsanitizer=undefined
...
출처 : https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

모든 내용을 다 공부하기는 어렵다. 시간적으로 여유가 많지 않으니까 말이다.

그래서 몇 가지 배워보고 싶은 내용만 집어서 매뉴얼을 보며 공부해보기로 했다.

 

-fsanitizer=address


이 옵션은 AddressSanitizer를 활성화시킨다.

 

Microsoft C++, C 및 어셈블러 공식 문서(Docs)에 의하면 AddressSanitizer는

아래와 같은 가양성 0으로 찾기 어려운 버그들을 노출한다고 한다.

  • alloc/dealloc 불일치 및 new/delete 형식 불일치
  • 힙에 대한 할당이 너무 클 때 (allocation-size-too-big)
  • calloc 오버플로 및 alloca 오버플로
  • double-free (double 'free') 및 free 후 사용
  • 힙 버퍼 오버플로
  • 스택 버퍼 오버플로 및 언더플로
  • etc...

출처 : https://docs.microsoft.com/ko-kr/cpp/sanitizers/asan?view=msvc-170

❖ 가양성 : 실제 농도는 정해진 허용치 미만이지만, 측정 농도가 그 허용치를 초과하는 성질

 

물론 Microsoft 공식  문서라서 리눅스/MacOS 등 환경에서는 조금 다를 수 있지만,

이런 것들을 탐지할 때 사용하면 좋을 것 같다는 느낌만 가져가도 될 것 같다.

 

-fsanitizer=thread


이 옵션은 ThreadSanitizer를 활성화한다.

ThreadSanitizer는 아래 글에 의하면 data race를 감지하는 감지기라고 한다.

 

https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual

 

 

❖ 데이터 레이스?

멀티 쓰레드/프로세스 환경에서 발생하는 오류 중 하나인데, 여러 쓰레드가 공유자원에 동시에 접근할 때 일어나는 경쟁 상황을 말한다.

예를 들어서, 변수 num의 값을 읽고 거기에 1을 5번 더한다고 했을 때

쓰레드0이 변수 num의 값을 읽어 +1을 하는 동시에

쓰레드1도 변수 num의 값을 읽어 +1을 하고,

또 동시에 쓰레드2가 변수 num의 값을 읽어 +1을 하는 상황이라면

(모두 동시에 접근해서 값을 더하고 있는 상황)

생각하기에는 10+1+1+1 = 13이 될 것 같지만 실제로는 10+1 = 11이 된다고 한다.

이 때문에 공유자원에 대해서 동기화 기법을 사용한다고 한다.

참고자료 : https://jungwoong.tistory.com/6

예전에 보안 공부하면서 레이스 컨디션 공격에 대해서 잠깐 공부했었는데, 그것과 비슷한 느낌의 개념인 것 같다.

 

-fsanitizer=leak


이 옵션은 LeakSanitizer를 활성화한다.

LeakSanitizer는 런타임 메모리 누수 감지기이다.

clang (gcc 컴파일은 아니지만 아마 비슷한 개념이지 않을까 싶다) 에서 말하기를 AddressSanitizer와 결합하여 메모리 오류와 메모리 누수를 모두 확인할 수 있다고 한다.

 

출처 : https://clang.llvm.org/docs/LeakSanitizer.html

 

-fsanitizer=undefined


사실 이 옵션을 공부하기 위해서 이 긴 공부들을 시작한 것이다.

이 옵션은 UndefinedBehaviorSanitizer(a.k.a. UBSan)를 활성화 시킨다.

즉, 런타임 시 정의되지 않은 동작을 감지하는 감지기를 활성화하는 것이다.

 

예를 들어서 0으로 나누기(1/0, 2/0, 3/0, 10/0, 11/0 등), 배열의 범위를 벗어나 액세스하기, 오버플로우, 언더플로우 등등이 있다.

개인적인 생각으로는 위에서 공부했던 -fsanitizer=address와 비슷한 것 같다.

 

왜 UBSan을 쓰면 좋은지 아직 감이 잡히지 않는다면, 아래의 예시를 생각해보자.

지금 우리 앞에 이런 코드가 있다.

#include <stdio.h>

int	main(void)
{
	int num = 0x7FFFFFFF; //int 범위의 최댓값

	num += 1;
	printf("%d", num);
	return (0);
}

 

이 코드를 실행해보면 아래와 같이 "런타임 에러 없이" 실행이 된다.

다만, 출력된 값은 우리가 원하는 값이 아니다. 

여기에서는 코드가 짧아서 한번에 원하는 값이 아니라는 것을 눈치챌 수 있고, 쉽게 찾을 수 있지만

코드가 길어질수록 찾기가 힘들어진다.

더구나 "에러"가 발생하는 것도 아니기 때문에 문제가 없다고 생각하고 배포하다가 큰 일이 터질 수도 있다.

 

이 상황에서 UBSan을 이용해보자.

참 감사하게도, 정수 오버플로우를 잡아서 알려준다.

이러한 정의되지 않은 문제를 잡아내기 위해서 UBSan을 사용한다.

 

 

그리고 UBSan은 하위 옵션이 있다.

가령 NULL 포인터를 역참조하는 경우만 검사하고 싶다면?

-fsanitize=null 옵션을 사용하면 된다.

 

정수 오버플로우만 검사하고 싶다면?

-fsanitize=signed-integer-overflow 옵션을 사용하면 된다.

 

UBSan을 잘 사용하면 프로그램을 코딩할 때 미처 고려/예상하지 못한 상황들을 점검할 수 있고,

그로 인해서 프로그램의 안정성이 떨어지는 위험 인자를 미리 제거할 수 있다. (오버플로우 등)

 

더 자세한 사항은 매뉴얼을 보자.

gcc 매뉴얼은 정말 친절하게 다 알려준다.

https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

 

Instrumentation Options (Using the GNU Compiler Collection (GCC))

Set the directory to search for the profile data files in to path. This option affects only the profile data generated by -fprofile-generate, -ftest-coverage, -fprofile-arcs and used by -fprofile-use and -fbranch-probabilities and its related options. Both

gcc.gnu.org

 

 

코드를 모두 작성하고, 잘 컴파일 된다고 방심하면 안될 것 같다.

앞으로 sanitizer를 잘 사용해서 안정성 있는 코드를 작성하도록 노력해야겠다.

Comments