Ian's Archive 🏃🏻

thumbnail
CMake, Makefile에 대해 알아보자
C
2024.03.17.

흠… c로 작성된 코드를 빌드하는 과정에 대해 알아보기 위해 정리한다

C언어 빌드 과정 (Build Process)

GCC란?

GCC(GNU Compiler Collection)는 GNU 프로젝트의 일환으로 개발되어 널리 쓰이고 있는 컴파일러

여기서 말하는 GNU는 GNU’s not UNIX의 재귀약자로, 리처드 스톨먼이 각종 자유 소프트웨어들이 돌아가고 번영할 수 있는 기반 생태계를 구축하기 위해 시작한 프로젝트

GCC는 C만을 지원한 컴파일러였지만 후에 다른 언어들도 컴파일 가능해졌다.

컴파일러란

어떤 프로그래밍 언어로 쓰여진 소스 파일을 다른 언어로 바꾸어주는 번역기

컴파일(Compile) : 어떤 언어의 코드를 다른 언어로 바꿔주는 과정
ex) 사람이 작성한 c언어 코드 -> 기계어

먼저 c로 간단한 프로그램 작성 후 gcc로 빌드 해보자

copyButtonText
#include <stdio.h>

int main() {
    printf("hello world!");
    return 0;
}
copyButtonText
gcc hello.c -o hello

hello 실행 파일이 생기고 명령어 실행 시 해당 파일이 실행된다

copyButtonText
./hello
# hello world!

자세한 과정에 대해 알아보자

cbuild

소스 코드를 실행 파일로 만들기 위해선 4단계를 거친다.

  1. 전처리 단계
  2. 컴파일 단계
  3. 어셈블 단계
  4. 링크 단계

gcc hello.c 명령어를 입력하면 실행 파일이 생성되지만, 각 단계의 파일들은 임시 파일로 생성되었다가 사라진다.

궁금하면 아래 명령어 실행 시 모든 중간파일 확인 가능하다

copyButtonText
$ gcc -Wall -save-temps {파일 이름}.c –o {파일 이름}

-W, -Wall은 컴파일 시 컴파일 되지 않을 정도의 오류라도 모두 출력하는 옵션

1. 전처리 단계

전처리 된 파일은 {파일 이름}.i에 저장된다.

#으로 시작하고, 세미콜론 없이 개행문자로 종료되는 라인을 의미한다.

전처리 지시자 기능
#include 프로그램 외부 파일을 불러옴
#define 매크로 상수/함수 정의
#undef 정의한 매크로 취소
#if ~ (#elif ~ #else ~) #endif 조건부 컴파일

전처리 단계에선

  1. 주석 제거

    • 주석을 제거해 실제 코드만을 대상으로 컴파일
  2. 매크로 확장

    • 소스 코드에 정의된 매크로는 해당 매크로가 사용된 곳에 매크로의 내용을 대체하는 과정을 거침
    • 매크로는 #define지시문을 통해 정의되며, 컴파일러는 소스 코드를 처리할 때 해당 매크로를 그에 대응하는 내용으로 치환
  3. 포함된 파일을 확장

    • 소스 파일에서 다른 헤더 파일을 #include 지시문을 사용해 포함
  4. 조건부 컴파일

    • #ifdef같은 전처리기 지시문을 사용해 소스 코드의 일부를 컴파일하는지 여부 결정

4단계를 거쳐 hello.i파일이 생성된다.

2. 컴파일 단계

전처리 된 hello.i 파일을 통해 어셈블리어로 된 hello.s파일을 생성한다.

3. 어셈블 단계

어셈블리 파일 hello.s를 기계어로 된 오브젝트 파일 hello.o파일로 변환한다.

즉, 0과1로 이루어진 2진수 코드로 변환 (바이너리 코드)

4. 링크 단계

작성한 프로그램이 사용하는 다른 프로그램이나 라이브러리를 가져와서 연결
-> 실행 가능한 파일 생성

더 자세한 과정이 궁금하면 아래 포스팅을 참고하자

C Build Process in details - Abdelaziz Moustafa

Make란?

파일 관리 유틸리티

  • 반복적인 명령 자동화를 위한 것
  • 파일간의 종속관계를 파악해 MakeFile(기술파일)에 적힌대로 컴파일러에 명령해 SHELL 명령이 순차적으로 실행된다.

즉, 반복적인 명령의 자동화로 인한 단숙 반복 작업 및 재작성 최소화가 되고,

