한국어
Linux Programming
 

3. 매크로(Macro) 와 확장자(Suffix) 규칙

pjk 2014.02.05 20:45 조회 수 : 2590


3.1 매크로란 무엇인가? (What is Macro)

앞에서 매크로에 대해서 대충 언급을 했다. 프로그램을 짜본 사람이나 로터스, 한글, 엑셀 등의 모든 패키지에서 매크로라는 것을 사용하게 된다. 은연중에 매크로의 정의는 대충 짐작하고 있을 것이다. 이미 알고 있는바와 같이 매크로는 특정한 코드를 간단하게 표현한 것에 지나지 않는다. Makefile에서 사용되는 매크로는 비교적 그 사용법이 간단하기 때문에 금방 익혀서 사용할 정도가 된다.

매크로의 정의는 프로그램을 작성할 때 변수를 지정하는 것처럼 하면 된다. 그리고, 매크로를 사용하기 위해서는 $(..)을 이용하면 된다. 아래는 매크로의 간단한 예제이다.

=> 참고: 매크로의 사용에서 ${..}, $(..), $..를 모두 사용할 수 있습니다. 그러나 대부분의 책에서는 $(..) 을 사용하라고 권하는군요.

Makefile예제 4

OBJS = main.o read.o write.o

test : $(OBJS) <- (1)
gcc -o test $(OBJS)
                ..........
 

첫 번째 장에서 다루었던 예제와 거의 비슷하다. 매크로는 사실상 복잡한 것을 간단하게 표시한 것에 지나지 않는다. (1) 번을 매크로를 안 쓰고 표현한다면 아마 아래와 같이 될 것이다.

Makefile예제 5

test : main.o read.o write.o 
gcc -o test main.o read.o write.o
 

=> 참고: 예제 5가 더 쉽지 않느냐고 반문하는 사람은 매크로의 위력을 잘 모르는 사람입니다. 거의 모든 소프트웨어에서 매크로를 지원하는 이유를 한번 잘 생각해 봅시다. 예제 4 의 (1)부분이 이해하기 난해하다고 하실 지는 모르겠지만, 대충 형식이 정해져 있기 때문에 조금만 익숙해지면 오히려 더 편할 겁니다.

make에 관해 설명한 책에 다음과 같은 명언(?) 이 나온다.

Macro makes Makefile happy. (매크로는 Makefile 을 기쁘게 만든다.)

이 말은 Makefile을 작성함에 있어 매크로를 잘만 이용하면 복잡한 작업도 아주 간단하게 작성할 수 있음을 말해 주는 말이 아닐까 생각한다. 매크로에 대해서는 더 이상 말할 것이 없다. (너무 간단하죠 ?) 이제 남은 것은 여러분들이 자신의 매크로를 어떻게 구성하느냐이다. 어떤 것을 매크로로 정의해야 할지는 여러분들의 자유이며, 나중에 전반적인 지침을 설명할 것이다.

3.2 미리 정해져 있는 매크로 (Pre-defined macro)


여러분들보다 머리가 약간 더 좋은 사람들이 make 라는 것을 만들면서 미리 정해 놓은 매크로들이 있다. 'make -p' 라고 입력해 보면 make에서 미리 세팅되어 있던 모든 값들(매크로, 환경 변수(environment) 등등)이 엄청 스크롤 된다. 이 값들을 보고 미리 주눅 들 필요는 없다. 어차피 대부분의 내용들은 우리가 재정의 해주어야 하기 때문에 결론적으로 말하면 우리가 모두 작성한다고 생각하는 것이 마음이 편하다.,.

아래에는 대부분이 UNIX 계열의 make에서 미리 정해져 있는 매크로들 중에 몇 가지만 나열해 본 것이다.

Predefined Macro 예제 6

ASFLAGS = <- as 명령어의 옵션 세팅
AS = as
CFLAGS = <- gcc 의 옵션 세팅
CC = cc (= gcc)
CPPFLAGS = <- g++ 의 옵션
CXX = g++
LDLFAGS = <- ld 의 옵션 세팅
LD = ld
LFLAGS = <- lex 의 옵션 세팅
LEX = lex
YFLAGS = <- yacc 의 옵션 세팅
YACC = yacc
MAKE_COMMAND = make
 

