컴퓨터반

컴퓨터반 게시판입니다.

제목C/C++ 빌드하기 : make, cmake2022-01-30 02:39
작성자user icon Level 3

88x31.png


1. 인터프리터 언어와 컴파일 언어


인터프리터 언어는 원시 코드를 기계어로 변환하지 않고 명령어를 바로 번역하여 실행하는 언어입니다.

파이썬(Python)이나 자바스크립트(Javascript) 같은 언어들이 이에 해당합니다. 기계어로 변환할 필요가 없기때문에 타입을 명시하지 않아도 되고 변경된 코드를 바로 실행할 수 있지만 컴파일 언어에 비해 속도는 느립니다.


컴파일 언어는 원시 코드를 기계어로 변환하여 실행하는 언어입니다.

C/C++이나 자바(Java) 같은 언어들이 이에 해당합니다. 컴파일 과정을 거치기때문에 빌드시간이 길지만 런타임 상황에서 인터프리터 언어보다 빠르게 실행가능합니다.



2. 컴파일(compile)과 링크(link)


컴파일 언어를 실행파일로 변환하는 과정을 빌드(build)라고 하며, 빌드를 하는 프로그램을 빌더(builder)라고 합니다. C언어는 gcc라는 빌더를 사용하고 C++은 g++이라는 빌더를 사용합니다.

빌드 과정에는 오브젝트 파일을 생성하는 컴파일(compile)과 오브젝트 파일을 하나의 실행파일로 엮어주는 링크(link) 과정이 있습니다. 컴파일을 하는 프로그램을 컴파일러(compiler), 링크를 하는 프로그램을 링커(linker)라고 합니다. 빌더는 컴파일러와 링커를 포함하고 있습니다.


컴파일 명령어는 다음과 같습니다.

 $ gcc -c [c언어 파일]


링크 명령어는 다음과 같습니다. a.out 이라는 실행 파일이 생성됩니다.

 $ gcc [오브젝트 파일 1] [오브젝트 파일 2] ... [오브젝트 파일 n]


오브젝트 파일이나 실행 파일의 이름을 지정하려면 -o 옵션을 넣어주세요.

 $ gcc [오브젝트 파일] -o [오브젝트 파일 이름]

 $ gcc [오브젝트 파일] -o [실행 파일 이름]



3. 빌드(build)


우선 작업공간을 만들고 이동하세요.

 $ mkdir c_ws && cd c_ws


c언어 파일을 만드세요. 확장자는 .c 입니다.

 $ touch main.c


다음과 같이 작성하고 저장하세요.

 $ xed main.c

#include <stdio.h>


void main(void) {

    printf("main!!\n");

}

mb-file.php?path=2022%2F01%2F29%2FF4574_1.png


컴파일을 해주세요. 

 $ gcc -c main.c


링크를 해주세요.

 $ gcc -o app main.o


실행해봅시다.

 $ ./app

mb-file.php?path=2022%2F01%2F29%2FF4575_2.png
 


4. Make와 Makefile


mb-file.php?path=2022%2F01%2F29%2FF4576_3.png
다음과 같이 3개의 소스파일을 통해 app이라는 실행파일을 만든다고 합시다. 

gcc를 통해 빌드해봅시다.


헤더파일과 c언어 파일을 생성하세요.

 $ touch {foo,bar}.{h,c}


각각 다음과 같이 작성하세요.

foo.h:

void foo(void);


bar.h:

void bar(void);


main.c:

#include "foo.h"

#include "bar.h"


int main(void) {

    foo();

    bar();


    return 0;

}


foo.c:

#include <stdio.h>


#include "foo.h"


void foo(void) {

    printf("foo!\n");

}


bar.c:

#include <stdio.h>


#include "bar.h"


void bar(void) {

    printf("bar!\n");

}


다음 명령어로 빌드하세요.

 $ gcc -c main.c

 $ gcc -c foo.c

 $ gcc -c bar.c

 $ gcc -o app main.o foo.o bar.o


