서론
윈도우 환경에서 비주얼 스튜디오의 컴파일 버튼을 누르면 알아서 컴파일 되는 것과는 달리,
쉘 상에서 컴파일을 하려면 어떤 파일들을 컴파일 하고, 어떠한 방식으로 컴파일 할 지 컴파일러에게 알려줘야 한다.
프로젝트의 크기가 커지고 파일들이 많아지면 매번 명령어를 친다는 것이 불가능에 가까워진다.
이 문제를 해결하기 위해서 리눅스에서는 make 라는 프로그램을 제공하는데, 이 프로그램은 Makefile 라는 파일을 읽어서 주어진 방식대로 명령어를 처리하게 한다.
덕분에 많은 수의 파일들을 명령어 한 번으로 컴파일 할 수 있습니다.
이 장에서는 make 프로그램이 어떠한 방식으로 작동되고, 프로그램들을 우리가 원하는 방식으로 컴파일 하기 위해서는 어떠한 방식으로 Makefile 을 작성해야 하는지 알아보겠다.
목차
컴파일(Compile)
소스 코드를 컴퓨터가 이해할 수 있는 어셈블리어로 변환하는 과정
- foo.h에 foo함수가 정의
- bar.h에 bar함수가 정의
- foo와 bar함수를 main에서 호출
아래 명령어는 g++에 전달하는 -c 인자 다음에 주어지는 파일을 컴파일 해서 목적파일(object file)로 생성하라
$ g++ -c main.cc
그럼 다음과 같이 오브젝트 파일이 생긴다.

아래 명령어를 통해 열어볼 수 있다.
$ objdump -S main.o

그러나 main.o 파일에는 foo, bar라는 함수를 호출해라 라는 내용만 있지 foo, bar는 어디에 있고 어떤 방식으로 작동한다에 관한 내용은 없는 것을 볼 수 있다.
-
main.o자체로는 프로그램을 만들 수 없다.
2. foo, bar에 관한 정보를 얻기 위해서는 foo.cc, bar.cc를 컴파일해서 만들어진 foo.o, bar.o가 main.o와 하나로 합쳐지는 과정이 필요하다.
이와 같은 과정을 아래 그림과 같이 링킹(Linking) 이라고 한다.
링크하기(Linking)
-o 옵션 뒤에 링킹 후에 생성할 파일 이름을 말한다.
$ g++ main.o foo.o bar.o -o main
Make
주어진 쉘 명령어들을 조건에 맞게 실행하는 프로그램
target … : prerequisites …
(tap)recipe
…
…
Target
make를 실행할 때 어떤 것을 make할 지 전달하는 곳
실행할 명령어(recipes)
반드시 Tap 한 번으로 들여쓰기 해야하며 make할 때 실행할 명령어들의 나열
필요 조건들(prerequisits)
주어진 타겟을 make할 때 사용되는 의존 파일(dependency)
Makefile을 만들고 make를 해보자
foo.o : foo.h foo.cc
g++ -c foo.cc
bar.o : bar.h bar.cc
g++ -c bar.cc
main.o : main.cc foo.h bar.h
g++ -c main.cc
main : foo.o bar.o main.o
g++ foo.o bar.o main.o -o main
이후 아래 명령어를 입력하면 main이라는 파일이 만들어진다.
$ make main
Make main이니까 Makefile에서 target이 main인 녀석을 찾는다.- 보니까
main에 필요한 파일들이foo.obar.omain.o이네. 이들 파일을 어떻게 만드는지 각각의 파일 이름으로된 타겟들을 찾아본다. foo.o의 경우 필요한 파일이foo.cc네. 아직foo.o가 없으니까 주어진 명령어를 실행해서 만든다.- 마찬가지로
bar.o,main.o도 컴파일한다. - 마지막으로 g++ foo.o bar.o main.o -o main을 실행한다.
변수
Makefile내에 변수를 정의할 수 있다.
$(CC)와 같이 $()안에 사용하고자 하는 변수의 이름을 지정한다.
CC = g++
foo.o : foo.h foo.cc
$(CC) -c foo.cc
는 아래와 같은 명령이 된다.
CC = g++
foo.o : foo.h foo.cc
g++ -c foo.cc
변수를 사용한 Makefile
보통 CC 에는 사용하는 컴파일러 이름, CXXFLAGS에는 컴파일러 옵션을 주는 것이 일반적임
CC = g++
CXXFLAGS = -Wall -O2
OBJS = foo.o bar.o main.o
foo.o : foo.h foo.cc
$(CC) $(CXXFLAGS) -c foo.cc
bar.o : bar.h bar.cc
$(CC) $(CXXFLAGS) -c bar.cc
main.o : main.cc foo.h bar.h
$(CC) $(CXXFLAGS) -c main.cc
main : $(OBJS)
$(CC) $(CXXFLAGS) $(OBJS) -o main
패턴사용(명령어 간소화)
아래 명령어를 다음과 같이 간소화 할 수 있다.
CC = g++
CXXFLAGS = -Wall -O2
foo.o : foo.h foo.cc
$(CC) $(CXXFLAGS) -c foo.cc
bar.o : bar.h bar.cc
$(CC) $(CXXFLAGS) -c bar.cc
간소화
%.o 는 *.o와 같은 의미를 같는다.
%.o: %.cc %.h
$(CC) $(CXXFLAGS) -c
← 글 목록
Loading
글을 불러오는 중입니다
lt;
예를들어 target이 foo.o 라면.. 다음과 같다.
foo.o: foo.cc foo.h
$(CC) $(CXXFLAGS) -c
← 글 목록
Loading
글을 불러오는 중입니다
lt;
❗ 패턴은 target, prerequisite 부분에서만 사용할 수 있다.
Loading 의 경우 글을 불러오는 중입니다
prerequisite 에서 첫 번째 파일의 이름에 대응되어 있는 변수다.
위 경우 foo.cc 가 된다. 따라서 위 명령어는 결과적으로 아래와 같다.
foo.o: foo.cc foo.h
$(CC) $(CXXFLAGS) -c foo.cc
Makefile에서 제공하는 자동 변수로는 Loading$@, , 글을 불러오는 중입니다
$^등등이 있다.

