그래픽 형태로 애플리케이션의 호출 트레이스(trace)를 볼 수 있다면 애플리케이션 이해에 큰 도움이 된다. 애플리케이션의 내부 작동을 이해할 수 있고 프로그램 최적화를 위한 정보도 얻을 수 있다. 예를 들어, 가장 자주 호출되는 함수들을 최적화 함으로써, 적은 노력으로 최상의 성능을 올릴 수 있다. 게다가, 호출 트레이스를 통해 사용자 함수의 최대 호출 깊이(depth)를 파악할 수 있고, 호출 스택이 사용하는 메모리를 효율적으로 바인딩 하는데 사용할 수 있다. (이는 임베디드 시스템에 있어서 중요한 고려 사항이다.)
호출 그래프를 캡쳐해서 디스플레이 하려면 네 가지 엘리먼트가 필요하다. GNU 컴파일러 툴체인, Addr2line 유틸리티, 커스텀 글루 코드, Graphviz 툴이다. Addr2line 유틸리티를 사용하여 주소와 실행 이미지에 대한 함수와 소스 라인 번호를 알 수 있다. 커스텀 글루 코드는 주소 트레이스를 그래프로 나타낼 수 있는 간단한 툴이다. Graphviz 툴을 사용해서 그래프 이미지를 만들 수 있다. 전체 프로세스는 그림 1과 같다.
그림 1. 트레이스 컬렉션, 감소, 시각화 프로세스
함수 호출 트레이스를 모으기 위해서는 애플리케이션에서 모든 함수가 호출되는 때를 파악해야 한다. 좋았던 시절에는 엔트리 포인트와 각각의 종료 포인트에서 고유의 심볼을 방출하는 함수를 직접 장착하여 이러한 일을 수행했다. 이 프로세스는 매우 따분하며, 에러를 만들어 내기 쉽고, 소스 코드를 엉망으로 만든다.
다행스럽게도, GNU 컴파일러 툴체인(gcc)이 애플리케이션의 함수들을 자동으로 설치하는 방식을 제공한다. 설치된 애플리케이션이 실행되면 프로파일링 데이터가 수집된다. 단 두 개의 특별한 프로파일링 함수들만 제공하면 된다. 한 개의 함수는 설치된 함수가 호출될 때 마다 호출된다. 또 하나의 함수는 설치된 함수가 종료할 때 호출된다. (Listing 1) 이러한 함수들은 특별한 이름이 주어지기 때문에 컴파일러가 구분할 수 있다.
Listing 1. 엔트리와 종료를 위한 GNU 프로파일링 함수
void __cyg_profile_func_enter( void *func_address, void *call_site )
__attribute__ ((no_instrument_function));
void __cyg_profile_func_exit ( void *func_address, void *call_site )
__attribute__ ((no_instrument_function));
|
설치된 함수가 호출될 때, __cyg_profile_func_enter도 호출되면서, 호출된 함수의 주소가 func_address로서 처리되고, 함수가 호출되었던 주소가 call_site로서 처리된다. 반대로, 함수가 종료할 때, __cyg_profile_func_exit 함수가 호출되면서, 함수의 주소로서 func_address가, 함수가 종료하는 실제 사이트가 call_site로서 전달된다.
이러한 프로파일링 함수 내에서, 향후 분석을 위해 주소 쌍들을 기록할 수 있다. 그러한 gcc가 모든 함수들을 설치하도록 요청하려면, 모든 파일들이 -finstrument-functions와 -g 옵션을 사용하여 컴파일 되어 디버깅 심볼을 보유해야 한다.
따라서, 이제 프로파일링 함수를 gcc에 제공하여, gcc가 여러분의 애플리케이션의 함수 엔트리와 종료 포인트로 삽입할 수 있다. 하지만, 프로파일링 함수가 호출되면, 제공된 주소를 사용하여 무엇을 할 것인가? 많은 옵션들이 있지만, 파일에 주소를 작성하는 것만 설명하겠다. 어떤 주소가 함수 엔트리인지, 어떤 것이 종료 엔트리인지 주목하라. (Listing 2)
주: Callsite 정보는 Listing 2에서는 사용되지 않는다. 이 정보는 프로파일링 애플리케이션에는 필요가 없다.
Listing 2. 프로파일링 함수
void __cyg_profile_func_enter( void *this, void *callsite )
{
/* Function Entry Address */
fprintf(fp, "E%p\n", (int *)this);
}
void __cyg_profile_func_exit( void *this, void *callsite )
{
/* Function Exit Address */
fprintf(fp, "X%p\n", (int *)this);
}
|
지금까지 프로파일링 데이터를 모으는 것에 대해서 설명했다. 그렇다면, 어디에서 트레이스 아웃풋 파일을 열고 닫을까? 지금까지, 프로파일링을 위해서 애플리케이션을 수정할 필요는 없었다. 그렇다면, main 함수를 포함하여, 프로파일링 데이터 아웃풋에 대한 초기화 없이, 전체 애플리케이션을 설치하는 방법은? gcc 개발자들도 이 부분을 생각했고, main 함수 컨스트럭터와 디스트럭터(destructor)를 위한 방법을 제공했다. constructor 함수는 main이 호출되기 전에 바로 호출된다. destructor 함수는 애플리케이션이 종료할 때 호출된다.
컨스트럭터와 디스트럭터를 만들려면, 두 개의 함수를 선언한 다음, constructor와 destructor 함수 애트리뷰트를 이들에 적용한다. constructor 함수에서, 새로운 트레이스 파일이 프로파일링 주소 트레이스가 작성될 곳에 열린다. destructor 함수 내에서, 트레이스 파일이 닫힌다. (Listing 3)
Listing 3. 프로파일링 컨스트럭터와 디스트럭터 함수
/* Constructor and Destructor Prototypes */
void main_constructor( void )
__attribute__ ((no_instrument_function, constructor));
void main_destructor( void )
__attribute__ ((no_instrument_function, destructor));
/* Output trace file pointer */
static FILE *fp;
void main_constructor( void )
{
fp = fopen( "trace.txt", "w" );
if (fp == NULL) exit(-1);
}
void main_deconstructor( void )
{
fclose( fp );
}
|
이 프로파일링 함수들(instrument.c에서 제공)은 컴파일 되고 목표 애플리케이션과 연결된다. 애플리케이션이 실행되고, 결국 trace.txt. 파일에 작성된 애플리케이션의 호출 트레이스가 생긴다. 결과적으로, 주소들로 가득 찬 큰 파일을 갖게 된다. 이 모든 데이터를 이해하려면 Addr2line이라는 비교적 덜 알려진 GNU 유틸리티를 사용해야 한다.
Addr2line을 사용하여 함수 주소를 함수 이름으로 변환하기
Addr2line 툴(표준 GNU Binutils의 일부)은 명령어 주소와 실행 이미지를 파일 이름, 함수 이름, 소스 라인 번호로 변환하는 유틸리티이다. 트레이스 주소를 보다 의미가 있는 것으로 바꾸는데 완벽한 툴이다.
프로세스 방법을 보기 위해 간단한 대화식 예제를 사용해 보도록 하자. (필자는 쉘에서 직접 작업한다. 이것이 프로세스를 나타내는 가장 쉬운 방법이기 때문이다. Listing 4) 간단한 프로그램에 cat을 수행하여 샘플 C 파일(test.c)이 생성되었다. (다시 말해서, 텍스트를 표준 인풋에서 파일로 리다이렉션 했다.) 이 파일은 gcc로 컴파일 되고, 이는 몇 개의 특별한 옵션으로 전달된다. 우선, (-Wl 옵션과 함께) 링커가 맵(map) 파일을 만들도록 명령을 받고, 컴파일러는 디버그 심볼 (-g)을 만들도록 명령을 받는다. 결과는 test.라고 하는 실행 파일이다. 이러한 새로운 실행 파일이 있으면, grep 유틸리티를 사용하여 맵 파일에 있는 main을 검색하여 그 주소를 찾을 수 있다. Addr2line에서 이 주소와 실행 이미지 이름을 사용하면 파일 이름(main), 소스 파일(/home/mtj/test/test.c), 소스 파일 내 라인 넘버(4)를 찾을 수 있다.
Addr2line 유틸리티가 호출되면서, 실행 파일 이미지를 -e 옵션을 가진 test로서 구분한다. -f 옵션을 사용함으로써, 툴에게 함수 이름을 나타내도록 명령한다.
Listing 4. addr2line의 대화식 예제
$ cat >> test.c
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
<ctld-d>
$ gcc -Wl,-Map=test.map -g -o test test.c
$ grep main test.map
0x08048258 __libc_start_main@@GLIBC_2.0
0x08048258 main
$ addr2line 0x08048258 -e test -f
main
/home/mtj/test/test.c:4
$
|
지금까지 함수 주소 트레이스를 모으는 방법과 Addr2line 유틸리티를 사용하여 주소를 함수 이름으로 바꾸는 방법을 배웠다. 하지만, 설치된 애플리케이션에서 얻을 많은 트레이스 주소가 있을 경우, 어떻게 데이터를 줄일 수 있을까? 바로 이 부분에서 오픈 소스 툴들간 차이를 메울 수 있는 커스텀 글루 코드가 필요하다. 이 유틸리티(Pvtrace)에 대한 전체 주석이 달린 소스가 이 글에서 제공되고, 이를 구현하여 사용하는 명령어도 포함되어 있다. (참고자료)
그림 1을 기억해 보면, 설치된 애플리케이션이 실행될 때, trace.txt라고 하는 트레이스 데이터 파일이 생성된다. 이 파일에는 주소들의 리스트가 포함되어 있고, 라인 당 하나씩, 접두사 문자가 각각 붙어있다. 접두사가 E이면, 그 주소는 함수 엔트리 주소이다. (다시 말해서, 이 함수가 호출되었다는 의미이다.) 접두사가 X이면, 그 주소는 종료 주소이다. (이 함수를 종료했다는 의미이다.)
따라서, 트레이스 파일에서, 엔트리 주소 (A) 다음에 또 다른 엔트리 주소 (B)가 있으면, A가 B를 호출했던 것으로 추론할 수 있다. 엔트리 주소 (A) 다음에 종료 주소 (A)가 나오면 함수 (A)가 호출되어 리턴 되었다고 볼 수 있다. 더 긴 호출 체인이 개입되면, 이들을 누가 이들을 호출했는지 알기가 어렵기 때문에, 엔트리 주소의 스택을 유지하는 것이 좋다. 엔트리 주소가 트레이스 파일에서 발견될 때마다 스택으로 들어간다. 스택의 맨 위에 있는 주소는 마지막으로 호출된 함수(활성 함수)를 나타낸다. 또 다른 엔트리 주소가 나오면, 그 스택에 있는 주소가 트레이스 파일에서 마지막으로 읽힌 주소를 호출했다는 의미이다. 종료 주소가 나오면, 현재 활성 함수가 리턴되고 이 스택의 상위 엘리먼트가 버려진다는 의미이다. 이는 콘텍스트를 이전 함수로 돌리는데, 이것은 호출 체인의 올바른 흐름이다.
그림 2는 데이터 정리 개념을 묘사한 것이다. 호출 체인이 트레이스 파일에서부터 파싱되기 때문에 연결 매트릭스(matrix)가 구현되어 어떤 함수가 다른 어떤 함수들을 호출했는지를 구분한다. 매트릭스의 행은 주소로부터의 호출을 나타내고, 행은 주소로의 호출을 나타낸다. 각 호출 쌍의 경우, 이들을 교차하는 셀이 증가된다. (호출 카운트). 전체 트레이스 파일이 읽히고 파싱되면, 호출 카운트를 포함하여 애플리케이션의 전체 호출 기록을 간략히 나타낸 결과가 나타난다.
그림 2. 트레이스 데이터를 매트릭스 폼으로 파싱 및 줄여나가기
간략한 함수 연결 매트릭스가 구현되었으므로, 이제는 그래프를 그릴 차례이다. Graphviz를 연구하여 연결 매트릭스에서 호출 그래프가 어떻게 만들어지는지를 이해해 보자.
Graphviz(Graph Visualization)는 AT&T에서 개발한 오픈 소스 그래프 시각화 툴이다. 여러 가지 그래픽 기능을 제공하지만, 필자는 Dot 언어를 사용한 그래프 기능을 설명하겠다. Dot으로 그래프 그리기 개요를 설명하고, 프로파일링 데이터를 Graphviz가 사용할 수 있는 스팩으로 변환하는 방법도 설명하겠다. (참고자료)
Dot 언어를 사용하여, 세 가지 유형의 객체들을 지정할 수 있다. 그래프, 노드, 엣지(edge)가 바로 그것이다. 이 세가지 엘리먼트를 설명하는 예제를 구현하여 객체들이 무엇을 의미하는지 알아보자.
Listing 5는 Dot 표기법으로 세 개의 노드들로 구성된 간단한 유향(Directed) 그래프이다. 1번 라인은 G라고 하는 그래프와 유형(digraph)을 선언한다. 다음 세 개의 라인에서는 node1, node2, node3라고 하는 그래프 노드를 만든다. 노드들은 그 이름들이 그래프 스팩에 나타날 때 만들어 진다. 두 개의 노드들이 엣지 연산자(->)에 의해서 합쳐질 때 엣지가 만들어 진다. (6-8번 라인) 필자는 옵션 애트리뷰트(label)를 이 엣지에 적용했다. 이것은 그래프 상에 엣지 이름을 정한다. 9번 라인에서 그래프 스팩이 완성되었다.
Listing 5. Dot 표기법으로 된 샘플 그래프 (test.dot)
1: digraph G {
2: node1;
3: node2;
4: node3;
5:
6: node1 -> node2 [label="edge_1_2"];
7: node1 -> node3 [label="edge_1_3"];
8: node2 -> node3 [label="edge_2_3"];
9: }
|
.dot 파일을 그래프 이미지로 변환하려면, Graphviz 패키지에서 제공되는 Dot 유틸리티를 사용해야 한다. Listing 6은 변환된 부분이다.
Listing 6. Dot을 사용하여 JPG 이미지 만들기
$ dot -Tjpg test.dot -o test.jpg
$
|
이 코드에서, Dot에게 필자의 test.dot 그래프 스팩을 사용하고 test.jpg 파일에 JPG 이미지를 생성하도록 명령했다. 결과 이미지는 그림 3과 같다. 필자는 JPG 포맷을 사용했지만, Dot 툴은 GIF, PNG, postscript 같은 다른 이미지 포맷들도 지원한다.
그림 3. Dot에 의해 생성된 샘플 그래프
Dot 언어는 쉐이프, 컬러, 많은 애트리뷰트 등 여러 옵션들도 지원한다. 이 옵션들도 잘 작동한다.
이제 여러분은 모든 과정을 배웠다. 이제는 Pvtrace 유틸리티를 추출 및 설치할 수 있어야 한다. 또한, instrument.c 파일을 실행 소스 디렉토리에 복사할 수 있어야 한다.
이 예제에서, test.c라고 하는 소스 파일을 사용했다. Listing 7은 전체 프로세스이다. 3번 라인에서, 필자는 설치 소스(instrument.c)를 사용하여 애플리케이션을 구현(컴파일 및 연결)한다. 4번 라인에서 test를 실행한 다음, ls 유틸리티를 사용하여 trace.txt 파일이 생성되었는지를 확인한다. 8번 라인에서, Pvtrace 유틸리티를 호출하고 유일한 인자로서 이미지 파일을 제공한다. 이미지 이름은 (Pvtrace 내에서 호출된) Addr2line이 그 이미지의 디버깅 정보에 액세스 하는데 필요하다. 9번 라인에서, 또 다른 ls를 실행하여 Pvtrace가 graph.dot 파일을 생성했는지를 확인한다. 마지막으로 12번 라인에서, Dot을 사용하여 이 그래프 스팩을 JPG 그래프 이미지로 변환한다.
Listing 7. 호출 트레이스 그래프를 만드는 전체 과정
1: $ ls
2: instrument.c test.c
3: $ gcc -g -finstrument-functions test.c instrument.c -o test
4: $ ./test
5: $ ls
6: instrument.c test.c
7: test trace.txt
8: $ pvtrace test
9: $ ls
10: graph.dot test trace.txt
11: instrument.c test.c
12: $ dot -Tjpg graph.dot -o graph.jpg
13: $ ls
14: graph.dot instrument.c test.c
15: graph.jpg test trace.txt
16: $
|
이 프로세스의 샘플 아웃풋은 그림 4처럼 보인다. 이 샘플 그래프는 Q learning을 사용하는 강화 교육 애플리케이션에 발췌한 것이다.
그림 4. 샘플 프로그램 트레이스 결과
또한 이 방식을 사용하여 훨씬 더 큰 프로그램도 볼 수 있다. 마지막 예제로 Gzip 유틸리티를 보겠다. instrument.c를 Makefile에 있는 Gzip의 내용물에 추가하고, 이를 구현하여, Gzip을 사용하여 트레이스 파일을 만들었다. 이미지는 너무 커서 상세히 보여줄 수 없지만, 그래프는 작은 파일을 압축하는 과정에서 Gzip을 나타낸다.
그림 5. Gzip 트레이스 결과
오픈 소스 소프트웨어와 약간의 글루 코드를 사용하여 짧은 시간 안에 재미있고 유용한 프로젝트를 개발할 수 있다. 애플리케이션 프로파일링에 여러 GNU 컴파일러를, 주소 변환에 Addr2line 유틸리티를, 그래프 시각화에 Graphviz 프로그램을 사용함으로써, 애플리케이션을 프로파일링 하고 호출 체인을 보여주는 그래프를 나타내는 프로그램을 만들 수 있다. 프로그램의 호출 체인을 그래픽으로 본다면 프로그램의 내부 작동을 이해하는데 많은 도움이 된다. 호출 체인과 각각의 빈도수를 이해함으로써 애플리케이션을 디버깅 및 최적화 하는데 매우 유용하다.
- 이 기술자료와 관련된 instrumentation and Pvtrace source code 다운로드.
- 최신 GNU Compiler Collection (GCC) documentation 보기.
-
Addr2line참조하기. -
GNU Binutils (gcc.org)
-
Graphviz 웹사이트.
-
Dot utility 메뉴얼.
-
Gzip utility
-
GNU/Linux Application Programming
- M. Tim Jones
- "리눅스 디버깅 기술 마스터하기 (한글)" (한국 developerWorks, 2006년 6월) - 리눅스 상에서 버그를 해결하는 핵심 전략들.
- "Kprobes를 이용한 커널 디버깅 (한글)" (한국 developerWorks, 2004년 8월) - printk's를 리눅스 커널에 삽입하기.
-
한국 developerWorks 리눅스 존에서 리눅스 개발자를 위한 기술자료를 볼 수 있다.
-
developerWorks 블로그와 커뮤니티 참여하기.
-
Browse for books
-
리눅스 SEK 주문하기: 리눅스용 DB2®, Lotus®, Rational®, Tivoli®, WebSphere® 최신 IBM 시험판 SW가 두장의 DVD에 담겨있다.
-
IBM 시험판 SW를 한국 developerWorks에서 다운로드하여 다음 리눅스 개발 프로젝트를 개선해보라.
