개발/CMake

[CMake] Ch2. 빌드 시스템에 대한 이해

growing-dev 2023. 9. 15. 00:09

빌드와 빌드 시스템이란?

Build라는 단어의 사전적 뜻이 무엇일까요? 별로 어렵지 않은 단어이죠. 만들다, 건물을 짓다, 창조하다 라는 뜻으로 자주 사용되는 단어입니다. 

네이버 사전 Build

 

우리가 코딩을 하는 이유는 코딩을 통해 작성한 소스 코드를 가지고 원하는 동작을 하는 프로그램을 만드는 것입니다. 빌드를 한다는 것은 바로 이 과정을 수행하는 것입니다. 즉 소스코드를 프로그램으로 바꾸어 주는 행위가 바로 빌드를 하는 것이고 이렇게 할 수 있도록 도와주는 도구들을 빌드 시스템이라고 말합니다.

 

빌드 시스템은 각종 프로그램, 컴파일러, 링커, 스크립트 등을 포함하는 말입니다. C/C++ 코드를 빌드하기 위해서는 특정 프로그램만을 써야 하는 것은 아니고 선택적으로 사용할 수 있습니다. 

이러한 빌드는 파일을 기계어로 변환하는 컴파일과 변환된 파일들을 연결하여 최종 프로그램으로 만드는 링크를 포함합니다. 종종 "빌드한다" 와 "컴파일한다" 가 그냥 동일하게 활용되기도 하지만 빌드가 컴파일과 링크를 포함하는 더 넓은 의미가 맞습니다.

이제 컴파일과 링크에 대해서 조금 더 알아보도록 하겠습니다.

 

컴파일의 정의

컴파일은 사람이 작성한 소스 코드를 컴퓨터가 이해할 수 있는 언어(기계어)로 변환하는 작업입니다. 작성된 소스 코드는 언어에 따라 다른 형태로 작성되며 실제로 컴퓨터에서 실행될 수 있는 프로그램으로 바꾸기 위해서는 컴퓨터와 약속되어 실행할 수 있는 기계어로 변환이 필요합니다. 그 과정을 수행하는 것이 컴파일하는 과정이며 컴파일 과정은 구체적으로 3가지 단계(전처리, 컴파일, 어셈블리)로 나눌 수 있습니다. 

 

이를 수행할 수 있는 프로그램이 컴파일러(GCC, Clang 등)입니다.

아래는 gcc의 C++ 빌드 명령어인 g++ 을 통해 main.cpp 를 main.exe 의 프로그램으로 만드는 명령어입니다.

g++ main.cpp -o main.exe
#include <iostream>

int main(int, char**){
    std::cout << "Hello, from growingDev!\n";
}

실행 결과

 

전처리 과정

전처리기(Preprocessor)를 통해 소스 코드 파일(*. c)을 전처리된 소스 코드 파일(*. i)로 변환하는 과정입니다.

우선 소스 코드 내에 #include "xx.h"로 표현된 헤더파일들을 소스 코드 내에 복사합니다. 헤더파일을 include 한다는 것은 단순히 복사한다는 것입니다. 모든 파일 안에서 매번 활용되는 것들을 매번 복사해서 작성하면 반복적이고 불편하니깐 따로 파일로 만들고 그 파일을 포함한다 라고 #include 라고 지정해주면 컴파일 시점에 전처리기가 알아서 복사해주는 것이지요. 별 것 아닌 것 같지만 막상 코드가 점점 복잡해지면 헤더파일의 중요성을 많이 느끼게 되는 것 같습니다.

또 하나 중요한 것이 바로 #define #ifdef과 같은 매크로를 적용하는 것입니다. #define과 같은 경우는 실제 소스코드에서 치환이 일어나게 되고 #ifdef과 같은 경우는 조건에 따라 코드를 포함하거나 지우거나 하는 작업이 진행됩니다.

#include <stdio.h>

#define MAX_VALUE 10

#ifdef x86
int get_max_value_at_x86() {
	return MAX_VALUE;
}
#endif

 

