프로그래밍 언어/C언어

gcc를 이용해서 빌드를 Make로 하기

원원 2024. 7. 14. 22:54

안녕하세요. 오늘은 Makefile에 대해 알아보겠습니다. 
테스트하는 환경은 windows이고 MinGW를 이용해서 gcc를 사용합니다. 
(이 글을 이해하기위해서는 gcc사용방법을 알고있어야합니다)

make란 소스코드를 컴파일하고 빌드하는 작업을 자동화하는 도구입니다. make는 Makefile이라는 파일을 읽어들여 작업을 수행합니다. Makefile에는 소스파일과 빌드과정에서 수행해야 할 명령어가 명시되어 있습니다.

Makefile은 확장자가 없고 아래처럼 만들면 됩니다.

 main.c에는 HelloWorld!를 print하는 코드가 c언어로 작성되어있습니다.
main.c를 오브젝트파일로 생성하려면 gcc -c main.c을 하면 됩니다
main.o를 링크하고 main.exe라는 실행파일을 만들려면 gcc main.o -o main.exe 하면 됩니다

위의 과정에서 실행파일을 만들기 위해서 두가지의 타이핑을 했습니다.
(1) gcc -c main.c
(2) gcc main.o -o main.exe 
Makefile을 이용한다면 make만 타이핑해도 main.exe가 만들어지게 할 수 있습니다.

Makefile의 기본구조는 아래와 같습니다.
target: dependencies
    command
target : 만들고자 하는 파일 또는 목표입니다.
dependencies : 타겟이 만들어지기 전에 필요한 파일 또는 타겟입니다.
command : 타겟을 만들기 위해 실행되는 명령어입니다. 명령어는 반드시 Tab으로 시작해야 합니다.

먼저 의존성에 대해 테스트를 해보겠습니다. 

TEST1:TEST3
	@echo "TEST1"
	
TEST2:
	@echo "TEST2"
	
TEST3:TEST2
	@echo "TEST3"
	
TEST4:
	@echo "TEST4"

make를하면 첫번째 타겟이 실행됩니다. 여기서 첫번째타겟은 TEST1입니다.
TEST1은 TEST3을 의존하고 TEST3은 TEST2를 의존합니다. TEST2는 다른 타겟에 의존하지 않으므로 "TEST2"가 출력됩니다. 그리고나서 "TEST3"이 출력되고 "TEST1"이 출력됩니다.

make TEST2를 하면 TEST2는 다른 타겟에 의존하지 않으므로 "TEST2"가 출력됩니다.

make TEST3을 하면 TEST3은 TEST2를 의존하고 TEST2는 다른 타겟에 의존하지 않으므로 "TEST2"가 출력되고나서 "TEST3"가 출력됩니다.

make TEST4를 하면 TEST4는 다른 타겟에 의존하지 않으므로 "TEST4"가 출력됩니다.

Makefile을 수정했습니다.

RUN:
	@echo "######RUN START"
	gcc -c main.c
	gcc main.o -o main.exe

RUN은 다른 타겟에 의존하지않으므로 RUN의 내용이 실행됩니다.

Makefile을 사용할때 약속되어있는 여러가지를 알아보겠습니다. (이 글에서 사용할 것만 적었습니다)
https://www.gnu.org/software/make/manual/html_node/Rules.html
(1) 변수선언
변수를 선언해서 사용하기가 가능합니다.
CC=gcc
EXE=main.exe
위와같이 선언하면 됩니다. 사용할때는 $(변수명) 형태로 사용합니다.

(2) wildcard
파일 이름 패턴을  찾아줍니다. 
C_SRC = $(wildcard *.c) 라고 적으면 현재 디렉토리의 .c파일을 모두 찾아서 C_SRC변수에 저장합니다.

(3) 변수 치환
변수명 치환이 가능합니다.
C_SRC = main.c fun.c
OBJS= $(C_SRC:.c=.o)
C_SRC에 있는 .c파일을 .o로 바꿔서 OBJS에 저장합니다.

(4) 자동화변수
$< : 의존 파일
$@ : 타겟 파일
$^ : 모든 의존 파일 

위에있는 약속을 사용해서 Makefile을 만들어보겠습니다. 사용되는 소스코드와 프로젝트 구조입니다
┌─include
│  └── func.h
├─main.c
├─func.c
└─Makefile 

// func.h

#ifndef _FUNC_H
#define _FUNC_H

void hello1();
void hello2();
void hello3();

#endif
// func.c

#include <stdio.h>
#include "func.h"


void hello1()
{
	printf("hello1 ~ \n");
}

void hello2()
{
	printf("hello2 ~ \n");
}

void hello3()
{
	printf("hello3 ~ \n");
}
// main.c

#include <stdio.h>
#include "func.h"

int main()
{
    printf("HelloWorld! \n");

#ifdef hello_1
    hello1();
#endif

#ifdef hello_2
    hello2();
#endif

#ifdef hello_3
    hello3();
#endif
    return 1;
}
CC=gcc
EXE=main.exe
C_SRC = $(wildcard *.c)
OBJS= $(C_SRC:.c=.o)
DEFINES=-Dhello_2

CFLAGS=-Wall
INCLUDES= -I./include

all : $(EXE)

$(EXE): $(OBJS)
	@echo "1"
	$(CC) -o $@ $^ $(CFLAGS) $(INCLUDES) $(DEFINES)

%.o: %.c
	@echo "2"
	$(CC) -c $(CFLAGS) $(INCLUDES) $< -o $@ $(DEFINES)

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

HelloWorld!를 무조건 출력하고 hello_1 , hello_2, hello_3 가 define선언된 여부에 따라서 hello1~3 함수를 호출해줍니다
Makefile을 해석해보면 아래와 같습니다.
(1) C_SRC = main.c fun.c 입니다. OBJS = main.o fun.o 입니다.
(2) 첫번째로 나온 타겟은 all이고 하는 $(EXE)에 갑니다.
(3) $(EXE)타겟은 $(OBJS)를 의존합니다. $(OBJS)는 main.o와 fun.o을 만들기위해 %.o:%.c 규칙을 찾습니다.
(4) %.o : %.c 규칙에 따라 %.c파일이 %.o파일로 컴파일됩니다.
(5) $(EXE)에서 오브젝트파일을 링크해서 실행파일이 생성됩니다.

위에 처럼 Makefile을 구성해서 사용한다면 .c파일이나 .h파일을 추가해도 Makefile을 수정할필요가 없습니다.