IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  리눅스 | 오픈 소스  >

Graphviz를 이용하여 함수 호출을 그림으로 나타내기 (한글)

오픈 소스 소프트웨어로 복잡한 호출 구조를 명확하게!

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.

영어원문

영어원문


제안 및 의견
피드백

난이도 : 중급

M. Tim Jones, Consultant Engineer, Emulex

2007 년 5 월 29 일

많은 소스 코드로 작업하다 보면 함수의 흐름을 파악할 수 있지만, 함수 포인터가 개입되거나 코드가 길거나 얽히게 되면, 프로세스는 상당히 어려워집니다. 이 글에서는 오픈 소스 소프트웨어와 커스텀 글루(glue) 코드를 사용하여 동적인 그래픽 함수 호출을 구현하는 방법을 설명합니다.

그래픽 형태로 애플리케이션의 호출 트레이스(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));

특정 함수의 설치 방지

gcc가 함수들을 설치한다면, __cyg_* 프로파일링 함수들의 설치 여부가 궁금할 것이다. gcc 개발자들 역시 이 부분에 대해 생각했고 함수 프로토타입에 적용될 수 있는, 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 함수는 애플리케이션이 종료할 때 호출된다.

컨스트럭터와 디스트럭터를 만들려면, 두 개의 함수를 선언한 다음, constructordestructor 함수 애트리뷰트를 이들에 적용한다. 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과 디버거

비록, GNU Debugger (GDB)가 내부적으로는 다른 메소드를 사용하지만, Addr2line 유틸리티는 기본적인 심볼릭 디버거 정보를 제공한다.




위로


함수 트레이스 데이터 줄이기

지금까지 함수 주소 트레이스를 모으는 방법과 Addr2line 유틸리티를 사용하여 주소를 함수 이름으로 바꾸는 방법을 배웠다. 하지만, 설치된 애플리케이션에서 얻을 많은 트레이스 주소가 있을 경우, 어떻게 데이터를 줄일 수 있을까? 바로 이 부분에서 오픈 소스 툴들간 차이를 메울 수 있는 커스텀 글루 코드가 필요하다. 이 유틸리티(Pvtrace)에 대한 전체 주석이 달린 소스가 이 글에서 제공되고, 이를 구현하여 사용하는 명령어도 포함되어 있다. (참고자료)

그림 1을 기억해 보면, 설치된 애플리케이션이 실행될 때, trace.txt라고 하는 트레이스 데이터 파일이 생성된다. 이 파일에는 주소들의 리스트가 포함되어 있고, 라인 당 하나씩, 접두사 문자가 각각 붙어있다. 접두사가 E이면, 그 주소는 함수 엔트리 주소이다. (다시 말해서, 이 함수가 호출되었다는 의미이다.) 접두사가 X이면, 그 주소는 종료 주소이다. (이 함수를 종료했다는 의미이다.)

따라서, 트레이스 파일에서, 엔트리 주소 (A) 다음에 또 다른 엔트리 주소 (B)가 있으면, A가 B를 호출했던 것으로 추론할 수 있다. 엔트리 주소 (A) 다음에 종료 주소 (A)가 나오면 함수 (A)가 호출되어 리턴 되었다고 볼 수 있다. 더 긴 호출 체인이 개입되면, 이들을 누가 이들을 호출했는지 알기가 어렵기 때문에, 엔트리 주소의 스택을 유지하는 것이 좋다. 엔트리 주소가 트레이스 파일에서 발견될 때마다 스택으로 들어간다. 스택의 맨 위에 있는 주소는 마지막으로 호출된 함수(활성 함수)를 나타낸다. 또 다른 엔트리 주소가 나오면, 그 스택에 있는 주소가 트레이스 파일에서 마지막으로 읽힌 주소를 호출했다는 의미이다. 종료 주소가 나오면, 현재 활성 함수가 리턴되고 이 스택의 상위 엘리먼트가 버려진다는 의미이다. 이는 콘텍스트를 이전 함수로 돌리는데, 이것은 호출 체인의 올바른 흐름이다.

