Make 사용 가이드


서론

윈도우 환경에서 비주얼 스튜디오의 컴파일 버튼을 누르면 알아서 컴파일 되는 것과는 달리,

쉘 상에서 컴파일을 하려면 어떤 파일들을 컴파일 하고, 어떠한 방식으로 컴파일 할 지 컴파일러에게 알려줘야 한다.

프로젝트의 크기가 커지고 파일들이 많아지면 매번 명령어를 친다는 것이 불가능에 가까워진다.

이 문제를 해결하기 위해서 리눅스에서는 make 라는 프로그램을 제공하는데, 이 프로그램은 Makefile 라는 파일읽어서 주어진 방식대로 명령어를 처리하게 한다.

덕분에 많은 수의 파일들명령어 한 번으로 컴파일 할 수 있습니다.

이 장에서는 make 프로그램이 어떠한 방식으로 작동되고, 프로그램들을 우리가 원하는 방식으로 컴파일 하기 위해서는 어떠한 방식으로 Makefile 을 작성해야 하는지 알아보겠다.

목차

  1. 컴파일(Compile)
  2. 링크하기(Linking)
  3. Make
  4. 변수
  5. 패턴사용(명령어 간소화)
  6. 최종 정리
  7. 함수 설명
  8. 멀티코어를 사용해 Make 속도를 올리자

컴파일(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는 어디에 있고 어떤 방식으로 작동한다에 관한 내용은 없는 것을 볼 수 있다.

  1. main.o 자체로는 프로그램을 만들 수 없다.

2. foo, bar에 관한 정보를 얻기 위해서는 foo.cc, bar.cc를 컴파일해서 만들어진 foo.o, bar.omain.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
  1. Make main 이니까 Makefile에서 target이 main인 녀석을 찾는다.
  2. 보니까 main에 필요한 파일들이 foo.o bar.o main.o 이네. 이들 파일을 어떻게 만드는지 각각의 파일 이름으로된 타겟들을 찾아본다.
  3. foo.o의 경우 필요한 파일이 foo.cc네. 아직 foo.o가 없으니까 주어진 명령어를 실행해서 만든다.
  4. 마찬가지로 bar.o, main.o 도 컴파일한다.
  5. 마지막으로 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 lt;

예를들어 targetfoo.o 라면.. 다음과 같다.

foo.o: foo.cc foo.h
	$(CC) $(CXXFLAGS) -c lt;

패턴은 target, prerequisite 부분에서만 사용할 수 있다.

lt; 의 경우 prerequisite 에서 첫 번째 파일의 이름에 대응되어 있는 변수다.

위 경우 foo.cc 가 된다. 따라서 위 명령어는 결과적으로 아래와 같다.

foo.o: foo.cc foo.h
	$(CC) $(CXXFLAGS) -c foo.cc

Makefile에서 제공하는 자동 변수로는 $@

lt;$^등등이 있다.

  • $@ : 타겟 이름에 대응
  • lt; : 의존 파일 목록에 첫 번째 파일에 대응
  • $^ : 의존 파일 목록 전체에 대응

최종 정리

아래와 같은 폴더 구조를 갖는다고 하자.

$ 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 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]

https://modoocode.com/311