뇌 마음 반반저장소

[42] 왕초보의 Makefile 뿌시기 본문

좌뇌

[42] 왕초보의 Makefile 뿌시기

맹진저 2022. 11. 28. 23:54
728x90

 so_long까지 어찌어찌 오면서 애매하게 알고 있던 Makefile이 계속 발목을 잡았다. 이번 기회에 확실히 잡고 넘어가야겠다. 코딩을 하면서 지금까지도 무엇부터 건드려야 할지 정말 모르겠는게.. 이 느낌이.. 아직도 피신때 그 느낌이다.. 일단 메인 함수 쓰고 추가 함수들을 늘려가는 건가? 점점 더 복잡한 코드들을 작문해야 할 때는 정말 어디서부터 건드려야 할지 감이 오지 않았다. (42는 아무것도 알려주지 않아요.. 섬세한 코린이는 매일 웁니다.. 뭐 이렇게 알아가는 거겠죠..)

 

 몇 번 경험을 해보니, 크게 Makefile - Header file - Function files이 서로 첨예하게 연결되어 있다는 것을 깨달았다. 그래서 나는 일단 기본적으로 필요한 것들을 세팅한 후에 코드를 본격적으로 들어가는 방법을 선택했다. 예를 들어 Makefile이나 헤더 파일에 필요한 기본적인 것들을 미리 세팅하고, 나중에 필요한 소스파일 이름을 Makefile에, 함수 이름을 Headerfile에 넣는 것이다. 일단 여기서는 Makefile만 정확히 알고 가보자.

 

1. 왓 더 X 이즈 컴파일

일단 컴파일을 하는 과정을 알아보자.

우리는 C를 컴파일할 때 gcc(GNU Compiler Collection)라는 컴파일러를 쓴다. 원래는 C만 컴파일해줬었는데 지금은 여러 프로그램을 지원한다. 그럼 컴파일러란 뭔 뜻일까?

컴파일러란?
컴파일러(compiler, 순화 용어: 해석기, 번역기)는 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 언어 번역 프로그램을 말한다.

그렇다. 우리가 C언어로 뚱땅뚱땅 열심히 작문을 하면 컴파일러가 컴퓨터 언어로 번역해준다. 컴파일러는 쉽게 말해 컴퓨터 언어 번역기 같은 샘이다. 컴파일은 총 네 가지의 실행단계를 거친다. 

 

1. 전처리기(Preprocessor), 구문분석 : 마치 우리가 고등학교때 영어학원에서 구문을 쪼개서 분석했던 것 처럼(아련하노~),

소스 코드 파일을 읽어 개별 문법요소(연산자, 괄호, 식별자 등) 단위로 자른 후,

이 문법요소들을 해석하여 추상 구문 트리(문법적 가지)를 생성한다.

이 과정에서 문법에 맞지 않는 소스 코드는 사용자에게 알려준다. (error 표시!)
-> 소스코드를 문법적으로 잘라서 번역할 준비를 한다!

  : *. c 파일들과 헤더(#include)와 매크로(#define)들을 분석해서 내용을 그대로 포함해줌. 👉 *. i 파일로 변함

 

gcc -E program.c -o program.i

👉maim.i 파일이 생성된다!

 

 

2. 컴파일러(Compiler), 최적화: 추상 구문 트리를 분석하여 최적화를 수행한다. 도달할 수 없는 코드를 식별하거나, 상수 표현식을 미리 계산해 두거나, 루프 풀기 등의 대부분의 최적화가 이 단계에서 수행된다.
-> 위에 확장된 소스코드를 컴퓨터 언어(어셈블리 코드)로 번역한다! : 👉 *.s 파일로 변함

 

gcc -S program.i -o program.s

👉main.s 파일이 생성된다!

 

3. 어셈블러(Assembler), 코드 생성: 완전히 기계어로 바꿔주는 역할을 한다. 최적화된 구문 트리로부터 목적 코드를 생성한다. 목표 언어가 기계어일 경우, 레지스터 할당, 연산 순서 바꾸기 등 하드웨어에 맞는 최적화가 이 단계에서 수행된다.
-> 최종 번역된 언어를 파일로 만든다! : 👉*.o 파일로 변함

 

gcc -c main.s -o main.o

main.s를 사용해 main.o를 만든다. 👉 main.o가 생성된다!

 

4. 링커(Linker), 연결하기: 목적 코드가 기계어일 경우, 여러 라이브러리 목적 코드를 묶어 하나의 실행 파일을 생성하게 된다. 이 과정은 링커에 의해 수행되며, 어떤 사람들은 링커를 컴파일러의 일부로 간주하지 않기도 한다.
->여러 번역 파일들을 쭉 연결하고 실행할 수 있는 하나의 파일을 만든다! : 👉 .o파일이 생성되고 .exe 파일도 생성됨! ex) ./a.out (만약 뒤에 지정할 이름을 붙이지 않으면 리눅스 에서는 자동으로 a.out으로 실행파일이 생성된다.) 

 

