목차, 영문

gcov: 테스트 커버리지 프로그램

gcov는 GNU CC와 함께 사용해서 프로그램에 대한 코드 커버리지(code coverage) 테스트를 수행할 수 있는 도구이다. 이 장에서는 gcov 버전 1.5를 대상으로 다음과 같은 사항들을 설명한다.


gcov에 대한 소개

gcov는 커버리지 테스트 프로그램이다. gcov를 GNU CC와 함께 사용하면 보다 빠르고 효율적인 프로그램 코드를 만들기 위한 분석을 시행해 볼 수 있다. gcov는 프로그램 코드의 어느 부분을 최적화 시키는 것이 가장 좋은 선택인지를 판단하기 위한 프로파일링 도구(profiling tool)로 사용될 수 있으며, gprof 등과 함께 사용하면 프로그램 코드의 어느 부분이 데이터 입출력 처리를 제외한 사칙 연산 등의 실제 연산 처리 소요 시간을 가장 많이 차지하고 있는 지 알아낼 수 있다.

프로파일링 도구들은 프로그램 코드의 성능을 분석하는데 도움이 되며, gprof와 같은 프로파일러를 사용하면 다음과 같은 기본적인 통계 정보를 얻을 수 있다.

위와 같은 사항들을 통해서 작성된 코드가 컴파일 과정에서 어떻게 동작하는 지를 확인한 후에, 어떤 부분을 최적화시켜야 할 지 판단할 수 있다. gcov는 어느 부분을 최적화 시켜야 하는 지를 판단하는데 도움을 준다.

소프트웨어 개발자들은 개발된 소프트웨어가 배포 가능한 정도의 성능을 보이는 지를 확인하기 위해서 단위 검사(unit test) 등의 테스트를 커버리지 분석과 함께 사용한다. 이때 사용되는 테스트들은 프로그램이 예상대로 작동하는 지를 검증할 수 있는데, 커버리지 프로그램은 테스트에 의해서 프로그램이 어떻게 작동하는 지를 확인하기 위해서 사용되며 그 결과에 따라서 개발자들은 테스트 기능을 보다 좋게 개량하고, 만들어질 제품을 향상시키기 위해서 어떠한 검사 항목이나 기능이 추가되어야 할 지를 결정할 수 있다.

gcov를 사용하고자 한다면 컴파일 시에 GNU CC 최적화 옵션을 사용하지 않아야 한다. 왜냐하면, 코드를 최적화 해서 컴파일 할 경우에는 여러 개의 행들이 하나의 함수로 통합될 수 있기 때문에 프로그램 실행 시에 계산 시간이 많이 소요되는 부분을 찾기 위한 정보를 많이 얻을 수 없기 때문이다. 또한 gcov는 행을 최소 처리 단위로 통계를 산출하기 때문에 한 행에 하나의 문장을 입력하는 프로그래밍 스타일을 사용할 때 최상의 결과를 가져올 수 있다. 만약, 루프나 제어 구문으로 확장된 복잡한 형태의 매크로를 사용한 경우에는 매크로가 호출된 행만을 출력해 주는 gcov의 통계는 큰 도움이 되지 않는다. 복잡한 형태의 매크로가 마치 함수와 같이 사용될 경우에는 이러한 문제를 해결하기 위해서 매크로가 아닌 인라인 함수(inline function)의 형태로 수정할 수 있다.

gcov는 소스 파일인 `sourcefile.c'를 구성하는 각각의 행들이 몇 번씩 실행되었는 지를 알려주는 로그 파일인 `sourcefile.gcov'를 생성하는데, 이러한 로그 파일은 gprof와 함께 프로그램을 보다 나은 성능으로 튜닝하는데 사용될 수 있다. gprofgcov로부터 얻은 정보에 덧붙여서 시간에 대한 정보도 함께 제공해 줄 수 있다.

gcov는 GNU CC로 컴파일된 코드에서만 동작하며, 다른 종류의 프로파일링이나 테스트 커버리지 방식과는 호환되지 않는다


gcov의 사용법

gcov [-b] [-c] [-v] [-n] [-l] [-f] [-o 디렉토리이름] 소스파일이름
-b
출력 파일에 분기가 일어난 횟수에 대한 사항을 기록하고 분기에 대한 요약 정보를 표준 출력으로 출력한다. 이 옵션을 사용하면 프로그램 안에서 각각의 분기가 어느 정도의 빈도로 일어났는 지를 확인해 볼 수 있다.

