ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Pintos] 디버깅 도구 (Debugging Tools) - printf, assert, __attributes__, backtraces
    프로젝트/Pintos 2021. 3. 16. 10:56

    디버깅이란 프로그램에서 발생할 수 있는 버그(에러) 를 찾아서 없애는 과정을 말한다. 이 장에서는 Pintos 프로젝트를 진행하면서 활용할 수 있는 여러가지 디버깅 도구들에 대해 설명한다. 여기서 설명된 방법들은 Pintos 프로젝트 뿐 아니라 다른 프로젝트들에서도 널리 사용될 수 있는 디버깅 방법들이므로 익혀두도록 하자.

    1. printf()

    printf() 를 c 언어의 가장 처음에 배우기 때문에 printf() 를 사용한 디버깅은 없어보인다고 생각하기 쉽다. 하지만 printf() 만큼 간단하고 유용하게 사용할 수 있는 Debugging Tools 도 없다.

    프로그램이 비정상적으로 작동할 때 printf() 를 코드의 중간중간 섞어 놓으면 문제가 발생한 코드의 범위를 특정할 수 있다.

     

    ex) code A - printf("after A") - code B - printf("after B") - code C

    실행결과 : after A (after B 는 출력되지 않음)

    -> code B 에 문제가 있음.

     

    2. ASSERT

    assert 함수는 표준 c 라이브러리 중 하나인 assert.h 에 포함되어 있는 매크로이다.

    pintos 에서는 <lib/debug.h> 에 간략하게 구현되어 있다.

    /* lib/debug.h */
    #ifndef NDEBUG
    #define ASSERT(CONDITION)                                       
            if (CONDITION) { } else {                               
                    PANIC ("assertion `%s' failed.", #CONDITION);   
            }
    #define NOT_REACHED() PANIC ("executed an unreachable statement");
    
    #else
    #define ASSERT(CONDITION) ((void) 0)
    #define NOT_REACHED() for (;;)
    #endif

     

     

    NDEBUG 라는 것은 디버그 모드가 아닌 경우(릴리즈 모드)를 말한다. 디버그 모드에서는 ASSERT 등의 디버깅 도구들이 작동하지만 릴리즈 모드에서는 디버깅 도구들은 무시하기 때문에 더 최적화된 실행파일이 만들어진다.

    (#ifndef 는 뒤 매크로가 정의되어 있지 않을 때 실행된다. #ifdef 와 헷갈리지 말도록 하자.)

     

    ASSERT 매크로는 정해진 조건에 맞지 않을 때 프로그램을 중단한다.

    위 코드에서 보면 조건(CONDITION) 이 참(true)이면 그냥 지나가지만, 거짓(false)라면 커널을 PANIC 에 빠뜨리고 Panic message(문제가 된 부분의 expression, file and line number, backtrace 등)을 출력한다. 즉, 프로그램이 올바로 작동하기 위해서는 assert 뒤의 조건이 항상 참(true)이 되도록 만들어야 한다.

    printf() 와 마찬가지로 버그가 의심되는 곳에 여기저기 섞어두면 유용하다.

     

    3. Function and Parameter Attributes

    pintos 의 <lib/debug.h> 에는 함수 혹은 함수 파라미터에 사용할 수 있는 특별한 속성(attributes) 들이 정의되어 있다.

    /* lib/debug.h */
    #define UNUSED __attribute__ ((unused))
    #define NO_RETURN __attribute__ ((noreturn))
    #define NO_INLINE __attribute__ ((noinline))
    #define PRINTF_FORMAT(FMT, FIRST) __attribute__ ((format (printf, FMT, FIRST)))

    우선 __attribute__ 라는 표현이 생소할 수도 있다. __attribute__ 표현은 gcc 에서 사용할 수 있는 확장 기능정도로 볼 수 있다. 이는 함수, 구조체, 변수 등의 컴파일 시에 특정 속성을 적용시키는 역할을 한다. "__attribute__ ((속성인자))" 의 형태로 사용하면 이 표현이 붙은 함수, 구조체 혹은 변수에 지정한 속성을 부여할 수 있다.

     

    예를 들어, __attribute__ ((unused)) 는 특정 변수에 unused 라는 속성을 부여하는데 사용될 수 있다.

    /* www.keil.com/support/man/docs/armcc/armcc_chr1359124982981.htm */
    void Variable_Attributes_unused_0() {
    	static int aStatic =0; 
        int aUnused __attribute__((unused)); 
        int bUnused; 
        aStatic++; 
    }

    이를 실행시키면 원래라면 사용하지 않은 aUnused, bUnused 라는 변수에 대해 컴파일러가 경고를 하지만, unused 속성을 부여받은 aUnused 변수에 대하여는 경고를 하지 않는다.

     

    이 내용을 가지고 위의 <lib/debug.h> 를 해석해보자.

    • UNUSED : 함수의 parameter 에 붙어서 해당 parameter 가 사용되지 않을 수 있음을 명시한다.
    • NO_RETURN : 함수 prototype 에 붙어서 해당 함수가 return 되지 않음을 명시한다. 이는 단순히 return 값이 없다는 것 이상으로 이 함수를 call 한 caller 에게로 control 이 돌아가지 않음을 의미한다. 따라서 NO_RETURN 속성을 가지는 함수 이후의 코드는 컴파일러에 의해 도달할 수 없는 코드로 최적화되어 제거된다.
    __attribute__ ((noreturn)) void noReturnf(){ ... }
    
    int main(){
        print("a");
        noReturnf();
        print("b");		// unreachable
        print("c");		// unreachable
    }
    • NO_INLINE : 함수 prototype 에 붙어서 해당 함수가 inline 처리되지 않음을 명시한다. inline 으로 정의된 함수는 컴파일 할 때 함수가 사용되는 모든 곳에 함수의 코드를 복사하여 넣어준다. 즉, 프로그램이 실행되면서 만나는 function call 에서 함수를 호출하지 않고 함수의 코드를 그대로 실행한다.

    이미지출처 : C언어 코딩 도장 (일반 함수와 인라인 함수의 차이)

    • PRINT_FORMAT (format, first) : 함수 prototype 에 붙어서 해당 함수가 printf 함수의 형식으로 사용됨을 명시한다. format 은 format string 의 위치이고, first 는 value parameter 의 시작 위치를 나타낸다. 이렇게 말하면 이해가 잘 되지 않으니 예시를 들어보겠다.
    /* lib/debug.h */
    void debug_panic (const char *file, int line, const char *function,
                      const char *message, ...) PRINTF_FORMAT (4, 5) NO_RETURN;

     

    위 함수는 pintos 의 panic message 를 출력해주는 디버깅함수이다. 이 함수에 PRINT_FORMAT (4, 5) 가 붙었으므로 이 함수의 parameter 는 printf 함수의 형식을 따라야 하고, 그렇지 않을 시 오류를 출력한다. 즉 format string 의 위치로 지정된 4번째 parameter 인 const char *message 에는 format string (ex. "%d + %d = %d" 등) 의 형식 문자열이 argument 로 들어와야 하고, value parameter 의 시작 위치로 지정된 5번째 parameter 부터는 format string 의 %d, %s, %c 등에 들어갈 값들이 알맞은 type 으로 입력되어야 한다.

    debug_panic ([blah], [blah], [blah], "error message here, line %d", 4)

    예시처럼 앞에 3개의 parameter 를 생략하고 보면 printf 의 형식과 같다. 만약 5번째 argument 로 4 대신 "c" 등의 다른 type 의 argument 가 들어오면 컴파일러가 경고 문구로 알려주게 된다.

     

    4. Backtraces

    kernel panic 이 발생했을 때 프로그램이 오류가 발생한 지점의 address 를 역추적하여 알려준다. <lib/debug.h> 에 debug_backtrace(), debug_backtrace_all() 로 구현되어 있으며 각각 현재 지점의 backtrace, 모든 threads 의 backtrace 를 출력한다. 이 함수는 역추적한 address 들을  hexadecimal 값으로 출력하여 주기 때문에 해석에 곤란함을 겪을 수 있다. 이를 위해, pintos 는 터미널에서 backtrace 라는 명령어를 제공하여 이 hexadecimal 값들을 function name, source file line number 로 해석하여 준다.

     

    example) debug_backtrace() 의 결과가 아래와 같이 출력되었다고 하자.

    Call stack : 0xc0106eff 0xc01102fb 0xc010dc22 0xc010cf67 0xc0102319 0xc010325a 0xc804812c 0x8048196 0x8048ac8

     

    결과의 해석은 backtrace [kernel.o(backtrace 를 제공한 kernel)] [hexadecimal numbers] 형식으로 사용한다.

    $ backtrace kernel.o 0xc0106eff 0xc01102fb 0xc010dc22 0xc010cf67 0xc0102319 
                         0xc010325a 0xc804812c 0x08048196 0x08048ac8
    # result
    0xc0106eff: debug_panic (lib/debug.c:86)
    0xc01102fb: file_seek (filesys/file.c:405)
    0xc010dc22: seek (userprog/syscall.c:744)
    0xc010cf67: syscall_handler (userprog/syscall.c:444)
    0xc0102319: intr_handler (threads/interrupt.c:334)
    0xc010325a: intr_entry (threads/intr-stubs.S:38)
    0xc804812c: (unknown)
    0x08048196: (unknown)
    0x08048ac8: (unknown)

    아래 3줄이 (unknown) 으로 표시된 것은 이들이 kernel 함수가 아니라 user program 의 함수이기 때문이다. 만약 어떤 user program 에서 kernel panic 이 발생했는지 알고있다면 해당 user program 에서 다시 backtrace 를 실행하면 (unknown) 의 정체를 알 수 있다.

    $ backtrace tests/filesys/extended/grow-too-big 0xc0106eff 0xc01102fb 
                                                    0xc010dc22 0xc010cf67
                                                    0xc0102319 0xc010325a
                                                    0xc804812c 0x08048196 
                                                    0x08048ac8
    # result
    0xc0106eff: (unknown)
    0xc01102fb: (unknown)
    0xc010dc22: (unknown)
    0xc010cf67: (unknown)
    0xc0102319: (unknown)
    0xc010325a: (unknown)
    0xc804812c: test_main (...xtended/grow-too-big.c:20)
    0x08048196: main (tests/main.c:10)
    0x08048ac8: _start (lib/user/entry.c:9)

    이번엔 kernel 함수들이 (unknown) 으로 표시되고 user program 함수들만 제대로 나온 것을 볼 수 있다. 당연한 얘기지만, 이 출력값은 Call stack 의 출력이므로 아래서부터 위로 올라가며 함수를 호출한 것이다.

    $ backtrace kernel.o tests/filesys/extended/grow-too-big ...

    이런 식으로 kernel 과 user program 을 동시에 넣으면 (unknown) 없이 한 번에 모든 값이 출력된다.

     

    5. GDB

    GDB 의 내용은 워낙 방대한 관계로 따로 포스팅하도록 한다.

     

     

     

     

    Reference)

    web.stanford.edu/class/cs140/projects/pintos/pintos.pdf

    www.keil.com/support/man/docs/armcc/armcc_chr1359124982981.htm

    dojang.io/mod/page/view.php?id=748

     

     

Designed by Tistory.