Search

make와 Makefile의 필요성

Created
2020/06/10
tag
C
씹어먹는 C 언어
make
Makefile

1. 들어가기 전에

저학년 시절에 C 언어C++을 사용하면서 컴파일이 필요할 때 대체로 MicrosoftVisual Studio를 이용했다. 2학년을 마칠 때까지 Visual Studio를 이용하지 않고 컴파일 했던 경험은 리눅스에서 C 언어를 컴파일 하기 위해 gcc(GNU Compiler Collection)를 이용했던 것이 전부였다.
이 때 gcc를 이용하면서 검색했을 때 makeMakefile을 처음 들어봤다.
3학년에 접어들고 운영체제 과제를 하면서 리눅스의 Kernel을 건드릴 일이 생기자 이전에 들어봤던 makeMakefile을 이용하게 되었고, 이것이 내 인생 처음으로 makeMakefile을 이용한 것이었다. 당시에는 과제로 System Call을 구현하기 위해서 이용했던 것인데, 왜 makeMakefile이 필요한지도 모른채로 그저 과제를 수행하는데 있어서 필요하니까 하라는대로 했다.
이 과제 이후에도 종종 makeMakefile이 귀를 스쳐 지나갈 때가 있었는데 별 신경 안 쓰고, Kernel에 대해서 수정한 것을 반영하기 위해 하는 것 정도로만 알고 넘겼다.
정말 바보 같지 않은가? 지금 생각해도 너무 무관심 했던 것 같다.
1년이 지나고 나서야 C 언어를 리뷰하면서 모두의 코드에 기재되어 있는 블로그를 통해 왜 makeMakefile이 필요하고 어떻게 쓰는 것인지 정확히 알 수 있었다.
위에서부터 쭉 보면 알다 싶이 C 언어C++을 컴파일 하는 방법은 여럿 있다. 대표적으로 컴파일에 사용되는 것은 아래와 같다.
Visual Studio
clang
gcc
이 중에서 gcc를 이용하여 컴파일 하는 과정을 살펴보고, makeMakefile이 어떻게 사용되는지 그리고 왜 필요한지 알아보겠다.
우선 make라고 하는 것은 요리라고 한다면 Makefile요리법이라고 생각하면 된다. 즉, Makefile에 기술되어 있는 대로 make를 통해 컴파일하게 된다고 보면 된다. 그렇다면 gcc 명령어를 이용하여 컴파일 하는 것과 make를 이용하여 컴파일 하는 것이 어떻게 다르기에 make를 이용하기도 하는지 의문이 들 것이다. 예제를 통해 확인을 하기 이전에 make를 사용 했을 때 어떤 점이 좋은지 간단히 밝히자면 아래와 같다.
반복되는 명령어를 작성하지 않아도 된다.
스크립트 이름들을 길게 나열한 명령어보다 스크립트들이 기술 되어 있는 파일을 이용하여 컴파일 함으로써, 컴파일 하려는 대상의 프로그램 구조를 쉽게 파악할 수 있고 이에 대한 관리가 용이하다.
이제 예제를 확인해보자.

2. gcc 명령어를 이용한 실행 파일 생성

다음과 같은 형태로 test1test2가 있고 각 파일에 있는 함수는 call 파일에 선언되어 있다. call 파일에 선언된 함수는 main에서 호출한다고 하자. 여기서 헤더 파일을 제외한 3개의 파일을 하나로 묶어 test라는 바이너리 파일 (실행 파일)을 만들고자 할 때, gcc 명령어를 이용하면 아래와 같이 컴파일 하여 각 파일에 대해서 목적 파일을 만들어야 한다.
1개의 파일만 바이너리 파일로 만들 때는 묶을 파일이 없기 때문에, 목적 파일을 생성하여 묶을 필요 없이 바로 바이너리 파일로 생성하면 된다. 이 경우는 3개의 파일을 이용하여 바이너리 파일로 만들어야 하므로 목적 파일을 묶어야 한다.
목적 파일을 생성하는 옵션은 -c 이고, 별도의 파일 이름을 지정하는 옵션은 -o이다.
위와 같이 3번의 명령어를 통해서 각 목적 파일을 만들었다. 아직 바이너리 파일이 생성된 것은 아니다. 바이너리 파일을 만들기 위해서는 생성되어 있는 목적 파일들을 묶어서 실행 파일을 생성해야 한다.
목적 파일 3개를 묶어 test라는 바이너리 파일이 생성된 것을 볼 수 있다.
위와 같이 생성된 바이너리 파일을 생성하게 되면 main에서는 test1test2의 함수를 정상적으로 호출하는 것을 볼 수 있다. 여기까지 진행한 것을 보면, 하나의 바이너리 파일을 만들기 위해서는 파일의 개수 + 1 (여기서 1바이너리 파일을 만들기 위해 목적 파일을 묶어내는 것이다.) 의 명령어를 입력해야 한다.
만일 리눅스의 Kernel과 같이 파일의 개수가 매우 많은 경우에는 gcc 명령어를 통해서 일일이 목적 파일을 생성하고, 이를 묶어 바이너리 파일을 만들어야 할까? 그렇지 않다. 이렇게 반복되는 명령어 수행을 방지하고자 makeMakefile을 이용하게 된다.