-c
분기가 일어난 빈도를 백분율이 아닌 횟수로 표시해 준다. (1.5 버전에는 이 옵션이 포함되어 있지 않다.)

-v
gcov의 버전을 진단 출력(standard error)으로 출력해 준다.

-n
출력 파일을 생성하지 않는다.

-l
인클루드(include)된 소스 파일을 위해서 긴 이름의 출력 파일을 생성한다. 예를 들면, 실제 코드가 `x.h' 헤더 파일 안에 포함되어 있고, 이 헤더 파일이 `a.c' 파일 안에 포함되어 있을 경우에 `a.c' 파일을 gcov의 인수로 -l 옵션과 함께 실행시키면 `x.h.gcov'가 아닌 `a.c.x.h.gcov'라는 이름으로 출력 파일을 만들 수 있다. 이것은 `x.h' 파일이 여러 개의 소스 파일 안에 포함되어 있을 경우에 유용하게 사용될 수 있다.

-f
파일에 대한 요약 정보 이외에 각각의 함수에 대한 요약 정보를 출력한다.

-o
오프젝트 파일이 있는 디렉토리를 지정한다. gcov-o 옵션으로 지정한 디렉토리에서 .bb.bbg 그리고 .da를 확장자로 갖는 파일을 검색한다.


gcov를 사용하기 위해서는 GNU CC의 `-fprofile-arcs -ftest-coverage' 옵션을 사용해서 프로그램을 컴파일 해야 한다. 이 옵션들은 GNU CC 컴파일러로 하여금 프로그램의 플로우 그래프(flow graph)를 포함한 gcov의 사용에 필요한 부가적인 정보를 생성하도록 할 수 있으며, 프로파일링 정보를 만들기 위한 사항들을 오프젝트 파일에 추가시킬 수 있다. 이 파일들은 소스 코드가 있는 디렉토리 안에 생성된다.

컴파일이 완료된 후에 gcov를 실행하면 출력 파일이 생성되는데, `-fprofile-arcs' 옵션과 함께 컴파일 된 소스 코드의 경우에는 .da를 확장자로 갖는 데이터 파일이 소스 디렉토리에 생성된다. 다음은 소스 파일의 이름이 `tmp.c'인 경우에 대한 기본적인 gcov의 사용 예이다. 먼저, 다음과 같은 내용을 담고 있는 소스 코드 `tmp.c'를 작성한다.

                main()
                {
                 int i, total;   
                 total = 0;
                 for (i = 0; i < 10; i++)
                  total += i;
                 if (total != 45)
                   printf ("Failure\n");
                 else
                   printf ("Success\n");
                }


gccgcov를 다음과 같은 순서로 실행시킨다.

$ gcc -fprofile-arcs -ftest-coverage tmp.c
$ ./a.out
$ gcov tmp.c
 87.50% of 8 source lines executed in file tmp.c
Creating tmp.c.gcov.


`tmp.c.gcov' 파일은 다음과 같은 실행 결과를 담고 있다. 왼쪽에 출력된 숫자는 해당 행이 실행된 횟수를 의미한다.

                main()
                {
           1      int i, total;
                
           1      total = 0;
                
          11      for (i = 0; i < 10; i++)
          10        total += i;
                
           1      if (total != 45)
      ######        printf ("Failure\n");
                  else
           1        printf ("Success\n");
           1    }


`-b' 옵션을 사용한 경우의 출력 결과는 다음과 같다.

$ gcov -b tmp.c
 87.50% of 8 source lines executed in file tmp.c
 80.00% of 5 branches executed in file tmp.c
 80.00% of 5 branches taken at least once in file tmp.c
 50.00% of 2 calls executed in file tmp.c
Creating tmp.c.gcov.


결과로 생성되는 `tmp.c.gcov' 파일에는 다음과 같은 형태의 통계 정보가 포함된다.

                main()
                {
           1      int i, total;
                
           1      total = 0;
                
          11      for (i = 0; i < 10; i++)
branch 0 taken = 91%
branch 1 taken = 100%
branch 2 taken = 100%
          10        total += i;
                
           1      if (total != 45)
branch 0 taken = 100%
      ######        printf ("Failure\n");
call 0 never executed
branch 1 never executed
                  else
           1        printf ("Success\n");
call 0 returns = 100%
           1    }