프로그램 종속 구조를 빠르게 파악할 수 있어 관리가 용이하다.
(또한 gcc -o main.c main.c같은 실수를 피할 수 있다… ㅋㅋ)

Makefile이란?

Makefile은 프로그램을 빌드하기 위해 make문법에 맞춰 작성하는 문서이다.

Make파일의 구조는 다음과 같은 구조를 가진다.

  • 목적파일(Target) : 명령어가 수행되어 나온 결과를 저장
  • 의존성(Dependency) : 목적파일을 만들기 위해 필요한 재료
  • 명령어(Command) : 실행 되어야 할 명령어들
  • 매크로(Macro) : 코드를 단순화 시키기 위한 방법

기본적으로 목표(target), 의존 관계(dependency), 명령(command)세개로 이루어진기본 규칙들이 계속 나열

copyButtonText
CC = gcc # 매크로 정의

target1 : dependency1 dependency2 # 타겟절 : 의존성
        command1                  # 명령어
        command2                  # 명령어

target2 : dependency3 dependency4 # 타겟절 : 의존성
        command3                  # 명령어
        command4                  # 명령어

Makefile 규칙

작성할 때 규칙은 다음과 같다

  1. 명령의 시작은 반드시 TAB으로 시작
  • make 규칙으로 명령은 TAB으로 시작
  • TAB으로 시작하지 않은 명령 절을 만나면 make는 명령 절이 아니라 타겟절로 해석
  1. 비어있는 행은 무시

  2. #을 만나면 개행 문자를 만날 때 까지 무시한다

  3. 기술 행이 길어지면 \을 사용해 이을 수 있다.

copyButtonText
target1 : dependency1 dependency2 \
          dependency3 dependency4
  1. ;은 명령라인을 나눌 때 사용 가능하다

  2. 종속 항목이 없는 타겟도 사용 가능하다

copyButtonText
clean:
  rm -rf *.o target1 target2
  • 종속 항목이 없기 때문에 명령 절은 바로 수행
  1. 레이블 사용 (명령 부분에는 어떤 명령이 와도 상관없다.)
target1 : dependency1 dependency2
  cp -f file1 file2
  gcc -o target1 dependency1 dependency2
  gdb target1
  • 어떤 명령이라도 알맞게 사용 가능
  1. 매크로 사용

매크로 사용은 ${...}, $(...), $... 모두 사용 가능

copyButtonText
OBJECTS = main.o read.o write.o

test : $(OBJECTS)
    gcc -o test $(OBJECTS)

main.o : io.h main.c
    gcc -c main.c
read.o : io.h read.c
    gcc -c read.c
write.o: io.h write.c
    gcc -c write.c
  1. 내장 규칙(Built-in Rule)

Make는 자주 사용되는 빌드 규칙들은 내장을 해서 자동으로 처리
(ex - 소스 파일(.c)을 컴파일해서 Object 파일(.o)로 만들어 주는 규칙)

  1. Makefile은 아래에서부터 위로 실행

  2. 환경 변수

변수 설명
CC 컴파일러 지정
INC include 되는 헤더 파일의 패스를 추가
LIBS 링크할 때 필요한 라이브러리를 추가
CFLAGS 컴파일에 필요한 각종 옵션을 추가
OBJS 목적 파일 이름
SRCS 소스 파일 이름
TARGET 링크 후에 생성될 실행 파일의 이름
clean makefile실행 후 지울 파일 목록

기본적인 makefile 규칙들이고 더 자세한 내용은 아래 글 참고

3. 매크로(Magro)와 확장자 규칙
4. Makefile를 작성할 때 알면 좋은 것들
5. make 중요 옵션 정리


간단한 예시

copyButtonText
.SUFFIXES : .c .o     --+
CFLAGS = -g             |
                        |
OBJS = main.o \         |
read.o \                | 매크로 정의 부분
write.o                 |
SRCS = $(OBJS:.o=.c)    |
                        |
TARGET = test         --+

$(TARGET): $(OBJS)                    --+
                $(CC) -o $@ $(OBJS)             |
dep :                                   |
                gccmakedpend $(SRCS)            |
new :                                   | 명령어 정의 부분
                touch $(SRCS) ; $(MAKE)         |
clean :                                 |
                $(RM) $(OBJS) $(TARGET) core  --+
copyButtonText
OBJS=main.o foo.o hello.o
TARGET=app.out

all: $(TARGET)

clean:
        rm -f *.o
        rm -f $(TARGET)

