작은 함수를 테스트해보기
이 튜토리얼은 KLEE를 사용하여 간단한 함수를 테스트하는 데 필요한 주요 단계를 설명한다.
다음은 get_sign이라는 간단한 함수이다.
int get_sign(int x) { if (x == 0) return 0; if (x < 0) return -1; else return 1; }
이 예제의 전체 코드는 examples/get_sign 아래의 소스 트리에서 찾을 수 있다.
소스 코드의 버전은 여기에서도 확인할 수 있다.
심볼릭으로 입력 표시하기
get_sign 함수를 KLEE를 사용하여 테스트하기 위해서는, 우리가 심볼릭 입력을 통해 실행시킬 필요가 있다.
변수를 심볼릭으로 표시하기 위해서는, klee/klee.h 헤더에 정의된 klee_make_symbolic() 함수를 사용할 수 있다.
이 함수는 세 개의 인수를 받을 수 있는데, 우리가 심볼로 다루고자 하는 변수의 주소 (=메모리 위치), 그 크기, 그리고 이름(임의로 지정 가능) 이렇게 구성되어 있다.
다음은 변수 ‘a’ 를 심볼릭으로 표시하고, 이를 통해 get_sign() 함수를 호출하는 간단한 main() 함수 예제이다!
int main() { int a; klee_make_symbolic(&a, sizeof(a), "a"); return get_sign(a); }
LLVM bitCode로 컴파일하기
KLEE는 LLVM bitCode에서 작동한다.
KLEE를 사용하여 프로그램을 실행시키려면, 먼저 clang 컴파일러에서 -emit-llvm 옵션을 사용하여 LLVM bitCode로 컴파일해야 된다.
$ clang -I /snap/klee/6/usr/local/include -emit-llvm -c -g -O0 -Xclang -disable-O0-optnone get_sign.c
위 명령을 실행하면, LLVM bitCode 형식인 get_sign.bc 파일을 생성시킨다.
-I 옵션은 컴파일러가 KLEE 가상 머신과 상호 작용하기 위해 사용되는 내장 함수의 정의를 포함하는 klee/klee.h 헤더 파일을 찾을 수 있도록 사용된다.
이러한 내장 함수에는 klee_make_symbolic과 같은 함수들도 포함된다.
-g 옵션은 bitCode 파일에 소스 줄번호와 함께 담긴 디버깅 정보를 생성하도록 만든다.
KLEE에 전달되는 bitcode가 최적화되어서는 안된다. KLEE의 –optimize 커맨드 라인 옵션을 사용하여 활성화할 수 있는 KLEE의 최적화를 이미 직접 선택했기 때문이다.
그러나 최신 LLVM 버전(5.0 이상)에서는 -O0 옵션을 KLEE를 위해 컴파일할 때 사용해서는 안된다.
KLEE가 자체 최적화를 수행하는 것을 방해하기 때문이다. 그 대신에 -O0 -Xclang -disable-O0-optnone 옵션을 사용해야 된다.
(자세한 내용은 이 이슈를 참조할 것)
테스트 케이스를 다시 실행시킬 의도가 없고 디버그 정보나 최적화에 관심이 없다면 klee/klee.h를 삭제한 후 get_sign.c를 다음과 같이 컴파일할 수 있다:
$ clang -emit-llvm -c get_sign.c
그러나 더 긴 명령어를 사용하는 것을 권장한다.
KLEE 실행하기
bitCode 파일에서 KLEE를 실행하려면 다음 명령을 실행한다.
$ klee get_sign.bc
그러면 다음과 같은 출력을 얻게 된다.
KLEE: output directory is "/home/ubuntu/whitehat/IOT-research/klee-study/klee-out-0" KLEE: Using STP solver backend KLEE: SAT solver: MiniSat KLEE: done: total instructions = 33 KLEE: done: completed paths = 3 KLEE: done: partially completed paths = 0 KLEE: done: generated tests = 3
간단한 함수를 통과하는 세 가지 경로가 있는데,
a가 0인 경우, 0보다 작은 경우, 그리고 0보다 큰 경우
→ 이렇게 3가지다.
예상대로, KLEE는 프로그램에서 3가지 경로를 탐색하고 각 경로에 대해 하나의 테스트 케이스를 생성했다고 알려준다.
큰 프로그램의 경우, KLEE는 시간 또는 메모리 제약으로 인해서 각 경로를 끝까지 탐색할 수 없을 수도 있다.
이러한 경우에는, 일부 완료된 경로의 수에 대해 알려준다.
KLEE 실행의 출력은 KLEE에 의해 생성된 테스트 케이스들을 포함하는 디렉토리가 된다.
(ex: klee-out-0)
KLEE는 출력 디렉토리를 klee-out-N이라고 명명하며, N
은 사용 가능한 가장 낮은 숫자를 의미한다. (0번지부터 쭉 하나씩.. 생성, 따라서 KLEE를 다시 실행하면 klee-out-1이라는 디렉토리를 생성한다)
또한 편의를 위해 이 디렉토리에 대한 심볼릭 링크인 klee-last도 생성한다.
$ ls klee-last/ assembly.ll messages.txt run.stats test000002.ktest warnings.txt info run.istats test000001.ktest test000003.ktest
KLEE에서 생성된 파일들의 개요를 보고 싶다면 여기를 참고할 것.
이 튜토리얼에서는 KLEE에서 생성한 실제 테스트 파일에만 초점을 맞춘다.
KLEE에서 생성된 테스트 케이스
KLEE에서 생성된 테스트 케이스는 .ktest 확장자를 가진 파일로 작성된다.
이들은 ktest-tool 유틸리티로 읽을 수 있는 바이너리 파일들이다.
ktest-tool은 동일한 객체에 대해 다양한 표현을 출력하며, 이를 테면 Python 바이트 문자열(데이터), 정수(int) 또는 ASCII 텍스트(텍스트) 등이 되겠다.
따라서 각 파일을 살펴보도록 하자.
$ /snap/klee/6/usr/local/bin/ktest-tool klee-last/test000001.ktest ktest file : 'klee-last/test000001.ktest' args : ['get_sign.bc'] num objects: 1 object 0: name: 'a' object 0: size: 4 object 0: data: b'\\x00\\x00\\x00\\x00' object 0: hex : 0x00000000 object 0: int : 0 object 0: uint: 0 object 0: text: .... $ /snap/klee/6/usr/local/bin/ktest-tool klee-last/test000002.ktest ktest file : 'klee-last/test000002.ktest' args : ['get_sign.bc'] num objects: 1 object 0: name: 'a' object 0: size: 4 object 0: data: b'\\x01\\x01\\x01\\x01' object 0: hex : 0x01010101 object 0: int : 16843009 object 0: uint: 16843009 object 0: text: .... $ /snap/klee/6/usr/local/bin/ktest-tool klee-last/test000003.ktest ktest file : 'klee-last/test000003.ktest' args : ['get_sign.bc'] num objects: 1 object 0: name: 'a' object 0: size: 4 object 0: data: b'\\x00\\x00\\x00\\x80' object 0: hex : 0x00000080 object 0: int : -2147483648 object 0: uint: 2147483648 object 0: text: ....
각 테스트 파일에 대해 KLEE는 프로그램이 호출된 인수(필자의 경우, 인자가 없고 프로그램 이름만 있음), 해당 경로의 심볼릭 객체 수(필자의 경우, 하나뿐), 심볼릭 객체의 이름(‘a’) 및 크기(4)를 보고해준다.
실제 테스트 자체는 입력값으로 나타난다:
- 첫 번째 테스트의 경우 0
- 두 번째 테스트의 경우 16843009
- 마지막 테스트의 경우 -2147483648
예상대로, KLEE는 0이라는 값과, 양의 값 하나(16843009), 그리고 음의 값 하나(-2147483648)을 생성했다.
이제 이러한 값들을 우리 코드의 네이티브 버전에서 실행시켜 코드를 통과하는 모든 경로를 확인할 수 있다!
테스트 케이스를 다시 실행하기
KLEE가 생성한 테스트 케이스를 수동으로 (또는 기존의 테스트 인프라 도움을 받아) 프로그램에서 실행할 수도 있지만,
KLEE는 간단한 재생 라이브러리를 제공하며, 이를 사용하면 klee_make_symbolic 호출을 .ktest 파일에 저장된 값으로 대체하는 함수 호출이 수행된다.
이를 사용하려면 프로그램을 libkleeRuntest 라이브러리로 링크하고 KTEST_FILE 환경 변수를 원하는 테스트 케이스의 이름을 가리키도록 설정해주면 된다.
$ export LD_LIBRARY_PATH=path-to-klee-build-dir/lib/:$LD_LIBRARY_PATH $ gcc -I ../../include -L path-to-klee-build-dir/lib/ get_sign.c -lkleeRuntest $ KTEST_FILE=klee-last/test000001.ktest ./a.out $ echo $? 0 $ KTEST_FILE=klee-last/test000002.ktest ./a.out $ echo $? 1 $ KTEST_FILE=klee-last/test000003.ktest ./a.out $ echo $? 255
예상대로, 첫 번째 테스트 케이스를 실행할 때 프로그램은 0을 반환하고,
두 번째 테스트 케이스를 실행할 때, 1을 반환하며,
마지막 테스트 케이스를 실행할 때, 255(-1을 0-255 범위의 유효한 종료 코드 값으로 변환)를 반환한다.
출처 및 번역
https://klee.github.io/tutorials/testing-function/