메뉴

문서정보

만든날 2005/5/15 <|4> http://joinc.co.kr/images/myavata/1045.gif
관리자 minzkn
홈페이지 http://minzkn.pe.ky
이메일 [mailto:minzkn@infoeq.com]

목차

교정 과정

  1. 2004년 어느날 끄적이기 시작
2. 2005/5/15 minzkn의 홈페이지에서 썩어가는 문서를 joinc 로 릴리즈

시작하기전에

프로그래밍을 배우려면 우선 "make"의 기본용법이라도 알고 시작하자는 주장을 해봅니다. 그 만큼 매우 중요한 유틸리티입니다. 나름대로 간편한 문법과 가독성이 높은 문법 때문에 요즘 대부분의 거대한 공개 프로젝트들은 "make"를 널리 사용하고 있습니다. 꼭 프로그래밍의 세계에 입문을 하고자 하시는 분들께서는 "make"의 사용법을 적어도 기본문법이라도 알고 계셔야 합니다. 그래서 필자는 간략하게나마 도움이 되고자 이렇게 문서로 남깁니다. 부디 멋진 GNU 개발자가 되시길 바랍니다.

Make 의 목적

우리가 일반적으로 개발하고 관리하는 하나의 프로젝트는 C source를 컴파일하여 Object 파일을 만들고 이러한 Object 몇개를 묶어서 링크(Link)과정을 거쳐서 최종 목적인 실행파일을 생성합니다. 이때 사용하는 명령은 다음과 같겠죠. 아~ 무지 길다~!
bash# cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c 
bash# ls 
test.c test.o 
bash# ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test.o 
bash# ./test 
Hello world ! 
bash# _ 

소스의 내용을 조금만 수정해도 이 과정은 반복되어야 합니다. 참~~~ 번거로운 일이 우리의 앞길?막고 있는듯한 느낌입니다. 그래서 이를 쉽게 간략화 해주는 유틸리티가 탄생했는데 "make" 가 바로 그것입니다. 실제로 "make"의 몇몇 기능만 사용하면 위의 문제는 쉽게 해결되지만 그 밖에 대규모 프로젝트에서 겪을수 있는 불편함을 해소해줄수 있다는 점이 보다 매력으로 다가올것입니다.

기본규칙

"make"는 크게 Target, Depend, Command, Macro 로 구성되어 있습니다. 우선 기본적으로 Target, Depend, Command 의 구성을 살펴보겠습니다.
<Target>: <Depend> ?... [[;] <Command>] 
<탭문자><Command> 

여기서 "Target"은 생성하고자 하는 목적물을 지칭하며 Depend 는 Target을 만들기 위해서 필요한 요소를 기술하게 됩니다. 그리고 Command 는 일반 Shell 명령이 옵니다. 이때 Command는 Depend 의 파일생성시간(또는 변경된 시간)을 Target과 비교하여 Target 보다 Depend의 파일이 시간이 보다 최근인 경우로 판단될때에만 실행됩니다. 물론 이것에 대한 예외적인 규칙이 있습니다만 일단 무시하고 받아들이세요. 이제 간단히 다음과 같이 "Makefile" 이라는 파일명으로 다음과 같이 작성하여 "make"의 행동을 기술합니다. 단, 주의할것은 Command 는 반드시 앞에 <TAB>문자가 와야 합니다. 물론 예외상황이 있기는 하지만 나중에 그에 대한 내용을 다루기로 하고 일단 ld, cc 명령 앞에 반드시 <TAB>문자로 입력하세요.
test: test.o 
        ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test.o 

test.o: test.c 
        cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c 

이제 명령프롬프트상에서 "make test" 라고만 입력하면 다음과 같이 실행됩니다.
bash# make test 
cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c 
ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test.o 
bash# _ 