전체 코드를 구성하는 단위 블록들에 대해서 블록의 마지막 행 밑에는 해당 블록이 분기(branch) 되면서 끝났는지, 아니면 다른 루틴을 호출(call) 하면서 끝났는 지를 알려주는 간단한 메시지가 출력된다. 여러 개의 블록이 함께 마무리 되는 위치에 있는 행의 경우에는 여러 개의 분기나 호출 사항이 표시될 수 있다. 이러한 경우에는 각각의 분기와 호출에 대해서 `branch 0', `branch 1'과 같은 형식의 식별 번호가 표시되지만, 단순히 소스 코드를 읽는 방법으로 분기나 호출이 시작된 원래의 위치로 정확히 거슬러 올라가는 것은 결코 쉬운 일이 아니다. 그러나 일반적으로 낮은 숫자를 갖는 분기와 호출일수록 소스 코드에서 왼쪽 방향으로 보다 멀리 위치하게 된다.

분기의 경우, 최소한 한 번 이상 실행되었을 때에는 해당 분기가 선택된 횟수를 분기가 실행된 횟수로 나눈 백분율이 표시된다. 만약 선택된 분기가 실행되지 않은 경우에는 `never executed'라는 메시지가 출력된다.

호출의 경우, 최소한 한 번 이상 실행되었을 때에는 호출에 대한 리턴값이 반환된 횟수를 호출이 실행된 횟수로 나눈 백분율이 표시된다. 이 값은 일반적으로 100%가 되어야 하지만, 모든 호출에 대해서 리턴값이 되돌려 지지 않는 exitlongjmp를 사용하는 함수의 경우에는 100%보다 작은 수치가 표시될 수도 있다.

실행 횟수는 누적된다. 따라서, .da 파일을 삭제하지 않은 상태에서 위의 예제 프로그램을 다시 실행시키면 각각의 소스 코드 행이 실행된 횟수를 나타내는 숫자가 이전의 2배로 증가하게 된다. 이것은 몇 가지 측면에 있어서 이점을 제공한다. 예를 들면, 테스트수트(testsuite)의 일부로 많은 프로그램들이 실행될 때 이를 축적된 데이터로 사용할 수 있으며, 여러 개의 프로그램이 실행되는데 따른 보다 정확한 장기 정보(long-term information)를 제공해 줄 수 있다.

.da 파일 안의 데이터는 프로그램이 종료되기 바로 직전에 기록된다. `-fprofile-arcs' 옵션과 함께 실행된 모든 소스 파일들에 대해서 프로파일링 코드는 .da 파일이 존재하고 있는 지를 먼저 확인한다. 만약 이 파일이 실행 파일과 단위 블록의 숫자에 대한 차이를 갖고 있다면 .da 파일의 내용을 무시한 뒤에 새로운 실행 횟수를 파일에 추가한 뒤에 필요한 데이터를 입력한다.


gcov와 코드 최적화

프로그램의 코드를 gcov를 이용해서 최적화시키려고 한다면, 먼저 2개의 옵션인 `-fprofile-arcs -ftest-coverage'를 사용해서 GNU CC로 프로그램을 컴파일해야 한다. 두 개의 옵션 이외에 또다른 GNU CC의 옵션들을 함께 사용하는 것도 가능하다. 그러나 gcov가 필요한 정보를 만들기 위해서는 프로그램을 구성하는 모든 행들의 실행을 확실하게 보장하기 위해서 GNU CC의 최적화 옵션을 사용해서는 안된다. 프로그램 최적화 도구들은 특정한 아키텍처 상에서 몇 개의 행들을 삭제한 뒤에 이들을 다른 행에 결합시킬 수 있다. 다음과 같은 간단한 코드의 예를 보자.

if (a != b)
  c = 1;
else
  c = 0;


특정한 아키텍처의 시스템에서는 위의 코드가 하나의 인스트럭션(instruction)으로 컴파일 될 수 있다. 이러한 경우에는 코드들이 행 단위로 분리되어 있지 않기 때문에 gcov가 실행 횟수를 행별로 산출하는 것이 불가능 해진다. 따라서 프로그램을 최적화시켜서 컴파일 할 경우에는 gcov에 의한 출력 정보가 다음과 같이 될 수도 있다.

      100  if (a != b)
      100    c = 1;
      100  else
      100    c = 0;


위의 블록은 최적화 결과로 인해서 하나의 코드로 결합되어 모두 100회씩 실행된 것으로 나타나고 있다. 어떤 의미에서 이러한 결과는 올바른 것이라고 볼 수 있다. 왜냐하면 4행의 코드가 하나의 인스트럭션을 구성하고 있기 때문이다. 그러나 위의 결과를 통해서는 결과가 0인 경우와 1인 경우가 각각 몇 번씩 나타났는 지를 확인할 수 없다.


gcov 데이터 파일

gcov는 프로파일링을 위해서 세 개의 파일을 사용하는데, 이 파일들의 이름은 원래의 소스 파일이 갖고 있던 이름에 .bb (Basic Block)와 .bbg (Basic Block Graph) 그리고 .da (DAta)를 확장자로 붙인 것이다. 이 파일들은 소스 파일이 있던 디렉토리에 생성되며 플랫폼에 독립적인 형태로 데이터를 저장하고 있다.

.bb.bbg 파일은 소스 파일을 `-ftest-coverage' 옵션과 함께 GNU CC로 컴파일할 때 생성된다. .bb 파일은 헤더 파일을 포함한 소스 파일들의 목록과 이 파일 안에 포함된 함수들의 목록을 담고 있다. 또한 소스 파일 안의 단위 블록들에 대응되는 행 번호를 담고 있다.