3. Makefile 작성 및 make 명령어를 이용한 실행 파일 생성

처음에 든 비교를 다시 확인 해보면 make요리라면 Makefile요리법이다. 물론 현실에서는 요리법 없이도 요리를 해도 되겠지만 여기서 make요리를 하기 위해선 어떻게 요리를 해야하는지를 알아야 요리가 가능하다. 따라서 Makefile이 필요하고 이에 대해서 먼저 작성해야 한다.
Makefile을 작성할 때는 다음과 같은 형식을 이룬다.
${매크로 정의} ${타겟 파일} : ${의존 파일} ${수행할 명령어}
처음 예제를 make로 컴파일 하기 위해 형식을 만족시키면서 Makefile로 구성해보면 위 사진과 같다.
Makefile에 정의된 clean이라는 것은 make clean이라는 명령어를 이용하면 모든 목적 파일test라는 바이너리 파일을 삭제하는 역할을 한다. 이렇게 Makefile에 대한 정의가 끝났다면 make 명령어를 통해 Makefile을 실행한다.
실행 결과는 gcc 명령어를 이용한 결과와 마찬가지로 test라는 바이너리 파일이 생성된 것을 확인할 수 있다.

4. 매크로 기능을 이용한 Makefile 개선

더 개선할 사항이 있다. 작성했던 MakefileMakefile의 형식을 비교해보면 매크로 정의를 사용하지 않은 것을 알 수 있고, 작성했던 Makefile에서 생각보다 꽤 많은 부분이 중복되어 사용된 것을 볼 수 있다. 매크로 정의를 이용하면 중복된 부분에 대해서 해결할 수 있다. 매크로는 다음과 같은 규칙을 준수하며 작성한다.
매크로의 정의는 사용되는 곳보다 항상 이전에 정의되어야 한다.
정의된 매크로를 사용할 때는 $()에서괄호 안에 매크로 이름을 넣어 사용한다.
-W -WALL는 컴파일 시에 발생하는 오류를 모두 출력시키는 옵션이다.
제시된 규칙을 만족 시키며 이전 Makefile매크로를 이용하여 다시 나타내면 위 그림과 같다.
여전히 test 바이너리 파일이 잘 생성된 것을 확인할 수 있다. 매크로를 사용했음에도 개선할 점이 한 가지 더 남았다. 매크로를 사용하여 작성한 Makefile을 확인해보면, 생성시킬 타겟 파일이 늘어날 때마다 일일이 이를 기록해줘야 한다는 것이다. 이런 현상은 내부 매크로를 이용하면 조금 더 간결하게 표현 할 수 있다
내부 매크로를 사용한 Makefile은 위 사진과 같다. 훨씬 간결해진 표현인 것을 볼 수 있다. 여기서 $@ $^의 의미는 각각 현재 타겟의 이름현재 타겟의 종속 항목 리스트를 나타낸다.
참고로 여기서 작성된 all에 대해서는 이번 예제에서는 사용되지 않았다. 타겟 파일이 1개이므로 사용되지 않았지만 타겟 파일이 여러개라면 all은 사용될 수 있다.
이전에 비해서 훨신 간결한 코드가 된 이유는 $(TARGET) : $(OBJECTS) 구문에서 타겟 파일을 위해서 의존 파일을 이용하게 되는데, 이 때 목적 파일이 존재하지 않으면 의존 파일의 이름과 동일한 목적 파일을 만들기 때문이다.
내부 매크로를 이용하여 작성한 Makefile 역시 test 바이너리 파일을 내놓는 동일한 결과를 보여준다.
예를 통해서 살펴 본 것과 같이 소스 코드의 파일 수가 많아질수록 Makefile을 이용할 때 관리하기 편해진다. 또한 여러 번 컴파일을 시킬 때 긴 명령어를 반복하여 사용할 필요 없이 미리 기술한 Makefile을 이용함으로써 make 명령어만으로 컴파일이 가능하게 된다.
리눅스 Kernel에는 매우 많은 소스 코드 파일이 존재하기 때문에 makeMakefile을 이용하지 않고서는 컴파일이 어렵다. 드디어 리눅스 Kernel 과제를 하고서 1년이 지나고야 makeMakefile을 사용했던 이유를 알 수 있었다.

5. Reference