gcc main.o -o main

👉 main.o파일이 생성되고 main.exe가 생성된다!

 

간단하게 컴파일 하기

gcc main.c -o main

 이렇게 하면 바로 위의 과정을 거쳐 실행파일로 변하는 것이다! 참 쉽쥬?

 

2. 맥파일, 뭔데!

 그렇다면 Makefile은 무엇일까.

Makefile이란?
Shell에서 컴파일하는 방법 중에 하나인데, Makefile이란 파일을 딱 하나만 만들어서, 할 수 있는 명령어는 다 때려 박는다. 그래서 make만 쓰면 수백수천 개의 파일을 다 컴파일해줄 수 있다.

Makefile은 위의 과정을 다 담고, 한꺼번에 파일을 오브젝트 파일을 만들어 주고, 한꺼번에 오브젝트 파일을 삭제해 주는 파일인 것이다. 그렇다면 맥파일은 어떤 구조로 이루어져 있을까?

 2-1. 일단 매크로를 만들자.

매크로는 복잡한 것을 간단하게 표시하는 것이다. 

매크로의 기본규칙은

대문자로 매크로 이름 만들기,

'=' 써주기,

매크로를 정의하는 문자열 작성하기이다.

각설하고 예제를 보자.

NAME = libft.a #libft.a 라이브러리 파일을 만든다.

CC = gcc #CC를 입력하면 gcc를 실행

CFLAGS = -Wall -Wextra -Werror #CFLAGS를 입력하면 이걸 실행

TARGET = hello #실행파일 이름을 hello로 생성해서 ./hello로 실행

SRC = main.c ... #소스로 사용되는 파일 이름들을 쭉 쓴다

- 매크로 이름 : NAME, CC, CFLAGS 명령어의 옵션을 세팅한다. 미리 정해져 있는 매크로가 있다! 👇

https://www.tutorialspoint.com/makefile/makefile_macros.htm

 

Makefile - Macros

Makefile - Macros The make program allows you to use macros, which are similar to variables. Macros are defined in a Makefile as = pairs. An example has been shown below − MACROS = -me PSROFF = groff -Tps DITROFF = groff -Tdvi CFLAGS = -O -systype bsd43

www.tutorialspoint.com

- 매크로를 정의하는 문자열: libft.a, gcc, -Wall -Wextra -Werror : 명령어 안에 실행될 친구들을 자세히 써준다.

- 나중에 매크로를 참조할 때는 $(매크로 이름) 혹은 ${매크로이름} 이렇게 사용해주자.

 

(보통 그냥 괄호를 많이 쓴다고 한다. 그리고 중괄호와 그냥 괄호를 혼합해서 쓰지 말고 단일 괄호로 사용하는 것을 권장한다. 컴퓨터의 헷갈림 방지. 이것 때문에도 한참 찾았...)

 -> 그러면 나중에 $(CC) 이렇게만 쳐도 gcc가 실행되는 것이다!

 

Makefile에 미리 정의되어있는 사전 정의 매크로들이 있다. make -p를 셸에서 명령어로 치면 주르륵 매크로 리스트들이 나오는데 CC, CFLAGS처럼 낯익은 이름들이 나온다. (엄청 길다..)  과제를 하면서 꼭 넣어주어야 했던 $(NAME), all, clean, fclean 그리고 re를 꼭 넣어주자. Libft 시작하기 참고!

 