그림 2는 데이터 정리 개념을 묘사한 것이다. 호출 체인이 트레이스 파일에서부터 파싱되기 때문에 연결 매트릭스(matrix)가 구현되어 어떤 함수가 다른 어떤 함수들을 호출했는지를 구분한다. 매트릭스의 행은 주소로부터의 호출을 나타내고, 행은 주소로의 호출을 나타낸다. 각 호출 쌍의 경우, 이들을 교차하는 셀이 증가된다. (호출 카운트). 전체 트레이스 파일이 읽히고 파싱되면, 호출 카운트를 포함하여 애플리케이션의 전체 호출 기록을 간략히 나타낸 결과가 나타난다.


그림 2. 트레이스 데이터를 매트릭스 폼으로 파싱 및 줄여나가기
프로세스 줄이기
툴 구현 및 설치

Pvtrace 유틸리티를 다운로드 하여 압축을 풀 때, 하위 디렉토리에 make를 타이핑 하여 Pvtrace 유틸리티를 구현한다. 이 유틸리티는 다음과 같은 코드를 사용하여 /usr/local/bin 디렉토리에도 설치될 수 있다.

$ unzip pvtrace.zip -d pvtrace
$ cd pvtrace
$ make
$ make install

간략한 함수 연결 매트릭스가 구현되었으므로, 이제는 그래프를 그릴 차례이다. Graphviz를 연구하여 연결 매트릭스에서 호출 그래프가 어떻게 만들어지는지를 이해해 보자.




위로


Graphviz 사용하기

Graphviz(Graph Visualization)는 AT&T에서 개발한 오픈 소스 그래프 시각화 툴이다. 여러 가지 그래픽 기능을 제공하지만, 필자는 Dot 언어를 사용한 그래프 기능을 설명하겠다. Dot으로 그래프 그리기 개요를 설명하고, 프로파일링 데이터를 Graphviz가 사용할 수 있는 스팩으로 변환하는 방법도 설명하겠다. (참고자료)

Dot을 이용한 그래프 스팩

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에 의해 생성된 샘플 그래프

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 트레이스 결과
Gzip 트레이스 결과



위로


요약

소셜 북마크

mar.gar.in mar.gar.in
digg Digg
del.icio.us del.icio.us
Slashdot Slashdot

오픈 소스 소프트웨어와 약간의 글루 코드를 사용하여 짧은 시간 안에 재미있고 유용한 프로젝트를 개발할 수 있다. 애플리케이션 프로파일링에 여러 GNU 컴파일러를, 주소 변환에 Addr2line 유틸리티를, 그래프 시각화에 Graphviz 프로그램을 사용함으로써, 애플리케이션을 프로파일링 하고 호출 체인을 보여주는 그래프를 나타내는 프로그램을 만들 수 있다. 프로그램의 호출 체인을 그래픽으로 본다면 프로그램의 내부 작동을 이해하는데 많은 도움이 된다. 호출 체인과 각각의 빈도수를 이해함으로써 애플리케이션을 디버깅 및 최적화 하는데 매우 유용하다.



참고자료



필자소개

M. Tim Jones

M. Tim Jones는 임베디드 소프트웨어 아키텍트이자 GNU/Linux Application Programming, AI Application Programming, BSD Sockets Programming from a Multilanguage Perspective 등을 저술한 작가이다. 정지 우주선용 커널 개발부터 임베디드 시스템 아키텍처와 네트워킹 프로토콜 개발에 이르기까지 다양한 엔지니어링 경력이 있다. Longmont, 콜로라도주에 위치한 Emulex Corp.의 컨설턴트 엔지니어이다.




기사에 대한 평가


보다 나은 서비스를 제공하기 위함이오니 잠시 짬을 내어 이 양식을 제출하여 주십시오.



아니오잘 모르겠음
 


 


12345
 



위로


developerWorks 콘텐트를 다른 사이트에 전재하기:
developerWorks 콘텐트에 대한 저작권은 IBM에 있습니다. IBM의 서면 허가나 원본 저자의 허락이 없이는 전재를 금합니다. 저희 콘텐트를 전재하시려면 IBM developerWorks 담당자 에게 문의하십시오.
    IBM 소개 개인정보 보호정책 문의