$(TARGET): $(OBJS)
        $(CC) -o $@ $(OBJS)

main.o: foo.h hello.h main.c
foo.o: foo.h foo.c
hello.o: hello.h hello.c

2번째 예시에서

make명령어 입력 시 아래의 결과를 확인 할 수 있다.

copyButtonText
test git:(main) ✗ make clean
rm -f *.o
rm -f app.out
test git:(main) ✗ make
cc    -c -o main.o main.c
cc    -c -o foo.o foo.c
cc    -c -o hello.o hello.c
cc -o app.out main.o foo.o hello.o
test git:(main) ✗ ls
Makefile app.out  foo.c    foo.h    foo.o    hello.c  hello.h  hello.o  main.c   main.h   main.o
test git:(main) ✗ ./app.out
main, world!
test git:(main) ✗

MakeFile을 생성해 make명령을 관리하면

  • 입력 파일 변경 시 결과 파일 자동 변경을 원할 때 배치 작업 관리
  • gcc명령 없이 간편한 컴파일 가능

더욱 자세한건 GNU Make 강좌, GNU make documnet 참고
(실제 예제, make 에러)

CMake란?

빌드 파일을 생성해주는 프로그램

프로젝트의 규모가 커질수록 Makefile로 유지/보수하는 작업이 힘들어진다.

  • 프로그램의 버전을 명시하는 이유로 빌드 시 전에 사용하는 헤더 파일들을 자동 생성하는 경우
  • 실행 파일 외에 공유 라이브러리들을 함께 생성 해 빌드 대상물이 여러개인 경우
  • 프로젝트에 포함 된 서브모듈(써드파티 프로그램)들이 다단계로 존재하는 경우
  • 빌드 전 프로그램에 사용되는 리소스 파일들을 묶어서 가상의 파일 시스템을 만들어야 하는 경우
  • 빌드 완료 된 실행 파일로 부터 임베디드 프로세서에 퓨징하기 위한 바이너리를 생성해야 하는 경우

CMakeMakefile보다 추상화된 기술 문법으로 Build step을 기술하면, 이로부터 Makefile을 자동으로 생성해준다.
-> CMake를 사용하면 소스코드 - 결과물 사이를 깔끔하게 추상화 해준다.

Build Step만 잘 구성해 놓으면, 이후에는 소스 파일(*.c)을 처음 추가할 때만 CMakeLists.txt 파일을 열어서 등록
-> 빌드에서 제외하지 않는 한 스크립트 수정이 필요 없다.

CMakeMake처럼 의존성 검사 후 Incremental Build를 수행하지만,

소스 파일 내부까지 들여다보고 분석해서 의존성 정보를 스스로 파악

헤더파일을 추가하면, 의존성 관계를 자동으로 추적해서 해더 파일까지 추적

즉, CMakeMakefile을 보다 쉽게 기술해주는 일종의 Meta-Makefile

CMake로 프로젝트를 관리하더라도 최종 빌드는 Make와 마찬가지로 make명령으로 수행한다.


간단한 예제

mac환경에서 brew를 사용해 install한다

copyButtonText
brew install cmake

그 후엔 CMake빌드 스크립트인 CMakeLists.txt파일 작성

copyButtonText
ADD_EXECUTABLE( app.out main.c foo.c hello.c)
copyButtonText
cmake CMakeLists.txt

위 명령어를 실행하면

  • 개별 Object파일들 생성 ex) make main.c.o
  • make all, make clean과 같은 매크로도 모두 정의

cmake CMakeLists.txt 명령은 자동 생성된 Makefile을 삭제하지 않는 한 최초 한 번만 실행하면

Makefile을 실행할 때 CMakeLists.txt파일의 변경 여부를 검사해서 필요한 경우 Makefile을 자동으로 재생성한다.

CMake 내부 동작

  1. 최초 1회 cmake CMakeLists.txt 명령어 실행
  2. make명령으로 CMake로 생성한 Makefile 실행
  3. CMakeLists.txt 파일 변경 여부 검사 -> 변경된 경우 Makefile 다시 생성 후 실행
  4. Makefile에 정의된 각 Target별로 빌드를 수행
    • 내부 Build Step에 따라 cmake명령으로 각 Target을 빌드하는 데 필요한 Sub-Makefile 생성
    • 이 때 생기는 Sub-Makefile또한 CMakeFiles디렉토리 내부에 저장되고, 재실행 시 변경 여부 검사 -> 필요 시 재 생성