👇다양한 타깃 더 찾아보기

https://www.gnu.org/software/make/manual/html_node/Rules.html

 2-2. 빌드 규칙을 만들자.

일단 빌드 규칙을 시작하기 전에 빌드 구조는 C언어처럼 위에서부터 순차적으로 실행되는 구조임을 이해하자. 일단 문법적으로 크게 세 가지 파트로 나뉜다. 공식은 이렇다.

target : Dependency
(Tab)	Recipe

1. 목표(Target) : 사용할 수 있는 명령어

2. 빌드 대상(Dependency) : 위의 목표나 파일 목록으로 연결

3. 탭(Tab) + 명령(Recipe) :  **꼭! 앞에 탭을 치고** 여러 가지 빌드 명령어를 작성한다. 다이내믹 매크로를 함께 사용.

 

👇다이내믹 매크로 설명 더보기👇

더보기

Dynamic Macros

make -p로 확인할 수 없는 달러표시와 연산 문자가 달린 명령어들이다.

 

1. $@ : 현재 목표(Target) 파일들

2. $^ : 현재 빌드 대상(Dependency) 파일 목록 전체

3. $* : 현재의 파일 목록의 파일들을 확장자를 제외한.. ex) $*.c (모든 C파일들)

4. $< : 현재의 목표(Target) 파일 목록의 첫 번째 파일, 그러니까 빌드 대상(Dependency)에 있는 첫! 번! 째! 파일!!! 얘 때문에 많이 헷갈렸는데 아래에서 계속 예를 들어 보겠다.

 

👇다이내믹 매크로 자세히 보기

https://docs.oracle.com/cd/E19504-01/802-5880/make-59132/index.html

 

예를 들면

%.o: %.c 
        $(CC) -c $(CFLAGS) -o $@ $<

모든.c 파일을 모두.o파일로 바꿀건데 아래 레시피처럼 요리해서 바꿔줘

 

      $(CC) : 를 사용해 gcc 하고
👉 -c  :  .o 파일인 오브젝트 파일을 생성하고 (-c는 그냥 오브젝트 파일만 만들어줌)

            (gcc옵션 : gcc man : .c ------> .o )
👉 $(CFLAGS) : 로 에러를 확인한뒤
👉 -o : 아웃풋(out) 출력 파일명을 지정한다. 위에는 지정을 안했으니 a.out으로 자동 지정

            (gcc옵션 : gcc man : .o ------> 실행파일명.exe)

누구를?

👉 $@ : 현재 목표 파일들을 ( = 모든 오브젝트 파일, .o 파일들)

👉 $< : 현재 .c파일들을 (빌드 대상(Dependency)에 있는 첫! 번! 째! 파일!!! 위치를 가리킴)

 

이게 내가 해석한 빌드 규칙이다 헥헥.. 명령어 하나하나 찾느라 오랜 시간이 걸렸지만 이렇게 이해를 해야 제대로 할 것 같아서 하나하나 찾았다. 요즘에는 아래처럼 간단하게 작성한다고 한다.

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

      $(SRCS) : 에 속한 모든 파일들을

👉 :  : 대입 참조 기법을 통해

👉 .c : 를 

👉 .o : 로 바꾸노라.

👉 이런 과정을 OBJS 매크로라고 칭하겠다!

 

3. 타깃을 만들자! 42 필수 타깃

그럼 이제 42의 필수 Makefile 명령어를 살펴보자.

 3-1. 필수 타깃 : $(NAME)

하지만 우리는 하나의 라이브러리, 즉 컴파일된 오브젝트 파일들이 하나로 묶여있는 상태로 만들어야 한다. 같은 아카이브 목록에 저장이 되어 있어야만 내가 필요할 때 쓸 수 있기 때문에 파일 라이브러리를 생성해 준다.

NAME = libft.a

이건 그냥 라이브러리만 만들어 준 것이다. 우리는 또 다른 명령어를 통해 이 라이브러리에 오브젝트 파일들을 추가해 주어야 한다..! (아오 쓰면서 이제 이해가 되네 어휴)

$(NAME) : $(OBJS)
	ar rc $(NAME) $(OBJS)

 