=> 참고: 직접 make -p를 해서 한번 확인해 보세요. 과연 make는 내부적으로 어떤 변수들을 사용하고 있는지 알아봅시다. 매크로는 관습적으로 대문자로 작성되니까 이점에 유의해서 보세요. make는 쉘상에서 정의한 환경 변수값들을 그대로 이용한다는 것을 알고 계시기 바랍니다.

위에 열거한 매크로는 make에서 정의된 매크로중 그야말로 일부에 지나지 않는다. 하지만 프로그램을 작성함에 있어 가장 많이 사용하게 될 매크로 들이다. 이들 매크로는 사용자에 의해 재정의 가능하다. 가령 gcc의 옵션 중에 디버그 정보를 표시하는 '-g' 옵션을 넣고 싶다면, 아래와 같이 재정의 한다.

 CFLAGS = -g
예제 6 의 각종 FLAG 매크로들은 대부분 우리가 필요에 의해 세팅해 주어야 하는 값들이다. 왜 굳이 make에서 값도 정의되지 않은 매크로를 우리가 정의해서 써야 하는지 의문을 던질지도 모른다. 우리가 더 이쁜 이름으로 매크로를 정의할 수도 있다고 하면서...

여기서 한가지 사실을 생각해 봐야 할 것이다. make에서 위에 나온 것들을 왜 미리 정해 두었을까? (왜일까요?) make에서 이들 매크로를 제공하고 있는 이유는 내부적으로 이들 매크로를 사용하게 되기 때문이다. 어떻게 이용하는지는 확장자 규칙(Suffix rule)을 설명하면서 해답을 제공할 것이다. 이제 예제 4 의 Makefile을 매크로를 이용하여 깔끔하게(?) 작성해 보자.

Makefile예제 7

OBJECTS = main.o read.o write.o
SRCS = main.c read.c write.c <- 없어도 됨

CC = gcc <- gcc 로 세팅
CFLAGS = -g -c <- gcc 의 옵션에 -g 추가

TARGET = test <- 결과 파일을 test 라고 지정

$(TARGET) : $(OBJECTS)
$(CC) -o $(TARGET) $(OBJECTS)

clean : 
                rm -rf $(OBJECTS) $(TARGET) core 

main.o : io.h main.c <- (1)
read.o : io.h read.c
write.o: io.h write.c
 

위의 Makefile 을 동작시켜 보자.

 % make 
gcc -g -c main.c -o main.o
gcc -g -c read.c -o read.o
gcc -g -c write.c -o write.o
gcc -o test main.o read.o write.o <- OK

% make clean
rm -rf main.o read.o write.o test core <- OK
그런데 여기서 한가지 이상한 점을 발견하게 될 것이다. .c 파일을 .o 파일로 바꾸는 부분이 없는데 어떻게 컴파일이 되었을까? 빼먹고 타이핑 못한 것은 아닐까 하고... 절대 아님!

앞에서 CFLAGS 같은 매크로는 make 파일의 내부에서 이용된다고 하였다. 그렇다면 make는 과연 어디에서 이용을 할까? 바로 컴파일하는 곳에서 이용을 하는 것이다. 따라서 우리는 CFLAGS를 셋팅해 주기만 하면 make가 알아서 컴파일을 수행하는 것이다. (얼마나 편리합니까!)

=> 참고: 확장자 규칙에서 다시 한번 자세히 설명을 하겠습니다.

(1) 에 해당하는 부분은 어떤 파일이 어디에 의존하고 있는지를 표시해 주기 위해서 꼭 필요하다. .c 파일을 컴파일하는 부분은 일괄적인 루틴으로 작성할 수 있기 때문에 이들 파일간의 의존 관계(dependency)를 따로 표시해 주어야 한다.

=> 참고: 파일간의 의존 관계를 자동으로 작성해 주는 유틸리티가 있습니다. 이것은 다음 장에서 다루기로 합니다.

3.3 확장자 규칙 (Suffix rule)


확장자 규칙이란 간단히 말해서 파일의 확장자를 보고, 그에 따라 적절한 연산을 수행시키는 규칙이라고 말할 수 있다. 가령 .c 파일은 일반적으로 C 소스 코드를 가리키며, .o 파일은 목적 파일(Object file)을 말하고 있다. 그리고 당연히 .c 파일은 컴파일되어서 .o 파일이 되어야 하는 것이다.