4번의 명령어를 통해 app 파일을 만들었습니다. 쉘스크립트를 작성한다면 한번의 명령어로 작성 가능할듯 합니다.

하지만 파일 하나를 수정한다면 어떻게 될까요? 쉘스크립트를 통해 실행파일을 다시 만든다면 전체 파일을 빌드하게 될것입니다. 여기서는 간단한 코드들로 이루어져 있지만 코드가 길고 복잡해질수록 빌드시간은 증가하게 됩니다.  


make는 Makefile에 작성한 조건에 따라 선택적인 빌드를 가능하게 함으로써 이런 문제를 해결해줍니다.

예제를 통해 make에 대해 알아봅시다.


Makefile을 작성하세요.

 $ touch Makefile && xed Makefile

app: main.o foo.o bar.o

gcc -o app main.o foo.o bar.o


main.o: main.c foo.h bar.h

gcc -c main.c


foo.o: foo.c foo.h

gcc -c foo.c


bar.o: bar.c bar.h

gcc -c bar.c

mb-file.php?path=2022%2F01%2F29%2FF4577_4.png


Makefile의 기본 구조는 다음과 같습니다.

<Target>: <Dependencies>

<Recipe>


Target은 생성할 빌드 대상의 이름을 입력합니다. 즉, 오브젝트 파일이나 실행 파일의 이름입니다.


Dependencies는 빌드 대상이 의존성을 가지는 파일이나 Target 목록을 입력합니다. 빌드 대상을 생성하기전에 의존성 목록에 있는 Target부터 생성합니다.


Recipe는 빌드 대상을 생성하기 위한 명령어를 입력합니다. Tab으로 구분됩니다.

xed 에디터의 경우 초기 설정이 탭대신 공백을 사용하도록 되어있으므로 편집 > 기본설정 으로 이동하여 탭 대신 여백 사용OFF 해줍시다.

mb-file.php?path=2022%2F01%2F29%2FF4578_5.png
 

터미널에 다음 명령어를 통해 app 파일이 생성되는 것을 확인하세요.

make 명령어를 입력하기전에 기존에 존재하는 오브젝트 파일과 실행 파일을 제거하세요.

make 뒤에는 빌드 대상(Target)을 입력합니다. 생략하면 첫번째로 입력된 Target을 빌드합니다.

 $ rm *.o app && make

mb-file.php?path=2022%2F01%2F29%2FF4579_6.png
 

make를 입력하면 Makefile에 첫번째로 입력된 app을 빌드대상으로 하여 app의 의존성 파일을 검사합니다. Dependencies 파일의 수정 시간이 Target 파일의 생성 시간보다 나중이라면 Target의 Recipe 명령어를 실행하여 Target 파일을 빌드합니다. 반대로 Target 파일의 수정 시간이 Dependencies 파일의 생성 시간보다 앞이라면 Target 파일을 빌드하지 않고 넘어갑니다.


make에서 자주 사용하는 규칙들은 내장되어 있으므로 Makefile에 입력하지 않아도 자동으로 처리됩니다.

여기에는 소스 파일(.c)을 오브젝트 파일(.o)로 빌드하는 규칙이 포함되어 있으므로 다음과 같이 작성해도 됩니다.

app: main.o foo.o bar.o

gcc -o app main.o foo.o bar.o


그러나 이경우 헤더파일의 변경 여부는 확인하지 않으므로 Target에 대한 Dependencies를 입력하여 헤더파일의 변경 여부도 확인할 수 있도록 해야합니다. 그렇지 않으면 헤더파일이 변경되어도 이를 알지 못하여 빌드하지않고 그냥 넘어갑니다. Recipe는 입력하지 않아도 내장된 규칙에 의해 오브젝트 파일이 생성됩니다.

app: main.o foo.o bar.o

gcc -o app main.o foo.o bar.o