1. $(OBJS) : 이 타깃을 (->오브젝트 파일이 만들어짐) 

2. ar rc $(NAME) $(OBJS)

      ar : 새로운 라이브러리 만들려고 하는데요,

👉 r : 새로운 오브젝트 파일이면 추가해주고, 기존 파일이면 새로 들어온 걸로 바꿔주시고요

👉 c : 아카이브(라이브러리 파일)를 생성해주세요. 

뭐를?

👉 $(NAME) : 이라는 라이브러리 파일에다가,

👉 $(OBJS) : 이 파일들을 넣어주세요. 

 

👇ar에 대한 설명을 더 보려면👇

더보기

Makefile ar rc에 대해.. 정말 여러 군데 다 뒤졌다.
'ar rc'는 라이브러리를 생성하는 데 사용되는 명령어이다. Makefile에서는 이렇게 간단하게 매크로를 만들 수 있다.

AR = ar -rc

rc 앞의 "-"는 써도 컴파일이 되고 안써도 되더라!

 

libft 프로젝트에서 만들어야 하는 것은 정적 라이브러리이다. 정적 라이브러리는 컴파일된 오브젝트 파일들이 하나의 아카이브로 묶여 있는 형태로, 오브젝트 파일들을 묶어 주는 명령어가 바로 ar 명령어이다.

ar : 파일 아카이브를 만들고 유지한다. 
r : 새로운 오브젝트 파일이면 추가, 기존 파일이면 치환
c : 아카이브(라이브러리 파일) 생성, 존재하지 않는 아카이브를 작성(또는 갱신)하는 경우에도 경고 메시지를 출력하지 않음
u : 오브젝트 파일의 타임스탬프를 비교해 새로운 파일일 경우에만 치환
s : ranlib(1)과 마찬가지로 아카이브 인덱스 생성. 아카이브 인덱스를 생성하지 않으면 링크 속도가 느려지고, 시스템 환경에 따라 에러가 발생할 수도 있음

 

(셸 라이브러리 추가 명령어를 그대로👇)

ar -tv lib-name.a #라이브러리 목록확인 명령어
ar rc lib-name.a file-name.o #라이브러리에 파일을 넣는 명령어

 

부분출처: https://velog.io/@welloff_jj/libft-Linux-ar-%EB%AA%85%EB%A0%B9%EC%96%B4-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC

 3-2. 필수 타깃 :  all

make이라는 명령어를 셸에 입력하면 가장 첫 번째 타깃을 기본적으로 처리한다. 그래서  all을 타깃들 중 가장 위에, 먼저 적어주게 되면! 자동으로 all 뒤에 입력된 빌드 대상으로 이동하면서 일을 수행한다. 그래서 Makefile 코드를 뒤죽박죽 작성했더라도 all을 사용하면 그곳이 가리키는 곳으로 가서 착착 일을 수행한다.

all : $(NAME)

 3-3. 필수 타깃 :  clean

clean 타겟은 .o파일들 즉, 오브젝트 파일을 모두 삭제하는 데 사용된다.

clean:
	rm -rf $(OBJS)

👇rm -rf 명령어 더보기👇

더보기

rm : 파일 혹은 디렉토리를 삭제하는 명령어

 -f : 삭제 메시지, 오류를 반환하지 않는 명령. 파일이 없어도 그냥 진행한다.

 -r : 디렉토리들과 그 내용물을 자신을 재 참조한 것까지 다 합쳐서(재귀적으로) 삭제한다.

 3-4. 필수 타깃 :  fclean

fclean 타깃은 모든 바이너리 형식의 파일(임시 파일, 실행 파일 및 개체 파일 등)을 삭제한다.

fclean: clean
	rm -rf $(NAME)

clean : clean을 실행하고

rm -rf $(NAME) : 모든 개체 파일과 실행파일까지 제거해라!

 3-5. 필수 타깃 :  re

re는 make re라는 명령어를 쳤을 때 아래와 같이 일단 fclean을 실행하고 다시 all로 모든 걸 실행하라는 지우고 다시 시작하는 명령어이다. 한마디로 리셋-> 리부트

re: fclean all