엇? 그런데 다시 실행해보시면 놀랍게도 이런 메세지가 나올겁니다. 이것이 바로 "make" 의 중요한 기능중에 한가지 입니다. 바로 똑같은 작업은 다시 해봤자 어차피 결과가 같을것이라는 예상때문에 더이상 같은 작업을 하지 않는 것이지요.
bash# make test 
make: `test'는 이미 갱신되었습니다. 
bash# _ 

그러면 간단히 다음과 같이 "test.c" 의 변경날짜를 바꿔봅시다. 그리고 다시 한번 "make test"명령을 수행해봅시다. 어떻습니까? 이번에는 다시 명령을 수행하는것을 보실수 있을겁니다.
bash# touch test.c 
bash# make test 
cc -O2 -Wall -Werror -fomit-frame-pointer -c -o test.o test.c 
ld -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o test.o 
bash# _ 

이제 천천히 분석해보자면 Target 보다 Depend의 변경시간이 최근이라면 Command 를 수행한다고 하였습니다. 그래서 "test" 라는 Target이 "test.o" 에 의존관계를 갖고 있는데 "test.o"는 다시 Target으로 기술되어 있고 여기에 "test.c"가 의존관계로 기술되어 있습니다. 때문에 "test.c"가 최근에 변경되어 "test.o"보다 변경된 날짜가 최근이 되면 "test.o"를 새로 생성하게 됩니다. 또한 "test.o"는 "test"보다 최근에 변경된것으로 보이므로 "test"는 "test.o"에 의해서 새로 생성되는 결과를 가져옵니다. 결국 소스가 변경되지 않으면 "make"는 아무것도 안하지만 소스가 변경되면(변경된 날짜가 갱신되면) "test"는 새롭게 빌드되는 것입니다. 이제 대충 의존관계 성립을 어떻게 기술하는지 보았습니다. 하지만 이렇게 작성하면 오히려 더 불편하다고 불만을 토하는 분들이 속출할것입니다. 그렇습니다. 실제로는 저렇게 작성하는 경우는 별로 쓰이지 않습니다. 조금더 세련되게 작성하도록 Macro 의 사용이 준비되어 있으니 불만은 이제 그만 하세요.

Macro

매크로는 다음과 같이 "=" 문자의 왼편에는 Macro의 대표이름(Label)을 기술하고 오른편에는 그 내용을 적습니다. 이때 "=" 문자에 인접한 양쪽의 공백(Space)문자는 무시됩니다. 이것은 매우 기본적인 Macro를 예기하는것이며 이 밖에도 몇가지 확장된 Macro 가 있습니다. 하지만 일단 그것은 나중에 예기하도록 하겠습니다.
<Macro name> = <Macro 내용> 

자! 이제 간단히 다음과 같이 조금 개선해서 "Makefile"을 작성해보겠습니다.
CC = cc 
LD = ld 
CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c 
LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 
STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o 

test: test.o 
        $(LD) $(LDFLAGS) -o test $(STARTUP) test.o 

test.o: test.c 
        $(CC) $(CFLAGS) -o test.o test.c 

여기서 하나만 살펴본다면 "CC = cc" 라고 매크로 선언을 보면 "CC"라는 매크로명은 "cc"라는 명령어로 정의됩니다. 이것을 사용하기 위해서는 "$"기호뒤에 괄호"("과 ")"을 두고 그 안에 매크로 이름을 넣어 사용합니다. 즉, "$(CC)" 는 "cc"로 해석됩니다. 하지만 아직도 이것이 너무 복잡하다는 불만을 가지신분이 계실겁니다. "make"는 그래서 기본 확장자 규칙이라는 방법도 지원하고 있습니다.

기본 확장자 규칙

우리는 보통 C source를 목적파일로 컴파일합니다. 이것은 확장자가 통상 ".c"에서 ".o"를 만들어 내는 규칙이 생성될법 합니다. "make"는 이점에 착안하여 다음과 같은 확장자 규칙을 이용할수 있습니다. 물론 이것은 한가지 방법일 뿐이며 이 밖에도 몇가지 방법이 있습니다.
.c.o: 
        $(CC) $(CFLAGS) -o $@ $< 

이제 여기서 새롭게 등장한 "$@"와 "$^", "$<" 의 의미를 예기할때가 왔습니다. 이것은 "make"의 기본 정의된 Macro중에 한가지 입니다. "$@" 또는 "$(@)"는 바로 Target 을 말합니다. 그리고 "$<"는 열거된 Depend중에 가장 왼쪽에 기술된 1개의 Depend를 말하며 "$^"는 Depend 전체를 의미합니다. 이것은 앞으로도 "make"를 사용하는데 있어서 굉장히 많은 부분 기여하는 매크로이므로 눈여겨 보셔야 할 부분입니다. 이 밖에도 "$?" 로 있는데 이것은 Target과 Depend의 변경날짜를 비교하여 Depend의 변경날짜중에 최근에 변경된것만 선택하는 매크로입니다. "$?"는 주로 라이브러리의 생성 및 관리시에 사용되는데 나중에 시간되면 설명하겠습니다. 위의 기술은 확장자 ".c"를 가진 파일을 확장자 ".o"를 가진 파일로 생성하는 공통적인 확장자 규칙을 예로 작성한 것입니다. 한번 사용해볼까요?
CC = cc 
LD = ld 
CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c 
LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 
STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o 

test: test.o 
        $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ 

.c.o: 
        $(CC) $(CFLAGS) -o $@ $< 

이게 ".c.o" 는 .c 를 .o로 만들어 보자는 내용입니다. 그리고 "$<"는 .c 에 취해지는 소스파일명인 "test.c"로 대체됩니다. 또한 "$@"는 Target인 "test.o"로 대치될것입니다.

가짜 target

이제 지금까지 작성한 "Makefile"을 사용해보면 한가지 불편한 점을 느낀 분들이 계실겁니다. "test.c"로 "test.o"를 만들고 "test.o"로 "test"를 만들도록 작성하였지만 소스파일의 수정이 없는 경우 항상 "make"는 더이상의 컴파일 및 링크과정을 하지 않는 점입니다. 때문에 간혹 다시 깨끗히 빌드하고 싶을때 "touch test.c"를 수행하거나 "rm test.o test" 를 수행해야 합니다. 이것 역시 매우 불편합니다. 이를 해소하고자 다음과 같이 작성될수 있겠습니다.
CC = cc 
LD = ld 
RM = rm -f 
CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c 
LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 
STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o 

clean: 
        $(RM) test.o test 

test: test.o 
        $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ 

.c.o: 
        $(CC) $(CFLAGS) -o $@ $< 

새로운 Target 인 "clean"이 보입니다. 그런데 Target "clean"에 대한 Depend가 없습니다. 이렇게 Depend 가 없으면 "clean"에 기술된 명령 "$(RM) test.o test"는 항상 실행되며 이것은 실제로 "clean"이라는 파일이 없습니다. 때문에 이것을 가짜 Target 이라고 합니다. 이제 "make clean" 하시고 "make test"하시면 항상 다시 빌드하는것을 볼수 있습니다. 하지만 만약 clean이라는 실행파일이 존재한다면 우리가 원하지 않는 결과가 나올법도 합니다. 물론 거의 대부분 "clean"이라는 실행파일을 만들지 않겠지만 우리는 여기서 "make"에서 Target "clean"는 가짜 Target 이라고 명확히 전달할 필요가 있습니다. 물론 불필요한 경우가 대부분의 경우지만 그래도 습관적으로 명시해주는게 좋습니다.
CC = cc 
LD = ld 
RM = rm -f 
CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -c 
LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 
STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o 

.PHONY: all clean 

all: test 

clean: 
        $(RM) test.o test 

test: test.o 
        $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ 

.c.o: 
        $(CC) $(CFLAGS) -o $@ $< 

자! 여기서 새로운 Target 인 ".PHONY" 가 보입니다. 이것은 "make"구문에서 예약된 Target 중에 하나이며 ".PHONY"에 명시된 Depend 는 모두 가짜 Target으로 보게 됩니다. 그리고 여기서 "all"이라는 Target이 추가되었습니다. 이것은 매우 관습적인 Target 으로 보통 최상단에 "all"과 "clean"이 위치하게 됩니다. 꼭 이렇게 해야 하는게 아니고 통상적인 관습이므로 반드시 따를 필요는 없습니다. 다만 다른 사람들도 함께 작업하는 프로젝트에서 통일성을 부여하기 위해서 이렇게 많이들 작성하게 됩니다. 위의 예제에서 Target "all"은 실제로 "all"이라는 파일을 생성하는것이 아니고 단지 "무엇이 최종 Target이다" 라는 암시적인 가짜 Target이 되며 "clean"은 "무엇을 깨끗이 정리하겠다" 라는 암시적인 가짜 Target이 됩니다. 이제 우리는 "make test"대신에 "make all"을 사용하게 될수 있습니다. 이때 "all"의 Depend로 "test"가 빌드될것입니다. 또한 "clean"은 관련된 파일을 정리하는것으로 용도가 정해집니다. 이때 "."으로 시작하는 Target을 제외하고 가장 처음 나오는 Target은 "make <target>" 에서 <target>을 생략해도 무관하게 됩니다. 때문에 ".PHONY"를 제외한 "all"이 가장 처음 나오는 Target이고 이제부터는 "make all"이 아니고 그냥 "make"만 입력해도 된다는 겁니다.

응용

이제 다음과 같이 4개의 파일을 작성하여 하나의 프로젝트를 작성하였다고 합시다.

hello.c
#include <stdio.h> 
void HelloWorld(void) 
{ 
 fprintf(stdout, "Hello world.\n"); 
} 

test.c
#include <stdio.h> 
#include "hello.h" 
int main(void) 
{ 
 HelloWorld(); 
 return(0); 
} 

hello.h
extern void HelloWorld(void); 

Makefile
CC = cc 
LD = ld 
RM = rm -f 
CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -v -c 
LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 
STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o 

BUILD = test 
OBJS = test.o hello.o 

.PHONY: all clean 

all: $(BUILD) 
clean: ; $(RM) *.o $(BUILD) 
test: $(OBJS) ; $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ 

# 의존관계 성립 
hello.o: $($@:.o=.c) $($@:.o=.h) Makefile 
test.o: $($@:.o=.c) hello.h Makefile 

# 확장자 규칙 (컴파일 공통 규칙) 
.c.o: ; $(CC) $(CFLAGS) -o $@ $< 

자! 이제 이 4개의 파일로 어떤 임의의 프로젝트를 만들었습니다. 엇? 근데 조금 이상한 부분이 보이죠? 하지만 이것은 매우 잘 동작하는 "Makefile"입니다. 이제 "make clean" 후에 "make" 명령을 입력하면 조용히 컴파일을 하게 될겁니다. 그리고 make 가 어떤 확장을 보이는지 궁굼하다면 "make -p" 명령을 통해서 "-p"옵션을 사용할수 있습니다. 꽤 많은 메세지를 확인할수 있을겁니다. 앞으로도 "-p"를 이용하면 make 가 어떤 확장을 보이는지 확인가능하니 낚시하는 법을 배운셈입니다.

위에서 "$($@:.o=.c)" 라는 이상한 문자열이 좀 마음에 안들겁니다. 하지만 이것은 매우 함축적인 매크로이며 많이들 애용하고 있는 겁니다. 대략 다음과 같은 형식을 사용합니다. "$(<문자열>:<우측으로부터 매칭될 문자열>=<치환될 문자열>)" 이것은 즉, "$@" 부분은 자신의 Target인 "hello.o" 또는 "test.o"를 말합니다. 그리고 거기서 우측으로부터 ".o"가 발견되면 ".c"로 치환하라는 뜻입니다. 즉, "$(hello.o:.o=.c)" 또는 "$(test.o:.o=.c)"로 확장되고 여기서 다시 각각 "hello.c" 와 "test.c"로 치환되어 결국 해당 소스를 지칭하게 되는 셈입니다.

그리고 Command 부분이 <TAB>이 쓰이지 않고 한줄에 ";"(세미콜론)으로 분리되어서 해당 라인에 직접 Command 가 쓰이는 것을 확인하실수 있을겁니다. 무지 거대한 "Makefile"을 간략히 보이게 하기 위해서 이렇게도 사용할수 있다는 것을 예로 보인것입니다. 의존관계를 성립하는 부분은 Command 가 없는것을 볼수 있는데 이것은 비슷한 다른 Target에서 Command 가 결합되어 수행될수 있고 여기서는 ".c.o: ; ..." 부분의 Command 가 결합됩니다. 여기서 의존관계를 최대한 자세하게 기술하였는데 만약 "hello.h" 가 변경된다면 "hello.o"와 "test.o"는 다시 빌드될것입니다. 또한 "Makefile" 도 수정되면 다시 빌드될것이라는 것이 예상됩니다. 이처럼 의존관계를 따로 기술하는 이유는 차후에 여러분들이 사용하시다보면 이유를 알게 될겁니다. 의존관계라는게 서로 굉장히 유기적으로 걸리는 경우가 많기 때문에 보다 보기 편하게 하는 이유도 있고 차후에 의존관계가 변경되었을때 쉽게 찾아서 변경을 할수 있도록 하는것도 한가지 이유입니다.

아주 조금만 공통적인 의존관계를 정리해서 작성한다면 다음과 같이도 작성할수 있습니다. 바로 Target이 한번에 2개 이상이 정의될수도 있다는 겁니다.
CC = cc 
LD = ld 
RM = rm -f 
CFLAGS = -O2 -Wall -Werror -fomit-frame-pointer -v -c 
LDFLAGS = -lc -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 
STARTUP = /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o 

BUILD = test 
OBJS = test.o hello.o 

.PHONY: all clean 

all: $(BUILD) 
clean: ; $(RM) *.o $(BUILD) 
test: $(OBJS) ; $(LD) $(LDFLAGS) -o $@ $(STARTUP) $^ 

# 의존관계 성립 
$(OBJS): $($@:.o=.c) hello.h Makefile 
# test.o hello.o: $($@:.o=.c) hello.h Makefile 

# 확장자 규칙 (컴파일 공통 규칙) 
.c.o: ; $(CC) $(CFLAGS) -o $@ $< 

토론및 잡담

조금씩 "make"문법을 익힐때마다 왠지 암호화 같은 냄새가 나지만 결국 나중에 사용하다 보면 굉장히 유용합니다. 일단 주의할것은 되도록이면 Depend 는 최대한 자세히 걸어주면 좋다는 점을 강조하면서 이만 마치겠습니다. 비록 이 문서는 모든 "make"문법을 다루지는 못하였지만 매우 기본적인 "make"의 기능을 익히는데 목적을 가지고 작성하였고 사실상 훨씬 많은 기능이 있다는 점을 꼭 염두해두시고 연구해보세요.

참고문헌