KLEE 첫번째 튜토리얼: 작은 함수를 테스트해보기

    작은 함수를 테스트해보기

    이 튜토리얼은 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/

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다