컴파일 과정

컴파일러를 통해 전처리된 소스 코드 파일(*. i)을 어셈블리어 파일(*. s)로 변환하는 과정입니다.

컴파일 과정에서 비로소 각 소스 코드 파일에서 코드의 문법을 검사하여 error나 warning을 출력하기도 하고 각 파일별로 저수준의 언어인 어셈블리어 형태로 변환합니다.

 

어셈블리 과정

어셈블러(Assembler)를 통해 어셈블리어 파일(*. s)을 오브젝트 파일(*. o)로 변환하는 과정입니다.

오브젝트 파일 내에는 기계어로 변환된 소스코드를 포함하여 해당 파일 내의 함수 이름이나 변수명과 같은 심볼들도 저장이 됩니다.  각 파일별로 오브젝트 파일이 생성이 되고 이 오브젝트 파일들을 모아서 심볼 정보를 바탕으로 연결(링크)시켜주면 하나의 프로그램이 완성이 되는 것입니다.

 

 

링크 과정

링크 과정은 링커(Linker)를 통해 오브젝트 파일(*. o)들을 묶어 실행 파일로 만드는 과정입니다.

만들어진 오브젝트 파일들과 필요한 라이브러리들을 포함하여 실행 파일을 만듭니다.

실행파일을 만들 때 라이브러리를 참조하는 방식에 따라 정적 라이브러리 방식과 동적 라이브러리 방식이 있는데 자세한 내용은 나중에 다뤄보도록 하겠습니다.

 

 

Make의 탄생

Make는 굉장히 많이 활용되는 빌드 시스템입니다. 1977년에 만들어졌고 윈도우, 리눅스에 모두 사용이 가능합니다. Make는 Makefile이라는 간단한 텍스트 파일을 사용하여 빌드할 수 있도록 작성합니다.

간단한 프로그램의 경우 Make로 작성하면 매우 쉽게 잘 사용할 수 있습니다. 많은 프로그램들이 여전히 Make 기반에서 빌드를 하고 있습니다. 하지만 프로젝트가 커질 수록 Make 자체가 점점 더 복잡해 집니다. 

아래는 간단한 Makefile 의 예시입니다.

아래와 같이 Makefile 을 작성한 뒤에 동일한 경로에서 make 명령어로 실행하면 아래 스크립트 대로 수행하는 것입니다.

# 컴파일러 설정
CC = g++
CFLAGS = -Wall

# 타겟 정의
my_program: main.exe
    $(CC) $(CFLAGS) -o my_program main.o

main.o: main.cpp
    $(CC) $(CFLAGS) -c main.cpp

실행 결과

앞서 보았던 명령어(g++ main.cpp -o main.exe)와 비슷한 기능을 하는 것 같은데 조금 복잡해 보이나요? 만약 파일이 늘어나고 CFLAGS 와 같이 입력해주어야 하는 것들이 더 늘어나면 매번 빌드할 때 마다 순서에 맞추어서 명령어를 입력해 주어야 하는 것이 여간 귀찮은 것이 아닙니다.

 

만약 프로그램이 점점 더 복잡해진다면?

Make는 앞서 설명했다시피 오래되었고 안정된 도구입니다. 하지만 프로그램이 점점 더 복잡해지고 여러 과제나 플랫폼을 동시에 지원해야 되는 상황들이 발생하고, 라이브러리 의존성까지 많아지면 Makefile을 만드는 것이 점점 더 어려워집니다. 복잡한 프로그램을 만들어야 하는 것도 어려운데, 빌드하기 위한 스크립트 파일까지 복잡해진다면 문제는 더 많이 발생할 것입니다.

이 상황을 개선해 보고자 나온 것이 바로 CMake입니다. CMake가 만능이라고는 할 수 없지만 점점 C/C++ 프로젝트의 빌드 시스템의 한 축을 담담해 나가고 있습니다. 다음 Chapter부터는 CMake에 대해서 소개하고 환경설정부터 해보면서 본격적으로 이해해 보도록 하겠습니다.

 

반응형