제공 : 한빛 네트워크
저자 : Mulyadi Santosa
역자 : 조성재
원문 : Getting Familiar with GCC Parameters
[이전 기사 보기]
GCC 파라메터와 친해지기(2)
GCC 파라메터와 친해지기(1)
디버깅에 관련된 옵션
모든 사람들은 그들의 코드를 때때로 디버깅해야 할 필요가 있습니다. 그 때가 오면, 보통 여러분들은 gdb를 실행하고, 정지점(breakpoint)를 여기저기에 두고, 백트레이스를 분석하는 등, 코드를 공격하는 정확한 위치를 알아내기 위한 일을 할 것입니다. 그리고 정확한 것을 얻었습니까? 어떤 디버깅 옵션을 사용한 적이 없다고 가정한다면 여러분은 EIP 레지스터 하나에 의해 지적된 주소만을 얻을 것입니다.
문제는 여러분이 실제로 주소를 원하지는 않는다는 것입니다. 여러분들은 gdb 혹은 다른 디버거가 단순히 관련된 코드를 보여주길 바랍니다. 그러나 gdb는 어떤 종류의 힌트가 없이는 그렇게 하지 못합니다. 이 힌트는 특별히 속성이 있는 레코드 형식을 가진 디버깅(Debugging With Attributed Record Formats:DWARF)이라 불리며, 여러분이 소스단계의 디버깅을 할 수 있도록 돕습니다.
어떻게 이것을 하냐고요? 여러분이 오브젝트 코드를 컴파일 할 때 -g 를 사용해주세요. 예를 들어, 다음과 같이 말이죠.
gcc -o -g test test.c
실제로 gcc가 소스코드와 관련된 주소를 연결하도록 무엇을 추가할 수 있습니까? 여러분은 그것을 찾기위해 dwarfdump[7]가 필요합니다. 이 도구는 "libdwarf" 타볼 파일이나, RPM 내부에 패키지되어 있습니다. 따라서 여러분은 따로 패키지를 찾을 수 없을 것입니다. 여러분의 시스템에 그것을 컴파일하거나 온라인 저장소에서 배포판의 패키지를 설치하십시오. 이 두 가지 방법 모두 동작합니다. 이 부분에서 저는 20060614 RPM 버전을 사용합니다.
readelf를 사용하는 것으로 여러분들은 Listing 1의 비-디버깅 버전 내에서 28개 부분(Section)이 있다는 것을 알게됩니다.
$ readelf -S ./non-optimized
그러나 디버깅 버전에서는 36개의 부분(Section)을 가지고 있습니다. 새로운 부분은 다음이 추가됩니다.
debug_arranges
debug_pubnames
debug_info
debug_abbrev
debug_line
debug_frame
debug_str
debug_loc
여러분은 다음의 모든 부분들을 삽질할 필요는 없습니다. .debug_line 내를 보는 것만으로도 충분한 관찰이 됩니다. 여러분에게 필요한 명령은 다음과 같습니다.
$ /path/to/dwarfdump -l
여기 여러분들이 얻게 될 결과물의 예제가 있습니다.
.debug_line: line number info for a single cu
Source lines (from CU-DIE at .debug_info offset 11):
[row,column] //
다음 메시지들의 설명은 아주 직관적입니다. 예로서 ( 문장 다음의) 첫 번째 항목을 보십시오.
line number 3 in file non-optimized.c is located in address 0x8048384.
gdb 스스로 같은 정보를 보여줍니다.
$ gdb non-optimized-debugging
(gdb) l *0x8048384
0x8048384 is in main (./non-optimized.c:3).
readelf 또한 --debug-info를 사용하는 것으로 동일한 정보를 제공합니다.
$ readelf --debug-dump=line
Line Number Statements:
Extended opcode 2: set Address to 0x8048384
Special opcode 7: advance Address by 0 to 0x8048384 and Line by 2 to 3
Advance PC by constant 17 to 0x8048395
Special opcode 7: advance Address by 0 to 0x8048395 and Line by 2 to 5
....
readelf와 dwarfdump 모두 디버그 정보를 분석할 수 있으며 여러분들은 자유롭게 선택할 수 있습니다.
여러분이 알게 될 것은 소스코드 자체가 오브젝트 파일에 내장되는 것이 아니라는 점입니다. 사실, 디버거는 분리된 소스 코드 파일을 확인해야만 합니다. 칸에 있는 항목은 소스코드 파일을 어디에서 가져올지 결정하도록 도와줍니다. 전체 경로를 포함한다는 것을 주의하십시오. 이것은 파일이 어디론가 옮겨졌거나 이름이 바뀌면 gdb가 그것을 가지고 오지 못한다는 것을 뜻합니다.
gcc는 스스로 여러가지 디버깅 정보를 생산하는 기능을 가지고 있습니다. DWARF와 비슷한 것들로 다음의 것들이 있습니다.
Stabs: -gstabs 는 순수 stabs 형식을 생성하지만, 그렇지 않으면 -gstabs+ 가 지정된 GNU 확장을 포함합니다.
공통 오브젝트 파일 형식(Common Object File Format:COFF): -gcoff 옵션으로 생성됨
XCOFF: -gxcoff 파라메터로 생성됩니다. 만약 GNU 확장을 포함하는 것을 선호한다면 -gxcoff+ 를 사용하십시오.
가상메모리 시스템(Virtual Memory System:VMS); -gvms 파라메터로 생성됨
각 형식은 미주([8],[9],[10])에 설명되어 있습니다만, x86 호환 아키텍쳐에서만 의심없이 DWARF 형식을 사용할 것입니다. 마지막 DWARF 사양은 DWARF 3이며, gdb는 -gdwarf-2 파라메터를 사용해서 생성할 수 있습니다. 이 파라메터는 처음 사용하는 사용자들에게는 잘못 선택되어 사용될 수 있습니다. 왜냐하면 여러분은 gdb가 DWARF-2 기반의 정보를 생성한다고 생각할 수 있기 때문입니다. 사실 DWARF 2에는 몇몇 DWARF 3 요소가 서로 대응됩니다. 모든 디버거가 버전 3을 지원하지 않기 때문에 주의깊게 사용하십시오.
하지만, 모든 것이 자연스럽게 흘러가지만은 않습니다. 여러분이 -O와 -g를 같이 사용할 때, 언급된 오프셋 주소에서 실제 코드와의 정보를 연관짓는 것이 코드 행정보를 생성하기 위해 필요합니다. 예제는 이것을 명확하게 해줄 수 있습니다. Listing 1 소스파일로 다음과 같이 컴파일 해보십시오.
$ gcc -O2 -ggdb -o debug-optimized listing-one.c
$ readelf --debug-dump=line debug-optimized
..
Special opcode 107: advance Address by 7 to 0x80483aa and Line by 4 to 11
...
그러나 gdb는 뭐라고 말합니까?
$ gdb debug-optimized
(gdb) l *0x80483aa
0x80483aa is in main (./non-optimized.c:11).
...
11 printf("acc = %lu\n",acc);
...
(gdb) disassemble main
...
0x080483aa : add $0x6,%edx
0x080483ad : cmp $0x1388,%eax
...
여러분들은 엉뚱한 정보들을 볼 것입니다. 디버그 정보만을 기대했기에 여러분은 CALL 명령과 같은 것들을 포함한 관련된 주소를 기대했을 것입니다. 그러나 실제로 여러분은 거의 loop 구조에 가까운 ADD와 CMP 명령어들을 보게됩니다. 이것은 최적화에 의해 일어난 부작용입니다. 이 경우는 명령어의 재배열에 관련된 최적화 때문입니다. 그러므로 관례에 따라 -g (혹은 이것과 비슷한 다른 파라메터)와 -O 파라메터를 섞어쓰지 마십시오.
컴파일 단계 제어 옵션들
배우기 위한 목적으로 때때로 당신은 소스코드들이 어떻게 실행파일로 변환되는지 알기 원할 것입니다. 운 좋게도 gcc는 어떤 처리 단계에서든지 멈출 수 있는 옵션들을 제공합니다. gcc가 몇몇 단계에서 수행이 완료되도록 다시 실행하십시오. 예를 들어 링크 부분에서 말입니다. 옵션들은 다음과 같습니다.
-c 는 어셈블리 구간에서 멈춥니다만 링크는 건너뜁니다. 결과물은 오브젝트 코드입니다.
-E 는 전처리 단계 이후에서 멈춥니다. 모든 지시자들의 전치리는 순수 코드로만 보이도록 확장됩니다.
-S 는 컴파일 후에 멈춥니다. 이것은 어셈블러 코드를 남깁니다.
-c 는 대부분 여러분이 여러 소스 파일들을 가지고 있을 때 사용하며 그것들을 마지막 실행파일을 만들도록 뭉칠 때 사용됩니다. 따라서 다음의 명령어
$ gcc -o final-binary test1.c test2.c
를 다음의 단계로 나누어 실행하는 것이 낫습니다.
$ gcc -c -o test1.o test1.c
$ gcc -c -o test2.o test2.c
그리고 나서
$ gcc -o final-binary ./test1.o ./test1.o
라고 실행하십시오. 여러분들은 아마도 makefile을 사용하여 프로그램을 빌드할 때 같은 순서를 사용하는 것을 알 것입니다. -c 옵션을 사용하는 잇점은 명백합니다. 여러분들이 변경한 파일만 따로 컴파일하면 되기 때문입니다. 다시 처리를 완료한 파일만 모든 오브젝트 파일에 연결하는 것만으로, 큰 프로젝트에서는 엄청난 시간을 줄이게 됩니다. 대표적인 예로 리눅스 커널이 그러합니다.
-E 는 여러분의 코드가 매크로, 선언, 그리고 그러한 확장 이후에 코드가 어떻게 보이는지 보기 원할 때 유용합니다. Listing 3 의 소스코드를 예로 듭니다.
#include
#define A 2
#define B 4
#define calculate(a,b) a*a + b*b
void plain_dummy()
{
printf("Just a dummy\n");
}
static inline justtest()
{
printf("Hi!\n");
}
int main(int argc, char *argv[])
{
#ifdef TEST
justtest();
#endif
printf("%d\n", calculate(A,B));
return 0;
}
[Listing 3] #define 과 #ifdef 을 포함한 코드
우리는 이것을 이렇게 컴파일 합니다.
$ gcc -E -o listing2.e listing2.c
우리는 -D 파라메터를 전달하지 않았다는 것을 주의하십시오. 즉, TEST 라는 선언이 정의되지 않았음을 기억하십시오. 그럼 우리가 전처리된 파일에서 무엇을 가지고 있습니까?
void plain_dummy()
{
printf("Just a dummyn");
}
static inline justtest()
{
printf("Hi!n");
}
int main(int argc, char *argv[])
{
printf("%dn", 2*2 + 4*4);
return 0;
}
main() 내부에서 justtest()로 호출된 곳이 어디입니까? 아무데도 없습니다. TEST는 정의되지 않았습니다. 그것이 코드가 제거된 이유입니다. 여러분은 또한 calculate() 매크로가 이미 곱셈에 확장되어 있는 것과 소스코드 내에 상수가 추가된 것을 볼 수 있습니다. 마지막 실행 형식에서 이 숫자는 결과를 처리하는데 대체될 것입니다. 여러분이 보시다시피 -E는 지시자의 정확성을 두 번 확인할 수 있는 아주 유용한 파라메터입니다.
plain_dummy()가 호출되지 않더라도 계속 존재하고 있는 것을 주의하십시오. 여기에서 컴파일이 일어나지 않는 이상 놀라울 것은 없으므로, 이 단계에서 사용하지 않는 코드의 제거는 일어나지 않습니다. stdio.h의 내용도 또한 확장됩니다만 이것은 다음 코드에서 보여주지 않습니다.
저는 HTML 제작 도구로서 -E의 흥미로운 응용을 찾았습니다. [11] 간단하게 HTML 세계에서 코드 변환과 매크로와 같은 것들의 공통적인 프로그래밍 연습을 하도록 적용하는데 도움이 됩니다. 어떤 것들은 순수한 HTML 코딩만으로는 할 수 없습니다.
-S 파라메터는 여러분들이 objdump -d/-D 로 보는 것과 같이, 어셈블리 코드를 전달합니다. 하지만, -S 를 사용하면 코드를 쉽게 보는데 도움이 되는 지시자와 심볼 이름을 계속 보게 됩니다. 예를 들어, printf("%dn", 20) 과 같은 호출이 다음과 같이 변환될 수 있습니다.
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%dn"
...
movl $20, 4(%esp)
movl $.LC0, (%esp)
call printf
여러분들은 읽기 전용 데이터 부분(.rodata)에서 string %d 형식을 볼 수 있습니다. 또한 여러분들은 스택의 최상위에서 문자열 형식으로 오른쪽부터 왼쪽으로 스택에 저장된 인자들을 확인할 수 있습니다.
결론
gcc는 코드를 우리가 좋아하는 형태로 생성하도록 여러 유용한 옵션들을 제공합니다. 이 파라메터들이 실제로 어떻게 동작하는지 이해하는 것으로 우리는 프로그램을 더 빠르고 가볍게 만들 수 있습니다. 하지만, 파라메터에만 의존하지 마십시오. 여러분들은 효율적이고 좋은 구조의 코드를 작성하는데 더 주의를 기울여야만 합니다.
감사의 글
저는 프리노드의 OFTC 채팅채널(#kernelnewbies 와 #gcc) 그리고 #osdev에 있는 커뮤니티 구성원들에게 값진 아이디어를 알려준 것에 대해 감사의 말을 전하고 싶습니다.
참조문서
Wikipedia"s article about Preprocessing
Wikipedia"s article about Compilation
Wikipedia"s article about Assembler
Wikipedia"s article about Linker
An example of code reordering using gcc
Frame pointer omission (FPO) optimization and consequences when debugging, Part 1 and Part 2
Explanation of DWARF
Explanation of stabs
Explanation of COFF
Explanation of XCOFF (a COFF variant)
Using a C preprocessor as an HTML authoring tool
gcc
online documentation
AMD Athlon Processor x86 Code Optimization Guide
저자 Mulyadi Santosa 는 인도에서 살고 있는 프리랜서 작가입니다.
역자 조성재님은 현재 오픈소스 데스크탑 환경인 Kool Desktop Environment (KDE) 프로젝트의 한국어 번역 코디네이터와 한국팀의 대표로 활동하고 있습니다.