3. make 강좌
3.1 머릿말
소스 한두 개로 이루어진 C/C++ 언어 교양과목 과제물을 제출하는 것이 아니라면 약간만 프로젝트가 커져도 소스는 감당할 수 없을 정도로 불어나게 되고 그것을 일일이 gcc 명령행 방식으로 처리한다는 것은 상당히 곤역스러운 일입니다.
그래서 하나의 프로젝트를 효율적으로 관리하고 일관성있게 관리하기 위하여 Makefile 이라는 형식을 사용하고 make 라는 유틸리티를 사용합니다.
여러분이 리눅스에서 소스 형태로 되어 있는 것을 가져와서 컴파일하게 되면 보통 마지막에는 make 라는 명령, 또는 make <어쩌구> 이런 식으로 치게 됩니다.
make 라는 유틸리티는 보통 현재 디렉토리에 Makefile 또는 makefile 이라는 일정한 규칙을 준수하여 만든 화일의 내용을 읽어서 목표 화일(target)을 만들어냅니다. Makefile의 이름을 다르게 명시하고 싶을 때는 다음과 같이 합니다.
$ make -f Makefile.linux
보통 멀티플랫폼용 소스들은 Makefile.solaris, Makefile.freebsd, Makefile.hp 이런 식으로 Makefile 을 여러 개 만들어두는 경향이 있지요. 또는 적절하게 만들어두어 다음과 같이 make <플랫폼> 라는 식으로 하면 컴파일되도록 하기도 합니다.
$ make linux
이런 일은 보통의 관례일 뿐이죠. 더 예를 들어보자면 이런 식입니다. 우리가 커널 컴파일 작업할 때를 보십시요.
$ make config /* 설정 작업을 한다 */ $ make dep /* 화일 의존성을 검사한다 */ $ make clean /* 만든 화일들을 지우고 깨긋한 상태로 만든다 */ $ make zImage /* zImage(압축커널)를 만든다 */ $ make zlilo /* 커널을 만들고 LILO를 설정한다 */ $ make bzImage /* bzImage(비대압축커널)를 만든다 */ $ make modules /* 커널 모듈을 만든다 */ $ make modules_install /* 커널 모듈을 인스톨한다 */
복잡한 것같아도 우리는 항상 make, make, make ... 일관성있게 make 라고만 쳐주면 됩니다. ^^ 분량이 작은 소스들의 경우에는 일반적으로 다음만 해도 되는 경우가 많죠.
$ make 또는 make all $ make install
영어권에 사는 사람들에게는 더욱 친밀하게 느껴질 겁니다. 그렇겠죠? ``만들라!''라는 동사를 사용하고 있는 것이고 그 다음에는 그들의 정상적인 어순에 따라 목적어가 나오죠.
$ make install.man
또한 관례상 ``맨페이지'' 같은 것은 별도로 인스톨하도록 배려하는 경우가 많습니다. 프로그램에 대해 잘 아는 사람이라면 맨페이지를 자질구레하게 설치하고 싶지 않을 때도 많으니까요.
다른 사람에게 공개하는 소스라면 더욱 make 를 사용해야 합니다. 그들뿐 아니라 여러분 자신도 make 라고만 치면 원하는 결과가 나올 수 있도록 하는 것이 좋습니다. 많은 소스를 작성하다 보면 여러분 스스로도 까먹기 쉽상입니다.
일단 make를 사용하는 일반적인 관례를 익히는 것이 중요하다고 봅니다. 리눅스 배포판 패키지만 설치하지 마시고 적극적으로 소스를 가져다 컴파일해보십시요. 실력이든 꽁수든 늘기 시작하면 여러분은 더욱 행복해지실 수 있습니다. =)
3.2 make 시작해 봅시다.
일관성있게 make라고만 치면 모든 일이 술술 풀려나가도록 하는 마술은 Makefile이라는 것을 어떻게 여러분이 잘 만들어두는가에 따라 결정됩니다. 바로 이 Makefile 을 어떻게 만드는지에 대하여 오늘 알아봅니다.
- 상황 1)
-
$ gcc -o foo foo.c bar.c
여기서 foo 라는 실행화일은 foo.c, bar.c 라는 2 개의 소스로부터 만들어지고 있습니다.
여러분이 지금 계속 코딩을 하고 있는 중이라면 이 정도쯤이야 가상콘솔 또는 X 터미널을 여러 개 열어두고 편집하면서 쉘의 히스토리 기능을 사용하면 그만이지만 하루 이틀 계속 해간다고 하면 곤역스러운 일이 아닐 수 없습니다.
자, 실전으로 들어가버리겠습니다. vi Makefile 해서 만들어봅시다. ( 편집기는 여러분 마음 )
foo: foo.o bar.o gcc -o foo foo.o bar.o foo.o: foo.c gcc -c foo.c bar.o: bar.c gcc -c bar.c
입력하는데 주의하실 것이 있습니다. 자, 위 화일을 보십시요. 형식은 다음과 같습니다.
목표: 목표를 만드는데 필요한 구성요소들... 목표를 달성하기 위한 명령 1 목표를 달성하기 위한 명령 2 ...
Makefile은 조금만 실수해도 일을 망치게 됩니다.
맨 첫번째 목표인 foo 를 살펴보죠. 맨 첫 칸에 foo: 라고 입력하고 나서 foo가 만들어지기 위해서 필요한 구성요소를 적어줍니다. foo가 만들어지기 위해서는 컴파일된 foo.o, bar.o 가 필요합니다. 각 요소를 구분하는데 있어 콤마(,) 같은 건 사용하지 않고 공백으로 합니다.
중요! 중요! 그 다음 줄로 넘어가서는 <탭>키를 누릅니다. 꼭 한 번 이상은 눌러야 합니다. 절대 스페이스키나 다른 키는 사용해선 안됩니다. 목표 화일을 만들어내기 위한 명령에 해당하는 줄들은 모두 <탭>키로 시작해야 합니다. Makefile 만들기에서 제일 중요한 내용입니다. <탭>키를 사용해야 한다는 사실, 바로 이것이 중요한 사실입니다.
foo를 만들기 위한 명령은 바로 gcc -o foo foo.o bar.o 입니다.
다시 한 번 해석하면 이렇습니다. foo 를 만들기 위해서는 foo.o와 bar.o가 우선 필요하다.( foo: foo.o bar.o )
일단 foo.o, bar.o 가 만들어져 있다면 우리는 gcc -o foo foo.o bar.o 를 실행하여 foo 를 만든다.
자, 이제부터 사슬처럼 엮어나가는 일만 남았습니다.
foo를 만들려고 하니 foo.o와 bar.o 가 필요합니다!
그렇다면 foo.o는 어떻게 만들죠?
foo.o: foo.c gcc -c foo.c
바로 이 부분입니다. foo.o는 foo.c를 필요로 하며 만드는 방법은 gcc -c foo.c입니다.
그 다음 bar.o 는 어떻게 만들죠?
bar.o: bar.c gcc -c bar.c
이것을 만들려면 이것이 필요하고 그것을 만들기 위해서는 또 이것이 필요하고...
소스를 만들어서 해봅시다.
- foo.c 의 내용
extern void bar ( void ); int main ( void ) { bar (); return 0; }
- bar.c 의 내용
#include <stdio.h> void bar ( void ) { printf ( "Good bye, my love.\n" ); }
Makefile을 위처럼 만들어두고 그냥 해보죠.
$ make 또는 make foo gcc -c foo.c gcc -c bar.c gcc -o foo foo.o bar.o
명령이 실행되는 순서를 잘 보십시요. 여기서 감이 와야 합니다. ^^
$ ./foo Good bye, my love.
다시 한 번 실행해볼까요?
$ make make: `foo' is up to date.
똑똑한 make는 foo를 다시 만들 필요가 없다고 생각하고 더 이상 처리하지 않습니다.
이번에는 foo.c 를 약간만 고쳐봅시다. return 0; 라는 문장을 exit (0); 라는문장으로 바꾸어보죠. 그리고 다시 한 번 다음과 같이 합니다.
$ make gcc -c foo.c gcc -o foo foo.o bar.o
자, 우리가 원하던 결과입니다. 당연히 foo.c 만 변화되었으므로 foo.o 를 만들고 foo.o가 갱신되었으므로 foo도 다시 만듭니다. 하지만 bar.c는 아무변화를 겪지 않았으므로 이미 만들어둔 bar.o 는 그대로 둡니다.
소스크기가 늘면 늘수록 이처럼 똑똑한 처리가 필요하지요.
$ rm -f foo $ make gcc -o foo foo.o bar.o
이것도 우리가 원하던 결과입니다. foo 실행화일만 살짝 지웠더니 make는 알아서 이미 있는 foo.o, bar.o 를 가지고 foo 를 만들어냅니다. :)
- 상황 2) 재미를 들였다면 이번에는 청소작업을 해보기로 합시다.
-
clean: rm -f foo foo.o bar.o
이 두 줄을 위에서 만든 Makefile 뒷부분에 추가해보도록 합시다.
$ make clean rm -f foo foo.o bar.o $ make gcc -c foo.c gcc -c bar.c gcc -o foo foo.o bar.o
make clean이라는 작업 또한 중요한 작업입니다. 확실히 청소를 보장해주어야 하거든요.
make, make clean 이런 것이 되면 상당히 멋진 Makefile 이라고 볼 수 있죠? 이번 clean 에서 보여드리고자 하는 부분은 이런 것입니다.
우리의 머리 속에 clean 이라는 목표는 단지 화일들을 지우는 일입니다.
clean: 옆에 아무런 연관 화일들이 없지요?
그리고 오로지 rm -f foo foo.o bar.o 라는 명령만 있을 뿐입니다. clean이라는 목표를 수행하기 위해 필요한 것은 없습니다. 그러므로 적지 않았으며 타당한 make 문법입니다.
- 상황 3)
-
all: foo
이 한 줄을 Makefile 맨 앞에 넣어두도록 합시다.
$ make clean $ make all gcc -c foo.c gcc -c bar.c gcc -o foo foo.o bar.o
이번예는 all 이라는 목표에 그 밑에 나오는 다른 목표만이 들어있을 뿐, 아무런 명령도 없는 경우입니다. 보통 우리는 make all 하면 관련된 모든 것들이 만들어지길 원합니다.
all: foo1 foo2 foo3 foo1: <생략> foo2: <생략> foo3: <생략>
이런 식으로 해두면 어떤 장점이 있는지 알아봅시다.
보통 make all 하면 foo1, foo2, foo3가 모두 만들어집니다. 그런데 어떤 경우에는 foo1만 또는 foo2만을 만들고 싶을 때도 있을 겁니다. 괜히 필요없는 foo3 같은 것을 컴파일하느라 시간을 보내기 싫으므로 우리는 단지 다음과 같이만 할 겁니다.
$ make foo1 $ make foo2
물론 일반적으로 다 만들고 싶을 때는 make all 이라고만 하면 됩니다.
make all 이건 아주 일반적인 관례이지요. 그리고 외우기도 쉽잖아요?
3.3 꼬리말 규칙, 패턴 규칙
잘 관찰해보시면 어쩌구.c -----------> 어쩌구.o 라는 관계가 매번 등장함을 알 수 있습니다. 이것을 매번 반복한다는 것은 소스 화일이 한 두 개 정도일 때야 모르지만 수십 개가 넘게 되면 정말 곤역스러운 일이라고 하지 않을 수 없지요.
다음과 같은 표현을 Makefile 에서 보는 경우가 많을 겁니다.
.c.o: gcc -c ${CFLAGS} $<
여기서 .c.o 의 의미를 생각해보겠습니다. ".c 를 입력화일로 받고 .o 화일을 만든다"
gcc -c ${CFLAGS} $<
이 문자을 보면 일단 눈에 띄는 것은 ${CFLAGS}라는 표현과 $< 라는 암호와도 같은 표현입니다. 여기서는 일단 $< 라는 기호의 의미를 알아보겠습니다.
유닉스에서 쉘을 잘 구사하시는 분들은 눈치채셨을 겁니다. 작다 표시(<)는 리다이렉션에서 입력을 의미하는 것을 아십니까? 그렇다면 $< 는 바로 .c.o 라는 표현에서 .c 즉 C 소스 화일을 의미합니다.
예를 들어 foo.c 가 있다면 자동으로
gcc -c ${CFLAGS} foo.c
가 수행되며 gcc 에 -c 옵션이 붙었으므로 foo.o 화일이 만들어질 것입니다.
3.4 GNU make 확장 기능
.c.o 라는 전통적인 표현 말고 GNU 버전( 우리가 리눅스에서 사용하는 것은 바로 이것입니다 )의 make 에서 사용하는 방법을 알아봅시다.
위에서 예로 든 것을 GNU 버전의 make 에서 지원하는 확장문법을 사용하면 다음과 같습니다.
%.o: %.c gcc -c -o $@ ${CFLAGS} $<
그냥 설명 전에 잘 살펴보시기 바랍니다.
우리가 위에서 알아보았던 표준적인 .c.o 라는 꼬리말 규칙(Suffix rule)보다 훨씬 논리적이라는 것을 발견하셨습니까?
우리가 바로 전 강의에서 main.o : main.c 이런 식으로 표현한 것과 같은 맥락이지요? 이것을 우리는 패턴 규칙(Pattern rule)이라고 부릅니다. 콜론(:) 오른쪽이 입력 화일이고 왼쪽이 목표 화일입니다. 화일명 대신 퍼센트(%) 문자를 사용한 것만 유의하면 됩니다. 여기서 foo.c 라는 입력화일이 있다면 % 기호는 foo 만을 나타냅니다.
gcc -c -o $@ ${CFLAGS} $<
라는 표현을 해석해봅시다. ( 후 마치 고대 문자판을 해석하는 기분이 안드십니까? ^^ )
$< 는 입력화일을 의미하고 $@ 은 출력화일을 의미합니다. .c.o와 같은 꼬리말 규칙과 별 다를 바 없다고 생각하실 지 모르나 -o $@ 를 통하여 .o 라는 이름 말고 전혀 다른 일도 해낼 수 있습니다.
다음 예는 그냥 이런 예가 있다는 것만 한 번 보아두시기 바랍니다.
%_dbg.o: %.c gcc -c -g -o $@ ${CFLAG} $< DEBUG_OBJECTS = main_dbg.o edit_dbg.o edimh_dbg: $(DEBUG_OBJECTS) gcc -o $@ $(DEBUG_OBJECTS)
%_dbg.o 라는 표현을 잘 보십시요. foobar.c 라는 입력화일(%.c)이 있다면 % 기호는 foobar 를 가리키므로 %_dbg.o 는 결국 foobar_dbg.o 가 됩니다.
- 기호정리
-
$< 입력 화일을 의미합니다. 콜론의 오른쪽에 오는 패턴을 치환합니다. $@ 출력 화일을 의미합니다. 콜론의 왼쪽에 오는 패턴을 치환합니다. $* 입력 화일에서 꼬리말(.c, .s 등)을 떼넨 화일명을 나타냅니다.
역시 GNU 버전이라는 생각이 들지 않으시는지요?
3.5 매크로(Macro) 기능
앞에서도 잠깐씩 나온 ${CFLAGS} 라는 표현을 보도록 합시다.
gcc 옵션도 많이 알고 make을 능수능란하게 다룰 수 있는 사람들은 다음과 같이 해서 자신의 프로그램에 딱 맞는 gcc 옵션이 무엇인지 알아내려고 할 것입니다.
$ make CFLAGS="-O4" $ make CFLAGS="-g"
이제 매크로에 대한 이야기를 나눠볼까 합니다. 이 이야기를 조금 해야만 위의 예를 이해할 수 있다고 보기 때문입니다. 그냥 시험삼아 해보십시다. 새로운 것을 배우기 위해서는 꼭 어떤 댓가가 와야만 한다는 생각을 버려야겠지요?
myprog: main.o foo.o gcc -o $@ main.o foo.o
이것을 괜히 어렵게 매크로를 이용하여 표현해보기로 하겠습니다.
OBJECTS = main.o foo.o myprog: $(OBJECTS) gcc -o $@ $(OBJECTS)
여러분은 보통 긴 Makefile을 훔쳐 볼 때 이런 매크로가 엄청나게 많다는 것을 보신 적이 있을 겁니다. ^^
ROOT = /usr/local HEADERS = $(ROOT)/include SOURCES = $(ROOT)/src
예상하시듯 위에서 HEADERS는 당연히 /usr/local/include가 되겠지요?
다음과 같은 문장도 있습니다.
ifdef XPM LINK_DEF = -DXPM endif
$ make XPM=yes
이렇게 하면 ifdef endif 부분이 처리됩니다.
자, make CFLAGS="-O" 이런 명령을 한 번 봅시다. ${CFLAGS}에서 {} 표현은 유닉스 쉘에서 변수값을 알아낼 때 쓰는 표현입니다. CFLAGS 값을 여러분이 Makefile에 고정적으로 집어넣지 않고 그냥 make 만 실행하는 사람에게 선택권을 주기 위해서 사용하거나 자기 스스로 어떤 옵션이 제일 잘 맞는지 알아보기 위해서 사용합니다. 다른 옵션으로 컴파일하는 것마다 일일이 다른 Makefile을 만들지 말고 가변적인 부분을 변수화하는 것이 좋습니다.
3.6 마지막 주의 사항
target: cd obj HOST_DIR=/home/e mv *.o $HOST_DIR
하나의 목표에 대하여 여러 명령을 쓰면 예기치 않은 일이 벌어집니다. 기술적으로 말하자면 각 명령은 각자의 서브쉘에서 실행되므로 전혀 연관이 없습니다. -.- cd obj 도 하나의 쉘에서 HOST_DIR=/home/e도 하나의 쉘에서 나머지도 마찬가지입니다. 각기 다른 쉘에서 작업한 것처럼 되므로 cd obj 했다 하더라도 다음번 명령의 위치는 obj 디렉토리가 아니라 그대로 변함이 없이 현재 디렉토리입니다. 세번째 명령에서 HOST_DIR 변수를 찾으려 하지만 두번째 명령이 종료한 후 HOST_DIR 변수는 사라집니다.
target: cd obj ; \ HOST_DIR=/hom/e ; \ mv *.o $$HOST_DIR
이렇게 적어주셔야 합니다. 세미콜론으로 각 명령을 구분하지요. 처음 두 줄의 마지막에 쓰인 역슬래쉬(\) 문자는 한 줄에 쓸 것을 여러 줄로 나누어 쓴다는 것을 나타내고 있습니다.
주의! 세번째 줄에 $HOST_DIR이 아니라 $$HOST_DIR인 것을 명심하십시요. 예를 하나 들어보죠. ^^
all: HELLO="안녕하세요?";\ echo $HELLO
Makefile의 내용을 이렇게 간단하게 만듭니다.
$ make HELLO="안녕하세요?";\ echo ELLO ELLO <verb> 우리가 원하는 결과가 아니죠? $HELLO를 $$HELLO로 바꾸어보십시요. <verb> $ make HELLO="안녕하세요?";\ echo $HELLO 안녕하세요?
all: @HELLO="안녕하세요?"; echo $$HELLO
명령의 맨 처음에 @ 문자를 붙여봅시다.
$ make 안녕하세요?
3.7 잠시 마치면서
Makefile에 대한 내용은 이것보다 훨씬 내용이 많습니다. 하지만 모든 것을 다 알고 시작할 수는 없겠지요? 이 정도면 어느 정도 충분하게 창피하지 않을 정도의 Makefile을 만들 수 있습니다.
참고로 autoconf/automake라고 하는 아주 훌륭한 GNU make 유틸리티를 시간나면 배워보시는 것도 좋습니다.
시간을 내서 리눅스에서의 C 프로그래밍에 필요한 다른 여러 가지 유틸리티들( 간접적이든 직접적이든 grep, awk, rcs, cvs 등 )의 간단/실전 사용법도 올려드릴까 생각 중입니다. ^^