여기서 한가지 매크로가 등장하게 된다. .SUFFIXES 라고 하는 매크로인데 우리가 make 파일에게 주의 깊게 처리할 파일들의 확장자를 등록해 준다고 이해하면 될 것이다.

.SUFFIXES : .c .o

위의 표현은 '.c' 와 '.o' 확장자를 가진 파일들을 확장자 규칙에 의거해서 처리될 수 있도록 해준다. .SUFFIXES 매크로를 이용한 예제를 살펴보자.

Makefile예제 8

.SUFFIXES : .c .o 

OBJECTS = main.o read.o write.o
SRCS = main.c read.c write.c

CC = gcc 
CFLAGS = -g -c

TARGET = test

$(TARGET) : $(OBJECTS)
                $(CC) -o $(TARGET) $(OBJECTS)

clean : 
                rm -rf $(OBJECTS) $(TARGET) core 

main.o : io.h main.c 
read.o : io.h read.c
write.o: io.h write.c
 

위의 Makefile 을 동작시켜 보자.

 % make
gcc -g -c main.c -o main.o
gcc -g -c read.c -o read.o
gcc -g -c write.c -o write.o
gcc -o test main.o read.o write.o <- OK
확장자 규칙에 의해서 make는 파일들간의 확장자를 자동으로 인식해서 필요한 작업을 수행한다. 즉 아래의 루틴이 자동적으로 동작하게 된다.

 .c.o : 
$(CC) $(CFLAGS) -c $< -o $@
=> 참고: gmake에서는 약간 다르게 정의되어 있지만, 우선은 같다고 이해합시다. $< , $@ 에 대해서는 곧 설명합니다.

우리가 .SUFFIXES : .c .o 라고 했기 때문에 make 내부에서는 미리 정의된 .c (C 소스 파일)를 컴파일해서 .o (목적 파일)를 만들어 내는 루틴이 자동적으로 동작하게 되어 있다. CC와 CFLAGS 도 우리가 정의한 대로 치환될 것임은 의심할 여지가 없다.

make 내부에서 기본적으로 서비스를 제공해 주는 확장자들의 리스트를 열거해 보면 아래와 같다. 각 확장자에 따른 자세한 설명은 생략한다.

.out .a .ln .o .c .cc .C .p .f .F .r .y .l .s .S .mod .sym .def .h .info .dvi .tex .texinfo .texi .txinfo .w .ch .web .sh .elc .el

Makefile내부에서 .SUFFIXES 매크로의 값을 세팅해 주면 내부적으로 정의된 확장자의 연산이 동작을 하게 된다. 따라서 확장자 규칙은 make가 어느 확장자를 가진 파일들을 처리할 것인가를 정해 주는 것이라고 생각할 수 있다.

그러나 이것은 필자만의 생각일지 몰라도 make에서 자동적으로 확장자를 알아서 해주는 것이 좋긴 하지만, 필자는 일부러 위의 .c.o 에 해당되는 부분을 그냥 정의해서 쓰길 더 좋아한다. 이것은 지금까지의 습관상 그렇지만 왠지 우리가 정의하는 것이 더 자유롭게(flexible) 사용할 수 있을 것 같기 때문이다. 그리고 이런 기능은 우리가 작성을 해봐야 make의 메카니즘을 더 잘 이해할 수 있다고 생각한다.

예제 8 의 내용을 약간 바꾸어 보자.

Makefile예제 9

.SUFFIXES : .c .o 

OBJECTS = main.o read.o write.o
SRCS = main.c read.c write.c

CC = gcc 
CFLAGS = -g -c 
INC = -I/home/raxis/include <- include 패스 추가

TARGET = test

$(TARGET) : $(OBJECTS)
                $(CC) -o $(TARGET) $(OBJECTS)

.c.o : <- 우리가 확장자 규칙을 구현
                $(CC) $(INC) $(CFLAGS) $<-

clean : 
                rm -rf $(OBJECTS) $(TARGET) core 

main.o : io.h main.c
read.o : io.h read.c
write.o : io.h write.c
 

 % make