.bb 파일의 형식은 소스 파일을 구성하고 있는 단위 블록들의 시작 위치를 나타내는 행 번호에 해당하는 4바이트 정수들의 목록으로 구성되어 있는데, 각각의 목록들은 행 번호 0으로 구분된다. 예제로 사용된 `tmp.c'로부터 생성된 `tmp.bb' 파일과 바이너리 정보를 출력해 줄 수 있는 od 명령어를 이용한 .bb 파일의 출력 형태는 다음과 같다. 여기서는 .bb 파일의 구성 형태를 설명하기 위해서 동일한 `tmp.bb' 파일의 내용을 4바이트 형태의 정수와 ASCII 형태로 각각 출력한 뒤에 paste 명령어를 이용해서 한행씩 대응시킨 출력 형태를 사용했다.

      $ paste 1 2 `od -w4 -d tmp.bb>1; od -w4 -c tmp.bb>2`
        0000000     2 32768     |     0000000 002  \0  \0 200
	0000004 24941 28265     |     0000004   m   a   i   n
	0000010     0     0     |     0000010  \0  \0  \0  \0
	0000014     2 32768     |     0000014 002  \0  \0 200
	0000020     0     0     |     0000020  \0  \0  \0  \0
	0000024     1 32768     |     0000024 001  \0  \0 200
	0000030 28020 11888     |     0000030   t   m   p   .
	0000034    99     0     |     0000034   c  \0  \0  \0
	0000040     1 32768     |     0000040 001  \0  \0 200
	0000044     3     0     |     0000044 003  \0  \0  \0
	0000050     4     0     |     0000050 004  \0  \0  \0
	0000054     5     0     |     0000054 005  \0  \0  \0
	0000060     0     0     |     0000060  \0  \0  \0  \0
	*	*               |
	0000074     6     0     |     0000074 006  \0  \0  \0
	0000100     5     0     |     0000100 005  \0  \0  \0
	0000104     0     0     |     0000104  \0  \0  \0  \0
	*	*               |
	0000114     7     0     |     0000114  \a  \0  \0  \0
	0000120     0     0     |     0000120  \0  \0  \0  \0
	0000124     8     0     |     0000124  \b  \0  \0  \0
	0000130     0     0     |     0000130  \0  \0  \0  \0
	*	*               |
	0000140    10     0     |     0000140  \n  \0  \0  \0
	0000144     0     0     |     0000144  \0  \0  \0  \0
	*	*               |           
	0000154    11     0     |     0000154  \v  \0  \0  \0
	0000160     2 32768     |     0000160 002  \0  \0 200
	0000164 18271 20300     |     0000164   _   G   L   O
	0000170 16706 24396     |     0000170   B   A   L   _
	0000174 18734 27950     |     0000174   .   I   .   m
	0000200 26977 18286     |     0000200   a   i   n   G
	0000204 20291    86     |     0000204   C   O   V  \0
	0000210     2 32768     |     0000210 002  \0  \0 200
	0000214     0     0     |     0000214  \0  \0  \0  \0
	*	*