main.o: main.c foo.h bar.h

foo.o: foo.c foo.h

bar.o: bar.c bar.h


Makefile에서는 변수의 사용이 가능합니다. 등호(=)를 통해 정의해주며 ${변수}$(변수)로 사용합니다.

CCCFLAGS 같은 내장변수도 있습니다. CC는 컴파일러(gcc), CFLAGS는 컴파일 옵션 입니다.

https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html를 참고하세요.


패턴의 사용도 가능한데 $@, $<, $^, $* 같은 자동변수가 있습니다.

$@는 Target 이름을, $<는 Dependencies의 첫번째 파일 이름을, $^는 전체 Dependencies를, $*은 확장자를 제외한 Dependencies 이름 목록을 나타냅니다.


%도 존재하는데 와일드카드같은 역할을 합니다. 예를 들어 %.o: %.c 라고 입력한다면, %에는 main, foo, bar이 입력됩니다. %는 Recipe 부분에서는 사용할 수 없습니다.


변수와 패턴을 사용하여 작성하면 아래와 같습니다.

CC = gcc

CFLAGS = -g -Wall

TARGET = app


SRCS = $(wildcard *.c)

OBJS = $(SRCS:.c=.o)


$(TARGET): $(OBJS)

$(CC) -o $@ $^


%.o: %.c %.h


SRCS 변수에서 wildcard 함수는 와일드카드를 나타냅니다. *.c 이므로 확장자가 .c인 소스 파일 목록을 나타냅니다. $(SRCS:.c=.o)는 SRCS에서 확장자 .c를 .o로 바꾼 오브젝트 파일 목록을 의미합니다. 즉, OBJS 변수에는 오브젝트 파일 목록이 들어갑니다.


마지막 부분에 %.o: %.c %.h의 경우 main.o 파일의 의존성 패턴이 일치하지 않습니다.

패턴대로라면 main.o: main.c main.h 라고 작성되는데 main.o 파일의 의존성 파일은 헤더가 foo.h, bar.h입니다. 해결방법은 있습니다. 다음 명령어를 입력해보세요.

 $ gcc -c main.c -MD


-MD 옵션을 주고 컴파일하면 오브젝트 파일(main.o)과 확장자가 .d인 파일(main.d)이 생성됩니다.

main.d 파일의 내용을 봅시다.

 $ cat main.d

main.o: main.c /usr/include/stdc-predef.h foo.h bar.h


오브젝트 파일 main.o의 의존성 파일이 입력되어 있습니다. 이 파일을 Makefile에 include하면 자연스럽게 헤더파일의 유효성도 검사할 수 있습니다. 다음과 같이 작성합니다.

CC = gcc

CFLAGS = -g -Wall

TARGET = app


SRCS = $(wildcard *.c)

OBJS = $(SRCS:.c=.o)

DEPS = $(OBJS:.o=.d)


$(TARGET): $(OBJS)

$(CC) -o $@ $^


%.o: %.c

$(CC) -o $@ $(CFLAGS) -c $< -MD


-include DEPS


처음 오브젝트 파일이 빌드될 때 의존성 기록 파일(*.d)이 생성되고, 그다음부터는 의존성 기록 파일에 작성된 의존성 파일의 유효성을 검사하게 됩니다. 마지막에 include 함수에 의해 *.d 파일의 내용이 Makefile에 포함됩니다. Target이 중복되면 make에 의해 자동으로 Dependencies가 합쳐지게 됩니다. 


이번에는 소스 파일과 오브젝트 파일, 헤더 파일을 분리하여 빌드해보겠습니다.

c_ws

└ src

    └ main.c

    └ foo.c

    └ bar.c

└ include

    └ foo.h

    └ bar.h

└ obj

└ Makefile


소스파일과 헤더파일을 각각 src, include 폴더로 이동합니다.

 $ mkdir src include obj && mv *.c src && mv *.h include


