[OS]/Embedded

[펌] gcc 이야기(2)

하늘을닮은호수M 2005. 9. 6. 22:18
반응형

gcc 이야기(2)

글쓴이 : holelee (2002년 04월 06일 오후 10:45)

[ 임베디드강좌/이규명 ] @ KELP

=== 시작하기에 앞서
gcc라는 컴파일러를 이용하여 C 언어 프로그램을 컴파일 하기 위해서 알아야 할 기본적인 옵션 및 발생할 수 있는 에러에 대해 초보자를 대상으로 작성된 글입니다. 고급 사용자라면 읽으실 필요가 없을 것으로 생각됩니다. Architecture dependent한 부분은 가능한 배제하였습니다. 단 gcc의 사용은 Linux를 비롯한 Unix 계열의 OS에서 사용된다는 가정을 하였습니다. 또한 이 글에 대한 모든 내용은 본인이 사용하고 있는 alzza linux 6.1에서 gcc-2.91.66을 바탕으로 하고 있습니다. gcc에 대하여 좀더 많은 것을 알고 싶으신 분은 gcc manpage와 gcc manual을 참조하시기 바랍니다.
이 글에 나오는 모든 내용이 정확하다고 할 수는 없으며, 그 글에 나오는 내용을 따라 gcc를 사용하는데 있어서 문제가 발생할 경우, 본인은 책임을 지지 않습니다. 이 글에 대한 저작권은 본인(holelee)에게 있습니다. 글에 대해 잘못된 점이나 지적할 사항이 있으신 분은 저 위의 "holelee"를 클릭하여 메일을 보내 주시기 바랍니다.


=== 시작 및 복습
좀 딱딱한 말로 글이 시작되었습니다. 양해 바랍니다.
전 글에서 우리는 C 언어 소스가 gcc를 사용하여 컴파일 될 때 거치는 네 단계에 대해서 알아보았습니다. 하나의 C 언어 소스가 실행파일(executable file)로 바뀌는데 C Preprocessing, C 언어 Compile, Assemble, Linking을 거치게 됩니다. 각각의 단계를 cpp, cc1, as, ld라고 하는 gcc와는 다른 실행파일 들이 담당한다고 알게 되었습니다. 이제 그 네 단계 중에 첫번째 단계인 C Preprocessing이 하는 일이 무엇이고, 어떤 gcc의 옵션이 그 수행에 영향을 미치며, 어떤 에러나 경고(Warning)가 날 수 있는지를 알아볼 차례입니다.


=== C Preprocessing(cpp)
C preprocessing을 우리말로 하면 "C 언어 전처리"라고 할 수 있겠죠? 모든 C 언어 문법책에서 정도의 차이는 있지만 C preprocessing에 대한 내용을 다루고 있습니다. C preprocessing에 대한 문법은 C 언어 문법의 한 부분으로 가장 간단한 예제인 hello.c에도 나오니 당연하겠죠. C preprocessing에 관한 문법은 모두 '#'으로 시작됩니다. 또한 정확하게는 '#'은 그 줄(line)의 선두 문자이어야 합니다. 즉, '#' 앞에는 어떠한 문자(공백 문자 포함)도 오면 안되죠. 하지만 대부분의 compiler가 '#'앞에 공백 문자가 오는 경우에도 처리를 해주는 것으로 알고 있습니다. 아무튼, 문법에 대해서는 가지고 있는 문법책을 참조하시길 바랍니다.