4. 그 외 여러 물음표들

 4-1. 두 번째 make을 실행했을 clean을 해줘야 다시 잘 되는 경우

 가끔 make를 두번째 실행했을 때 오류가 뜨는 경우가 있다. (가끔 다른 사람들이 한 코드를 실행했을 때 많이 봤던..)  이럴 경우 clean이나 fclean 사용한 뒤 실행하면 잘 되는 상황을 마주한다. stackoverflow에도 clean을 하고 다시 해보세요~라는 답변을 많이 보는데 결국 우여곡절 끝에 답을 찾았다.

 

 이는 재 컴파일할 때 수정한 파일만 재컴파일하면 좋겠지만 makefile을 쓰는 우리 입장에서는 그냥 make만 눌러서 실행하고 싶은 마음이 굴뚝같을 것. 이는 .h파일이 재컴파일되는 것을 설정해 놓지 않아서 그렇다. 대부분의 42 make파일은 .h파일의 의존성을 설정하지 않고 .c파일만 설정해 놓은 경우가 많다. $ 매크로 덕분에 수정된 최신 파일들은 모두 컴파일에 성공하지만 .h파일은 만족하는 규칙이 없기 때문에 아무런 Recipe이 수행되지 않는다. 그래서 추가로 오브젝트 파일에 헤더 파일과의 의존성을 추가하면 이 문제가 해결된다고 한다. 

확실하지 않은 오류 해결방법 👇

더보기

헤더를 종속시켜서 항상 함께 다니게 한다?

%.o : %.c %.h

 4-2. PHONY는 무엇인가

PHONY는 보통 이렇게 사용한다.

.PHONY : all clean #타겟이름들

PHONY앞에 붙어 있는 "."은  스페셜 내장 타깃(Special Built-in Target Names)을 불러올 때 사용한다. 가장 유명한 스페셜 타겟으로는 .PHONY와 .SUFFIXES가 있다. 더 많은 스페셜 타깃을 보려면 여기로 가보자. https://www.gnu.org/software/make/manual/html_node/Special-Targets.html

 

Special Targets (GNU make)

The targets which .PRECIOUS depends on are given the following special treatment: if make is killed or interrupted during the execution of their recipes, the target is not deleted. See Interrupting or Killing make. Also, if the target is an intermediate fi

www.gnu.org

PHONY는 타겟과 파일이 동일한 이름일 경우, 둘이 다르다는 것을 make에게 알려주는 매크로다. 예를 들어 파일 이름 중에 all이라는 파일이 있으면 makefile은 "엥? all이 벌써 있네? 주인님! 일 끝났어요!" 라며 실행을 마친다. 그래서 실제 Makefile의 경로에 동명의 이름으로 된 파일이 있을 경우 오동작을 일으킬 가능성이 있으므로 PHONY를 사용해서 프로그램 안에서 오해를 방지한다.

(Phony.. 참 쉽죠? 정말 별거 아니었다..)

 4-3. your Makefile must not RELINK

프로젝트를 시작할 때 위의 영문처럼 리링크 절대 하지 말라고 쓰여있다. (Common Instructions 확인하러 가기 :https://sudo-me.tistory.com/3) 그렇다면 일단 Relink란 무엇인가?

Relink 리링크
개체를 다시 연결하는 무한 굴레의 명령을 실행한다.

구선생을 샅샅이 뒤져본 결과 많은 리링크의 사례를 적어놓은 분의 블로그를 찾게 되었다. 할렐루야!

다양한 리링크의 사례가 있으니 리링크 문제를 겪고 있다면 한번 들어가 보면 좋을 것 같다.

 

👇다양한 relink 리링크 사례 보기

https://stdbc.tistory.com/69

 

 

5. 끝으로

얼마 전에 C++프로젝트 에발 갔을 때도 Makefile 쓰고 있던데.. 앞으로 계속 알게 되는 건 업데이트를 추가로 해야겠다. Makefile만 잘해도 프로젝트가 쉬워진다던데 이제 본격적으로 시작해볼까!

 

 

 

도움을 주고 싶으신 내용이나

틀린 내용이 있다면 댓글로 남겨주시고,

도움이 되었다면 공감 한 번씩 눌러주세요👍❤️

728x90
Comments