gcc -I/home/raxis/include -g -c main.c
gcc -I/home/raxis/include -g -c read.c
gcc -I/home/raxis/include -g -c write.c
gcc -o test main.o read.o write.o <- OK
예제 8 과 예제 9 의 차이는 그저 .c .o 부분을 누가 처리하느냐이다. 그리고 예제 9에서는 INC 라는 매크로를 추가시켜서 컴파일할때 이용하도록 하였다.

3.4 내부 매크로 (Internal macro)


make에서는 내부 매크로라는 것이 있다. 이것은 우리가 맘대로 정할 수 있는 매크로는 절대 아니다. 대신 매크로를 연산, 처리하는데 쓰이는 매크로라고 하는 것이 더 적당할 것이다.

Internal Macro 예제 10

$* <- 확장자가 없는 현재의 목표 파일(Target)

$@ <- 현재의 목표 파일(Target)

$< <- 현재의 목표 파일(Target)보다 더 최근에 갱신된 파일 이름

$? <- 현재의 목표 파일(Target)보다 더 최근에 갱신된 파일이름
 

=> 참고: 책에서는 $< 와 $?를 약간 구분하고 있지만 거의 같다고 봐도 무방할 것입니다.

각 내부 매크로에 대한 예를 보기로 한다.

 main.o : main.c io.h
gcc -c $*.c
$* 는 확장자가 없는 현재의 목표 파일이므로 $* 는 결국 main 에 해당한다.

 test : $(OBJS)
gcc -o $@ $*.c
$@는 현재의 목표 파일이다. 즉 test에 해당된다.

 .c.o :
gcc -c $< (또는 gcc -c $*.c)
$< 는 현재의 목표 파일보다 더 최근에 갱신된 파일 이름이라고 하였다. .o 파일보다 더 최근에 갱신된 .c 파일은 자동적으로 컴파일이 된다. 가령 main.o를 만들고 난 다음에 main.c를 갱신하게 되면 main.c는 $<의 작용에 의해 새롭게 컴파일이 된다.

=> 참고: 이제 예제 9 을 이해할 수 있겠습니까?

=> 참고: Makefile 파일을 작성해 놓고, 그냥 make만 치시면 make는 Makefile의 내용을 살펴보다가 첫 번째 목표 파일에 해당되는 것을 실행시키게 됩니다. 따라서 위의 예제에서는 make test 라고 해도 같은 결과를 내게 됩니다. 반면 clean에 해당하는 부분을 윗부분에 두게 되면 make는 항상 make clean을 수행하게 됩니다.

 % make <- make clean 이 실행됨
rm -rf main.o read.o write.o test core

% make test <- 강제적으로 test 가 생성되게 한다.
gcc -I/home/raxis/include -g -c main.c
gcc -I/home/raxis/include -g -c read.c
gcc -I/home/raxis/include -g -c write.c
gcc -o test write.c main.o read.o write.o <- OK
Makefile의 이해를 돕기 위해서 Makefile을 하나 더 작성해 보기로 한다. make.tex 파일을 make.dvi로 만든 다음 이것을 다시 make.ps로 만드는 것이다. 보통의 순서라면 아래와 같다.

 % latex make.tex <- make.dvi 가 만들어진다.
% dvips make.dvi -o <- make.ps 가 만들어진다.
보통의 가장 간단한 Makefile을 작성해 보면 아래와 같다.

Makefile예제 11

make.ps : make.dvi
                dvips make.dvi -o

make.dvi : make.tex
                latex make.tex 
 

위와 같은 일을 하는 Makefile을 다르게 한번 작성해 보자. 매크로를 어느정도 사용해 보기로 하며, 확장자 규칙을 한번 적용해 보기로 한다.

Makefile예제 12

.SUFFIXES : .tex .dvi 

TEX = latex <- TEX 매크로를 재정의

PSFILE = make.ps 
DVIFILE = make.dvi

$(PSFILE) : $(DVIFILE)
                dvips $(DVIFILE) -o

make.ps : make.dvi 
make.dvi : make.tex
 

예제 12 에서는 .tex 와 .dvi 를 처리하는 루틴이 자동적으로 동작을 하게 된다. Makefile 을 한번 동작시켜 보자.

 % make
latex make.tex
....
dvips make.dvi -o <- OK
예제 11 과 예제 12 는 하는 일은 같다. 하지만 예제 12는 매크로를 사용함으로써 나중에 내용을 바꿀 때 예제 11보다 편하다는 것을 이해하였으므로...