행 번호 -1은 (od를 실행한 위의 예에서는 음수를 2의 보수로 표현하는 컴퓨터의 수체계에 의해서 `1 32768''이 -1을 의미하게 된다.) 소스 파일의 이름을 나타내기 위해서 사용하며, .bb 파일을 파싱할 때 보다 식별을 용이하게 하기 위해서 끝나는 위치에도 -1이 한번 더 표시된다. 위의 출력 예에서 두개의 -1 (`1 32768') 사이에 파일의 이름인 `tmp.c'가 기록되어 있는 것을 확인할 수 있다. 행 번호 -2는 (od를 실행한 위의 예에서는 음수를 2의 보수로 표현하는 컴퓨터의 수체계에 의해서 `2 32768''이 -2를 의미하게 된다.) 함수의 이름을 나타내기 위해서 사용되며, -1의 경우와 마찬가지로 끝나는 위치에 한번 더 표시된다. 이 숫자들은 모두 4 바이트 정수로 기록된다. 위의 예에서는 `tmp.c' 파일 안에서 사용된 두개의 함수인 main()printf()에 해당하는 이름이 기록되어 있는 것을 확인할 수 있다.

.bbg 파일은 소스 코드에 대한 프로그램 플로우 그래프를 재구성하는데 사용된다. 이 파일은 하나의 단위 블록으로부터 발생 가능한 분기인 arcs들의 목록을 담고 있으며 .bb 파일과 함께 사용되어 gcov가 프로그램 플로우 그래프를 만들 수 있게 해준다. .bbg 파일의 형식은 다음과 같다.

       첫 번째 함수를 구성하는 기본 블록(basic block)의 숫자 (4바이트 정수)
       첫 번째 함수를 구성하는 arcs의 숫자 (4바이트 정수)
       첫 번째 기본 블록 안의 arcs의 숫자 (4바이트 정수)
       첫 번째 arc의 데스티네이션 기본 블록 (4바이트 정수)
       플래그 비트 (4바이트 정수)
       두 번째 arc의 데스티네이션 기본 블록 (4바이트 정수)
       플래그 비트 (4바이트 정수)
       ...
       N번째 arc의 데스티네이션 기본 블록 (4바이트 정수)
       플래그 비트 (4바이트 정수)
       두 번째 기본 블록 안의 arcs의 숫자 (4바이트 정수)
       첫 번째 arc의 데스티네이션 기본 블록 (4바이트 정수)
       플래그 비트 (4바이트 정수)
       ...

4바이트 숫자로 저장되는 -1은 단위 블록 안에 포함된 함수들을 구별하는 식별자로 사용된다. 또한 파일이 올바르게 읽혀졌는 지를 검증하는데 사용되기도 한다.

.da 파일은 GNU CC의 `-fprofile-arcs' 옵션으로 컴파일된 오브젝트 파일을 포함하는 프로그램이 실행될 때 각각의 소스 파일마다 생성된다. .da 파일은 소스 파일이 이 옵션과 함께 실행된 후에 오브젝트 파일이 있는 위치에 저장되는데, 소스 파일의 이름에 .da를 확장자로 갖는 이름이 사용된다.

.da 파일의 포맷은 매우 간단하다.

      $ od -w4 -d tmp.da
	0000000     7     0     0     0
	0000010     1     0     0     0
	0000020    10     0     0     0
	0000030     1     0     0     0
	0000040     0     0     0     0
	0000050     1     0     0     0
	*
	0000100

처음 8바이트, 즉 첫 번째 숫자인 7은 파일 안에 있는 카운터의 갯수를 의미한다. 이어지는 8바이트 단위의 숫자들은 프로그램 안에서 각각의 arcs들이 실행된 횟수이다. (gcov.c 소스 파일 참조) 카운터들은 .gcov 파일과 마찬가지로 결과가 누적되는 형태를 갖고 있다. 따라서 프로그램이 실행될 때마다 카운터들은 기존의 값에 새로운 값이 추가되는 형태를 띠게 된다. 만약, .da 파일의 내용이 프로그램의 arcs 정보와 다를 경우에는 기존의 내용은 무시되고 내용이 새롭게 생성된다. 즉, 소스가 수정될 경우에는 수정된 소스로 만들어진 오브젝트 파일이 실행될 때 .da 파일의 내용 또한 자동으로 변경된다.

.bb.bbg 그리고 .da 세 개의 파일들은 정수형 자료를 저장하기 위해서 모두 gcov-io.h 헤더 파일에 정의된 함수를 사용하는데, 이 헤더 파일에 포함되어 있는 함수들은 아키텍처에 무관하게 입출력 스트림으로부터 데이터를 저장하고 검색할 수 있는 방법을 제공한다.


목차, 영문