Git으로 버전 관리한다면, .gitihnore파일에 다음 4줄을 추가한다.

/CMakeCache.txt
/cmake_install.cmake
/CMakeFiles/
/Makefile

CMake 변수 및 명령어

[CMake 튜토리얼] 2. CMakeLists.txt 주요 명령과 변수 정리 참고하자


여기까지가 대략적으로 이해해야 할 명렁어들이고 더 찾아볼 일이 생기면 CMake 문서가서 읽어보자

CMakeLists.txt 기본 패턴

  • CMake 빌드 스크립트를 작성할 때도 Makefile을 비롯한 여타 스크립트와 마찬가지로 상단에는 설정 변수를 정의 하단에서는 이 설정에 따라 빌드 절차를 결정하도록 구성
  • 이렇게 하면 빌드 환경이나 구성이 바뀌어서 빌드 스크립트를 수정해야 할 필요가 있을 때, 필요한 부분을 금방 찾아서 고치기 가능

예시

copyButtonText
# 요구 CMake 최소 버전
CMAKE_MINIMUM_REQUIRED ( VERSION 2.8 )

# 프로젝트 이름 및 버전
PROJECT ( "andromeda" )
SET ( PROJECT_VERSION_MAJOR 0 )
SET ( PROJECT_VERSION_MINOR 1 )

# 빌드 형상(Configuration) 및 주절주절 Makefile 생성 여부
SET ( CMAKE_BUILD_TYPE Debug )
SET ( CMAKE_VERBOSE_MAKEFILE true )

# 빌드 대상 바이너리 파일명 및 소스 파일 목록
SET ( OUTPUT_ELF
        "${CMAKE_PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.out"
        )
SET ( SRC_FILES
        bar.c
        foo.c
        main.c
        )

# 공통 컴파일러
SET ( CMAKE_C_COMPILER "gcc" )

# 공통 헤더 파일 Include 디렉토리 (-I)
INCLUDE_DIRECTORIES ( include driver/include )

# 공통 컴파일 옵션, 링크 옵션
ADD_COMPILE_OPTIONS ( -g -Wall )
SET ( CMAKE_EXE_LINKER_FLAGS "-static -Wl,--gc-sections" )

# 공통 링크 라이브러리 (-l)
LINK_LIBRARIES( uart andromeda )

# 공통 링크 라이브러리 디렉토리 (-L)
LINK_DIRECTORIES ( /usr/lib )

# "Debug" 형상 한정 컴파일 옵션, 링크 옵션
SET ( CMAKE_C_FLAGS_DEBUG "-DDEBUG -DC_FLAGS" )
SET ( CMAKE_EXE_LINKER_FLAGS_DEBUG "-DDEBUG -DLINKER_FLAGS" )

# "Release" 형상 한정 컴파일 옵션, 링크 옵션
SET ( CMAKE_C_FLAGS_RELEASE "-DRELEASE -DC_FLAGS" )
SET ( CMAKE_EXE_LINKER_FLAGS_RELEASE "-DRELEASE -DLINKER_FLAGS" )

# 출력 디렉토리
SET ( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BUILD_TYPE} )
SET ( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BUILD_TYPE}/lib )
SET ( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BUILD_TYPE}/lib )

# 빌드 대상 바이너리 추가
ADD_EXECUTABLE( ${OUTPUT_ELF} ${SRC_FILES} )

빌드 파일 생성할 때는 build라는 폴더를 하나 만들어서 cmake ..를 실행한다.


좋은 자료가 있어서 링크 남김 (나중에 필요하면 또보자)

[CMake 튜토리얼] 1. CMake 소개와 예제, 내부 동작 원리
CMake할 때 쪼오오오금 도움 되는 문서 - luncliff
examples_CMake - jacking75 github

Reference

Compiling a C Program: Behind the Scenes - geeksforgeeks
make와 Makefile - bowbowbow
GNU Make강좌 - 임대영
make Makefile개념 및 사용법 정리
CMake 튜토리얼 1. CMake소개와 예제, 내부 동작 원리
CMake할 때 쪼오오오금 도움 되는 문서 - luncliff examples_CMake - jacking75 github
Ninja 빌드 시스템의 소개와 hello 예제 - makepluscode
리눅스상 C/C++ 빌드툴 정리.
Ninja document
GN(빌드시스템) - 위키백과
닌자 (빌드 시스템) - 위키백과
모두의 코드
find_package 와 pkg_check_modules에 의한 라이브러리 탐색

Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.