Makefile을 다음과 같이 수정합니다.

 $ xed Makefile

CC = gcc

CFLAGS = -g -Wall

TARGET = app


INCLUDE = -Iinclude


SRC_DIR = ./src

OBJ_DIR = ./obj


SRCS = $(notdir $(wildcard $(SRC_DIR)/*.c))

OBJS = $(patsubst %.o,$(OBJ_DIR)/%.o,$(SRCS:.c=.o))

DEPS = $(OBJS:.o=.d)


all: $(TARGET)


$(TARGET): $(OBJS)

$(CC) -o $@ $^


$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c

$(CC) -o $@ $(INCLUDE) $(CFLAGS) -c $< -MD


clean:

$(RM) $(OBJS) $(DEPS) $(TARGET)


.PHONY: clean all


-include DEPS


SRCS 변수에는 소스 파일 목록을 입력합니다. wildcard 함수를 통해 src 폴더 아래에 있는 소스파일 목록을 가져오고, notdir 함수를 통해 경로를 제외한 파일이름 목록만 가져옵니다. (main.c foo.c bar.c) 


OBJS 변수에는 오브젝트 파일 목록을 입력합니다. patsubst 함수는 변수에서 특정 패턴을 다른 문자열로 치환하는 함수입니다. $(patsubst 패턴,치환된 문자열,변수) 형식으로 입력합니다. SRCS에서 확장자를 .o로 바꾼 문자열(main.o foo.o bar.o)에서 *.oobj/*.o로 바꾼 문자열(obj/main.o obj/foo.o obj/bar.o)로 치환합니다. 


나머지는 동일한데 오브젝트 파일을 생성할때 -I 옵션을 통해 헤더파일 경로를 지정합니다.-I[헤더파일 경로] 형식으로 경로를 지정합니다.


 그 외 Target으로 all, clean, .PHONY를 추가했는데 all은 Target 없이 make만 입력했을때 빌드될 의존성 파일을, clean은 빌드관련파일 제거 명령을 입력합니다. .PHONY는 해당 파일이 존재하더라도 명령어를 수행하는 역할을 합니다. 만약 .PHONY가 입력되지 않는다면 clean이라는 파일이 존재할때 무조건 유효성검사를 통과하여 명령어가 실행되지 않습니다.



5. CMake와 CMakeLists.txt


CMake는 빌드파일을 생성해주는 프로그램입니다. 예를 들어 CMake를 통해 make의 빌드 파일인 Makefile을 만들 수 있습니다. CMakeLists.txt 파일은 ROS에서 C++ 패키지를 만들때 작성한 적이 있습니다. makefile을 직접 작성하면 Target에 대한 의존성 정보를 모두 작성해야합니다. 반면 CMake를 통해 makefile을 만든다면 소스파일만 입력하면 됩니다.  


간단하게 CMake를 통해 make 파일을 만들고 빌드해보도록 하겠습니다.

cmake_ws

└ build

└ CMakeLists.txt

└ main.c 

 $ mkdir -p ~/cpp_ws/build && cd ~/cpp_ws && touch CMakeLists.txt main.c


소스 파일(main.c)를 작성합니다.

 $ xed main.c

#include <stdio.h>


int main(void) {

printf("main!!\n");


return 0;

}


CMakeLists.txt 파일을 작성합니다. 

 $ xed CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(example)

add_executable(${PROJECT_NAME} main.c)


cmake_minimum_required 명령은 사용가능한 cmake의 최소버전을 입력합니다. cmake는 버전마다 사용할 수 있는 기능이 다르므로 적절하게 기입해줍시다. 


project 명령은 프로젝트 정보를 입력합니다. 최소 프로젝트 이름만 입력하셔도 됩니다.

project(example 

VERSION 0.1

DESCRIPTION "예제 프로젝트" 

LANGUAGES C)


프로젝트 이름은 ${PROJECT_NAME} 변수를 통해 접근가능합니다.


add_executable 명령은 실행파일을 빌드합니다. 실행파일 이름과 소스파일 이름을 입력합니다. 

add_executable([실행파일] [소스파일 1] [소스파일 2] ... [소스파일 n])


빌드파일을 생성하겠습니다. 빌드파일은 프로젝트 루트폴더에 생성하는 것 보다 따로 build 폴더를 만들기를 권장합니다. 빌드파일을 생성하는 명령어는 다음과 같습니다.

 $ cmake [CMakeLists.txt 파일이 존재하는 루트폴더]


build 폴더로 이동하면 CMakeLists.txt 파일은 부모 폴더에 존재하므로 ..을 입력하면 됩니다.

 $ cd build && cmake ..


ls를 통해 파일을 확인하면 Makefile이 만들어진것을 확인할 수 있습니다. cmake로 빌드파일을 한번 만들어 놓으면 CMakeLists.txt의 내용이 변경되어도 다시 빌드파일을 만들 필요가 없습니다. make를 통해 빌드파일을 빌드하기만 하면 자동으로 변경된 내용으로 빌드파일을 만들어 실행시킵니다.


make를 입력하여 빌드를 하면 실행파일 example이 생성됩니다. 

 $ make

 $ ./example


이번에는 위에 make 예제파일에서 만든 소스파일을 이용하여 cmake를 통해 빌드파일을 만들어보겠습니다.

 $ cp ~/c_ws/{include,src}/* ..


CMakeLists.txt 파일을 수정합니다.

 $ xed ../CMakeLists.txt

set(SRCS main.c foo.c bar.c)

cmake_minimum_required(VERSION 3.5)

project(example)

add_executable(${PROJECT_NAME} ${SRCS})


set 명령을 통해서 변수를 설정할 수 있습니다.

set([변수명] [값1] [값2] ... [값n])


make를 통해 빌드하여 실행파일을 실행해보세요.

 $ make

 $ ./example


끝입니다.. 위에 Makefile을 작성한 것과 비교해보세요.

app: main.o foo.o bar.o

gcc -o app main.o foo.o bar.o


main.o: main.c foo.h bar.h

gcc -c main.c


foo.o: foo.c foo.h

gcc -c foo.c


bar.o: bar.c bar.h

gcc -c bar.c


물론 위의 코드는 Makefile도 간단한 형태로 줄일 수 있지만 프로젝트가 복잡해질수록 Makefile을 작성하기에는 여간 번거로운 일이 아닐겁니다.


이번엔 헤더파일을 include 폴더에 따로 분리해보겠습니다.

 $ mkdir ../include && mv ../*.h ../include


CMakeLists.txt 파일을 수정합니다.

 $ xed ../CMakeLists.txt

set(SRCS main.c foo.c bar.c)

cmake_minimum_required(VERSION 3.5)

project(example)

include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_SOURCE_DIR}/include)

add_executable(${PROJECT_NAME} ${SRCS})


include_directories 명령은 헤더파일의 경로를 설정합니다. 따로 경로를 설정하지 않으면 기본값은 소스파일이 존재하는 폴더입니다. CMakeLists.txt 파일이 존재하는 프로젝트 루트 폴더는 변수 ${CMAKE_SOURCE_DIR}을 통해 접근가능합니다. 

include_directories([실행파일 이름] PUBLIC [경로1] [경로2] ... [경로n])


PUBLIC이라고 입력한 부분은 접근자인데 실행파일을 참조하는 다른 파일이 헤더파일의 경로도 참조하는지를 나타냅니다. PUBLIC은 헤더파일 경로를 참조하고, PRIVATE는 헤더파일 경로를 참조하지 않습니다. 실행파일을 참조할 수 없어서 여기서는 어느것을 입력해도 중요하지 않습니다.

#c# c++# make# cmake# Makefile# CMakeLists.txt
댓글
자동등록방지
(자동등록방지 숫자를 입력해 주세요)