그럼 C preprocessing이 하는 일을 자세히(?) 알아보도록 하죠.
== C preprocessing이 하는 일
(1) 입력 : C 언어 소스 코드
(2) 출력 : C 언어 소스 코드(C preprocessing된)
(3) 하는 일
- 파일 포함(file inclusion : 직역이 어색하네요)
- 매크로(macro) 치환
- 선택적 컴파일(conditional compile)
- 기타(#line, #error, #pragma)

너무 간단한가요? 말로 하면 cpp는 C 언어 소스코드를 입력 받아서 C preprocessing에 관련된 문법 사항을 적절히 처리하고 결과로 C 언어 소스코드를 출력하는 프로그램입니다. 입력은 작성된 C 언어 소스 코드이므로 굳이 설명을 안 해도 될 듯하고 출력으로 나온 C 언어 소스 코드에는 C preprocessing 문법에 관련된 어떠한 것도 남아있지 않습니다. 즉, #define, #include 등을 찾을 수 없다는 이야기입니다. 단, 남아 있는 정보는 있습니다. 그것은 file 이름과 줄수(line number)에 관한 정보는 여전히 남아 있습니다. 그 이유는 추후의 컴파일 과정에서 에러가 날 때 그 정보를 이용해서 error를 리포팅할 수 있도록 하기 위해서 입니다. 그럼 C preprocessing을 직접 해보도록 하죠. shell command line에 다음과 같이 입력하세요.(당연히 $은 shell prompt이므로 입력하지 말아야 하고, hello.c는 전에 입력했던 그 소스코드 파일입니다.)
$ gcc -E -o hello.i hello.c
결과로 hello.i라는 파일이 생깁니다. 그 파일 내용이 너무 길어 여기에 싣지는 못합니다만 에디터로 한번 열어보세요. hello.c의 첫번째 줄에 있는 #include 를 처리한 결과가 보입니까?

(*) -E 옵션
-E 옵션은 gcc의 컴파일 과정 중에서 C preprocessing까지만 처리하고 나머지 단계는 처리하지 말라는 것을 지시하는 것입니다. 평소에는 별로 쓸모가 있는 옵션이 아니지만 다음과 같은 경우에 유용하게(?) 사용할 수 있습니다.
(1) C 언어 소스 코드가 복잡한 선택적 컴파일을 하고 있을 때, 그 선택적 컴파일이 어떻게 일어나고 있는지 알고 싶은 경우.
(2) preprocessing의 문제가 C 언어 에러로 나타날 경우. 다음과 같은 소스코드를 고려해 보죠.
-- start of cpperr.c
#define max(x, y) ((x) > (y) ? (x) : (y) /* 마지막에 ")"가 없다!!! */

int myMax(int a, int b)
{
return max(a, b);
}
-- end of cpperr.c
$ gcc -c cpperr.c
다음과 같은 에러가 납니다.(>>는 에러메시지를 나타내는 기호이며 실제로 출력되지 않습니다.)
>> cpperr.c: In function `myMax':
>> cpperr.c:4: parse error before `;'
cpperr.c파일의 4번째 줄에서 ';'가 나오기 전에 parse error(뒤를 참조)가 났다고 하는 군요. 하지만 실제 에러는 #define에 있었으므로 그것을 확인하려면 -E 옵션으로 preprocessing을 하여 살펴 보면 쉽게(?) 알 수 있습니다.

(*) 참고 : parse error before x(어떤 문자) 에러는 그야말로 parsing을 할 때 발생한 에러를 말합니다. parsing이란 syntax analysis라는 과정인데 쉽게 말하면 C 언어 소스코드를 읽어드려 문법적 구성요소 들을 분석하는 과정이라고 할 수 있습니다. 보통 gcc에서 parse error라고 하면 괄호가 맞지 않았거나 아니면 ';'를 빼먹거나 했을 때 발생합니다. 보통의 경우 before x라고하여 x라는 것이 나오기 전에 parse error가 발생하였음을 알려주기 때문에 그 x가 나오기 전에 있는 C 소스 코드를 뚫어지게 바라보면 문제를 찾을 수 있습니다.


C preprocessing의 문법과 나머지 C 언어의 문법과는 거의 관계가 없습니다. 관계가 있는 부분이 있다면 정의된 macro가 C 언어의 문법 상의 char string literal에는 치환되지 않는다는 정도입니다. (좀더 쉽게 이야기 하면 큰 따옴표 안에서는 macro 치환이 되지 않습니다.) 또한 c preprocessing은 architecture dependent하지 않습니다. 즉, i386용으로 컴파일된 cpp를 다른 architecture에서 사용해도 무방합니다. 조금 문제가 있을 수 있는 부분이 있다면 gcc의 predefined macro(i386의 경우 i386용 자동으로 define된다.)가 다를 수 있다는 점 뿐입니다. 따라서 cpp를 C 언어 소스코드가 아닌 다른 부분에서 사용하는 경우도 있습니다. 대표적으로 assembly 소스 코드에서도 사용합니다. assembler가 사용하고 있는 macro 문법이 c preprocessing의 macro문법 보다는 배우기 쉽기 때문이죠.(정확히는 쉽다고 하기 보다는 새로 assembler macro를 배우지 않아도 되므로...)

이제 preprocessing이 하는 일에 대해서 좀더 알아 보겠습니다.
== 파일 포함(file inclusion)
#include
#include "config.h"
위와 같이 많은 C 언어 소스코드에서 헤더 파일을 포함하죠. <>와 ""의 차이는 아실테고...(혹 모르신다면 C 언어 문법책을 참조하세요.) 그런데 여기서 한가지 의문이 생깁니다. "include한 헤더 파일을 어느 디렉토리에서 찾는가?"입니다. 보통은 default로 특정 디렉토리를 찾게 됩니다. Linux 시스템의 경우 /usr/include가 default 디렉토리죠.(실제로도 그곳에 stdio.h라는 파일이 있습니다.) 그 다음은 현재 디렉토리를 찾게 됩니다.(<>와 ""에 따라서 다릅니다만...) 파일이 없으면 당연히 에러가 나겠죠. gcc의 경우 다음과 같은 에러가 납니다. (>>은 에러메시지를 나타내는 기호로 실제로는 출력되지 않습니다.)
>>소스코드파일명:line number: 헤더파일명: No such file or directory
또는(LANG=ko일때)
>>소스코드파일명:line number: 헤더파일명: 그런 파일이나 디렉토리가 없음

그렇다면 include하고 싶은 파일이 default 디렉토리와 현재 디렉토리에 없으면 어떻게 할까요? 그런 문제를 해결하기 위해서 다음과 같은 옵션이 존재합니다.
(*) -Idir 옵션
여기서 dir은 디렉토리 이름이고 -I와 디렉토리 이름을 붙여 써야 합니다. 그럼 include한 헤더 파일을 그 디렉토리에서도 찾아 주게 됩니다. 당연히 옵션을 여러 번 다른 디렉토리 이름으로 줄 수도 있어서 헤더 파일을 찾을 디렉토리를 여러 개로 지정할 수 있습니다. 꼭 알아 두어야 할 옵션입니다.

관련 옵션을 하나만 더 알아보죠.
(*) -nostdinc 옵션
이 옵션은 default 디렉토리(standard include 디렉토리)를 찾지말라고 지시하는 옵션입니다. 어플리케이션 프로그래머는 관심을 둘 필요가 없지만 kernel 프로그래머는 관심 있게 볼 수 있는 옵션이죠.

== macro 치환
macro 치환에 대해서는 특별히 일어날만한 에러는 없습니다. 가끔 문제가 되는 부분이 macro 정의가 한 줄을 넘어선 경우 역슬레쉬('')로 이어져야 하는데 그 소스 파일이 windows용 에디터로 편집 되었으면 parse error가 나는 경우가 있습니다. 그것은 개행문자(new line character)가 서로 달라서 그런 것인데...음 자세히 이야기하자면 끝이 없으므로 그냥 넘어가도록 하죠. 또한
macro가 define된 상황에서 macro를 undef하지 않고 다시 define하면 다음과 같은 Warning이 납니다.
>>
'xxxx' redefined
macro 치환에서 대한 옵션 두개를 알아보도록 하죠.

(*) -Dmacro 또는 -Dmacro=defn 옵션
gcc의 command line에서 macro를 define할 수 있도록 하는 옵션입니다. 예를 들어 -D__KERNEL__이라는 옵션을 주면 컴파일 과정 중에 있는 C 언어 소스코드의 맨 처음에 #define __KERNEL__이라고 해준 것과 같이 동작합니다. 또한 -DMAXLEN=255라고하면 C 언어 소스코드의 맨 처음에 #define MAXLEN 255 라고 한 것과 동일한 결과를 줍니다. 선택적 컴파일을 하는 경우에 많이 이용하는 옵션으로 꼭 알아야 할 옵션입니다.

(*) -Umacro 옵션
이 옵션은 #undef하고 하는 일이 똑같은데 C 언어 소스코드와는 하등의 관계가 없습니다. -Dmacro옵션처럼 C 언어 소스코드의 맨처음에 #undef macro를 해주는 것은 아무런 의미가 없기 때문이죠.(어짜피 #define은 그 이후에 나올 것이므로...) 이 옵션의 목적은 위의 -Dmacro옵션으로 define된 macro를 다시 undef하고자 할 때 쓰는 옵션입니다. 평상시에는 별로 쓸 일이 없는 옵션이지만 그냥 -Dmacro와 같이 짝으로 알아 두시길 바랍니다.

== 선택적 컴파일
#if 시리즈와 #else, #elif, #endif 등으로 선택적 컴파일을 수행할 수 있는 것은 모두 아실 것으로 생각됩니다. 위에서 설명한 -Dmacro 옵션과 같이 쓰는 경우가 많죠. 암튼 특별히 설명할 옵션은 없고 #if와 #else, #endif의 짝이 잘 맞아야 합니다. 안 그러면 당연히 에러가 발생합니다. 단순히 parse error라고 나오는 경우는 드물고, #else, #if 에 어쩌고 하는 에러가 납니다. 많이 경우의 수가 있으므로 직접 에러가 발생되도록 코딩을 해보고 확인해 보셔도 좋을 듯 합니다.

== 기타(#line, #error, #pragma)
#line, #error, #pragma라는 것이 있는지도 모르는 분들이 꽤나 있을 듯 싶습니다. 자세한 것은 당연히 C 언어 문법 책을 찾아 봐야 겠죠. #line의 경우 C 언어 소스코드 직접 쓰이는 경우는 거의 없으니까 무시하고 #pragma는 compiler에 dependent하고 gcc에서 어떤 #pragma를 사용하는지도 알 수 없으므로 그냥 넘어가도록 하겠습니다. #error의 경우 C preprocessing 과정에서 강제로 에러를 만드는 지시어입니다. 선택적 컴파일 과정에서 도저히 선택되어서는 안 되는 부분에 간혹 쓰입니다. 당연히 #error를 만나면 에러가 생깁니다. linux kernel 소스코드에서 include 디렉토리를 뒤져 보시면 사용하는 예를 만나실 수 있습니다.

== predefined macro
사용자가 C 언어 소스코드에서 #define을 하지 않아도 이미 #define된 macro가 있습니다. ANSI C에서는 __LINE__, __FILE__, __TIME__, __DATE__, __STDC__ 다섯 가지는 이미 define되어 있는 macro로 강제적인 사항입니다.(당연히 모르면 문법책 참조) gcc도 당연히 다섯 가지 macro를 predefine합니다. 뿐만 아니라 GCC의 버전에 대한 macro, architecture에 관한 사항 등을 -Dmacro 옵션 없이도 predefine합니다. 전에 말씀드린 -v 옵션을 실행하면 출력되는 specs파일을 열어보시면 감을 잡으실 수 있을 겁니다.(specs파일이 어떻게 해석되는지는 저도 잘 모르니까 묻지 마시길...)

== 꼭 알아두면 좋은 옵션 한가지
다음과 같이 shell 상에 입력해 보세요.(hello.c는 계속되는 그 녀석입니다.)
$ gcc -M hello.c
어떤 것이 출력되나요? "hello.o: hello.c /usr/include/stdio.h 어쩌구저쩌구"가 출력될 것입니다. 어디서 많이 본 듯한 형식 아닌가요?

(*) -M 옵션
-M 옵션은 cpp에게 makefile로 만들 수 있는 rule을 만들어달라고 하는 요청을 보내는 명령입니다. file을 include하는 녀석은 cpp이므로 rule은 cpp가 만들 수 있겠죠. 당연히 -Dmacro, -Umacro, -Idir 옵션 등을 같이 사용할 수 있고 그에 따라 결과가 달라질 수도 있습니다. makefile을 좀 쉽고 정확하게 만들 때 쓰는 옵션이므로 알아두면 좋습니다. 단지 안 좋은 점은 default 디렉토리에 있는 보통 사용자는 고칠 수도 없는 파일까지 무식(?)하게 만들어 준다는 것입니다.


이제 총 정리 해 보도록 하겠습니다. C preprocessing은 C 언어 소스코드를 입력으로 받아 file inclusion, macro 치환, 선택적 컴파일, 기타를 처리하고 C 언어 소스코드를 출력하는 과정입니다. 그 중에 몇 가지 에러가 날 수 있는 부분이 있고(물론 사용자의 잘못으로) 몇 가지 중요한 옵션을 알아봤습니다.
이상 장황한 C preprocessing에 관한 gcc 이야기를 끝냅니다. 넘 길었나요? 다음에는 C 언어 compile과정에 대해서 알아보도록 하겠습니다.(다음 편은 매우 짧게 할 예정입니다.)

반응형