$@: 타겟 이름에 대응: 의존 파일 목록에 첫 번째 파일에 대응← 글 목록 lt;Loading
글을 불러오는 중입니다
$^: 의존 파일 목록 전체에 대응
최종 정리
아래와 같은 폴더 구조를 갖는다고 하자.
$ tree
.
├── include
│ ├── bar.h
│ └── foo.h
├── Makefile
├── obj
└── src
├── bar.cc
├── foo.cc
└── main.cc
그럼 다음과 같은 Makefile을 만들 수 있다.
CC = g++
# C++ 컴파일러 옵션
CXXFLAGS = -Wall -O2
# 링커 옵션
LDFLAGS =
# 헤더파일 경로
INCLUDE = -Iinclude/
# 소스 파일 디렉토리
SRC_DIR = ./src
# 오브젝트 파일 디렉토리
OBJ_DIR = ./obj
# 생성하고자 하는 실행 파일 이름
TARGET = main
# Make 할 소스 파일들
# wildcard 로 SRC_DIR 에서 *.cc 로 된 파일들 목록을 뽑아낸 뒤에
# notdir 로 파일 이름만 뽑아낸다.
# (e.g SRCS 는 foo.cc bar.cc main.cc 가 된다.)
SRCS = $(notdir $(wildcard $(SRC_DIR)/*.cc))
OBJS = $(SRCS:.cc=.o)
DEPS = $(SRCS:.cc=.d)
# OBJS 안의 object 파일들 이름 앞에 $(OBJ_DIR)/ 을 붙인다.
OBJECTS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(OBJS))
DEPS = $(OBJECTS:.o=.d)
all: main
$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cc
$(CC) $(CXXFLAGS) $(INCLUDE) -c
← 글 목록
Loading
글을 불러오는 중입니다
lt; -o $@ -MD $(LDFLAGS)
$(TARGET) : $(OBJECTS)
$(CC) $(CXXFLAGS) $(OBJECTS) -o $(TARGET) $(LDFLAGS)
.PHONY: clean all
clean:
rm -f $(OBJECTS) $(DEPS) $(TARGET)
-include $(DEPS)
함수 설명
wildcard 는 함수로 해당 조건에 맞는 파일들을 뽑아낸다.
foo.cc, bar.cc, main.cc 가 있을 경우 $(wildcard $(SRC_DIR)/*.cc) 의 실행 결과는 ./src/foo.cc ./src/bar.cc ./src/main.cc 가 된다.
경로를 제외한 파일 이름인 foo.cc bar.cc main.cc 로 뽑아내려면 notdir 함수를 사용한다. notdir 은 앞에 오는 경로를 날려버리고 파일 이름만 깔끔하게 추출해준다.
# Make 할 소스 파일들
# wildcard 로 SRC_DIR 에서 *.cc 로 된 파일들 목록을 뽑아낸 뒤에
# notdir 로 파일 이름만 뽑아낸다.
# (e.g SRCS 는 foo.cc bar.cc main.cc 가 된다.)
SRCS = $(notdir $(wildcard $(SRC_DIR)/*.cc))
따라서 아래 부분에서 OBJS 는 foo.o bar.o main.o 가 될 것입니다.
OBJS = $(SRCS:.cc=.o)
이제 이 OBJS를 바탕으로 실제 .o파일들의 경로를 만들어내야 한다.
patsubst함수를 사용하면 이들 파일 이름 앞에 $(OBJ_DIR)/을 붙여줄 수 있다.
patsubst함수는 $(patsubst 패턴,치환 후 형태,변수)의 같은 꼴로 사용한다.
아래의 명령어는 $(OBJS) 안에 있는 모든 %.o 패턴을 $(OBJ_DIR)/%.o 로 치환해라 의미
# OBJS 안의 object 파일들 이름 앞에 $(OBJ_DIR)/ 을 붙인다.
OBJECTS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(OBJS))
위 명령어의 결과 OBJECTS 에는 ./obj/foo.o ./obj/bar.o ./obj/main.o이 들어가게 된다.
멀티코어를 사용해 Make 속도를 올리자
make실행하게 되면 1 개의 쓰레드만 실행되어서 속도가 느리다.
그런데 make를 여러 개의 쓰레드에서 돌릴 수 있다.
아래 명령어는 make가 8 개의 쓰레드에 나뉘어서 실행된다.
$ make -j8
통상적으로 코어 개수 + 1 만큼의 쓰레드를 생성해서 돌리는 것이 가장 속도가 빠르다.
만약 코어 개수를 모른다면 아래 명령어를 사용하자(리눅스)
$ make -j$(nproc)
$(nproc)이 알아서 내 컴퓨터의 현재 코어 개수로 치환됩니다.
코어 갯수 확인하는 명령어는 아래와 같다.
$ grep -c processor /proc/cpuinfo
[reference]
Comment