리눅스에서 C/C++ 컴파일 하기 (#2)

윤찬호·2023년 4월 27일
0

Linux

목록 보기
2/3

빌드 예제

세 개의 소스파일을 컴파일하여 Object 파일을 생성하고, 이들을 한 데 묶는 링크 과정을 통해 실행 파일인 app.out을 생성한다. main.c 파일은 foo.h, bar.h를 참조한다.

Makefile을 사용하지 않고 불편하게 빌드하기

1. 컴파일 하기

$ gcc -c -o main.o main.c
$ gcc -c -o foo.o foo.c
$ gcc -c -o bar.o bar.c

-c 옵션은 소스파일(.c)로 Object파일(*.o) 생성하는 옵션이다. (링크를 하지 않고 컴파일만 한다)
-o 옵션은 출력 파일의 이름을 지정하는 옵션이다.

main.c 파일을 컴파일 하여 main.o 파일을 생성하고 마찬가지로 foo.o, bar.o를 생성한다.

2. 링크 및 실행 파일 생성하기

$ gcc -o app.out main.o foo.o bar.o

main.o, foo.o, bar.o 파일들을 묶는 링크 과정을 수행하고 실행 파일 app.out을 생성한다.

링킹 이란?
main.o 에는 foo, bar 함수를 호출하라는 내용만 있고, foo는 어디에 있고 어떻게 동작한다는 내용은 없다.
foo에 관한 정보는 foo.o에 있고, bar에 관한 정보는 bar.o에 있다.
따라서 실행 파일을 만들기 위해서는 main.o, bar.o, foo.o를 하나로 합치는 과정이 필요하고 이 과정을 링킹(Linking)이라고 한다.

Makefile을 사용해야 하는 이유

윈도우의 경우 IDE를 사용하여 헤더파일과 소스파일을 작성하고 빌드 버튼만 클릭하면 위의 과정들을 자동으로 수행해서 실행 파일을 생성해 주지만, 리눅스에서는 명령어를 수동으로 입력해야 한다.

Makefile을 사용하면 컴파일을 위해 수많은 명령어들을 일일이 반복해서 입력하지 않아도 된다. Makefile에 한번만 명령어들을 적어두면, make 명령어 하나만 실행해도 자동으로 컴파일 할 수 있다.

Makefile을 사용하면 Incremental Build기능을 사용할 수 있다. Incremental Build는 변경된 소스코드에 의존성이 있는 대상들만 추려서 다시 빌드하는 기능이다. Makefile에서 빌드 대상(Target)별로 의존성을 명시해 주면 이에 따라 자동으로 Incremental Build를 수행한다.

Incremental Build기능이 없다면 소스코드 한 줄을 수정하면 다른 많은 파일들을 다시 빌드해야 한다.

위 예제에서 main.c 파일을 수정했다면 아래 두 줄만 수행한다.

# main.o 컴파일, app.out 링크
$ gcc -c -o main.o main.c 
$ gcc -o app.out main.o foo.o bar.o

Makefile을 사용하고 편하게 빌드하기

빌드 규칙

<TARGET> : <DEPENDENCY>
(Tab) <COMMAND>
  • TARGET: 빌드 대상 이름. 최종적으로 생성하려는 것을 TARGET에 명시한다.

  • DENPENDENCY: 빌드 대상이 의존하는 TARGET이나 파일 목록. 여기에 나열된 대상들을 먼저 빌드하고 TARGET을 빌드 한다.

  • COMMAND: 빌드 대상을 생성하는 명령. 각 줄 시작은 반드시 Tab으로 들여쓰기를 해야한다.

※ DEPENDENCY에 나열된 파일들의 수정 시간보다 TARGET이 더 나중에 수정되었다면 TARGET을 빌드하지 않는다.

# example
all : app.out

app.out : main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o
    
main.o : foo.h bar.h main.c
	gcc -c -o main.o main.c

foo.o : foo.h foo.c
	gcc -c -o foo.o foo.c
    
bar.o : bar.h bar.c
	gcc -c -o bar.o bar.c

Makefile이 있는 경로에서 make 명령어를 치면 자동으로 main.o, foo.o, bar.o 파일들과 함께 app.out 파일이 생성된다.

1) all : app.out 최종으로 빌드하고자 하는 TARGET은 app.out 이다.

2) app.out : main.o foo.o bar.o app.out을 빌드하기 위해 main.o, foo.o, bar.o 가 필요하다.
만약 main.o, foo.o, bar.o 파일이 존재하고 해당 파일들보다 app.out이 더 나중에 만들어졌다면 빌드를 하지 않고 종료한다.

3) main.o 파일이 없다면 컴파일을 해야 하는데, 이때 필요한 파일들은 foo.h bar.h main.c 파일이다. gcc -c -o main.o main.c

4) foo.o, bar.o에 대해 3)과 동일한 작업을 수행한다.

all 옵션
모든 빌드 대상을 한 번에 빌드하거나, 기본적으로 빌드되어야 하는 타겟을 지정하는 데 사용된다. all 옵션이 없는 경우 제일 첫 번째 TARGET만 실행 시키고 종료한다. 위에 예시에서는 all 옵션을 사용하지 않아도 첫 번째 TARGET인 app.out을 빌드하기 위해 main.o, foo.o, bar.o가 생성되었지만, all 옵션을 사용하지 않고 foo.o가 첫 번째 TARGET으로 지정되었다면 foo.o만 생성된다. 따라서 all 옵션을 사용하여 최종적으로 생성하고자 하는 TARGET을 지정한다.

make 명령 뒤에 TARGET을 명시하면 해당 TARGET만 선별적으로 빌드할 수 있다.

$ make main.o

Makefile에서 주석은 문장 맨 앞에 '#'을 붙이면 된다.

내장 규칙

Make에서 자주 사용되는 빌드 규칙들은 내장이 되어 따로 기술하지 않아도 자동으로 처리된다. 소스 파일을 컴파일해서 Object파일로 만들어 주는 규칙이 여기에 해당한다. 따라서 Makefile의 내용을 아래와 같이 입력해도 app.out을 빌드할 수 있다.

app.out : main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o

하지만 위와 같이 작성한 경우 Incremental Build를 위한 의존성 검사에서 헤더 파일의 변경을 감지하지 못한다. Make는 소스 파일의 마지막 변경 시점만 확인할 뿐, 소스코드를 일일이 들여다보고 어떤 헤더 파일이 포함되었는지는 추적하지 않기 때문이다.

따라서 Incremental Build를 위해 아래와 같이 TARGET에 대한 DENPENDENCY까지 명시해 줘야 한다. 이렇게 하면 헤더 파일만 변경된 경우에도 의존성이 올바르게 탐지된다. 물론, 해당 소스파일에 헤더 파일을 추가(#include)할 때마다 이 부분을 업데이트 해야 한다. (혹은 그냥 방치하다가 원인 불명의 문제로 빌드가 안 되면 Clean build를 하는 방법을 쓸 수도 있다.)

app.out : main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o

main.o : foo.h bar.h main.c
foo.o : foo.h foo.c
bar.o : bar.h bar.c

마지막 세 줄은 TARGET에 대한 COMMAND가 생략되어 있지만 make 내장 규칙에 의해 컴파일이 수행된다.


위에 설명에서 소스 파일의 마지막 변경 시점만 확인할 뿐, 헤더 파일의 변경을 감지하지 못한다고 설명했다. 그렇다면 DENPENDENCY에 소스 파일은 명시하지 않아도 되는 것으로 보이나, 모든 상황에서 그렇지는 않다.

#example1)
all : app.out
app.out : main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o
main.o : foo.h bar.h
foo.o : foo.h   
bar.o : bar.h

#example2)
all : app.out
app.out : main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o
main.o : foo.h bar.h
foo.o : foo.h foo.c
bar.o : bar.h

위에 Makefile을 이용해 app.out을 빌드한 이후 foo.c의 내용을 수정하고 다시 make 명령어를 실행해보자. example1) 에서는 make: Nothing to be done메시지가 나오고 app.out을 다시 빌드하지 않는다. example2) 에서는 foo.c의 수정을 감지하고 foo.o, app.out을 다시 빌드한다.

변수 사용하기

변수를 사용하면 Makefile을 보다 깔끔하고 확장성 있게 작성할 수 있다. 변수 선언 및 사용법은 Bash쉘 스크립트에서와 같다.

변수 선언변수이름 = 값
변수 사용$(변수이름)

내장 변수

CC
C컴파일러를 호출하는 도구의 이름. (기본값 CC = cc)
기본값이 cc로 설정되어 있기 때문에 일반적으로 CC = gcc로 선언하여(덮어쓰기) 사용한다.

CFLAGS
C컴파일러에 전달할 옵션.

CXX
C++ 컴파일러를 호출하는 도구의 이름. (기본값 CXX = g++)

CXXFLAGS
C++ 컴파일러에 전달할 옵션.

LD
링커를 호출하는 도구의 이름. (기본값 LD = ld)

LDFLAGS
링커에 전달할 옵션.

LDLIBS
링커가 라이브러리를 링크할 때 사용할 라이브러리 지정.

AR
라이브러리 아카이브를 생성하는 도구의 이름. (기본값 AR = ar)

Make에서 내부적으로 정의되어 있는 변수들은 make -p 명령어를 사용하여 확인할 수 있다.


자동 변수

Makefile에서 사용되는 자동 변수들은 빌드 과정에서 자동으로 할당되는 특별한 변수들로, Makefile에서 명령어나 규칙을 작성할 때 유용하게 사용될 수 있다.

$@
현재 타겟(target)의 이름을 나타낸다.

$^
현재 규칙(rule)의 모든 의존성(dependency)의 목록을 나타낸다.

$<
현재 규칙(rule)의 첫 번째 의존성(dependency)의 이름을 나타낸다.

$?
현재 규칙(rule)의 변경된 의존성(dependency)의 목록을 나타낸다.
예를 들어, "make foo.o" 명령어를 실행할 때, "foo.c"라는 의존성이 변경되었다면, "foo.c"라는 문자열이 $?에 할당됩니다.

$$
$ 문자 자체를 나타낸다. Makefile에서 $는 변수나 자동 변수를 나타내기 위해 사용되는 특수 문자이므로, 실제로 $ 문자를 사용하려면 $$로 표시해야 합니다.

자동 변수 목록
https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html

# 컴파일러
CC = gcc
# 컴파일 옵션 (ex, -g: 디버깅 정보 생성, -Wall: 가능한 모든 경고 메시지 출력)
CFLAGS = -g -Wall
# 빌드 대상
TARGET = app.out
# DENPENDENCY
OBJS = main.o foo.o bar.o

all : $(TARGET)
$(TARGET) : $(OBJS)
    $(CC) $(CFLAGS) -o $@ $^
#	$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
# '$@'은 TARGET을 나타낸다.
# '$^'은 DEPENDENCY를 나타낸다.

gcc 컴파일 옵션 정리
https://jangpd007.tistory.com/220

clean

make의 clean은 빌드된 결과물(*.out) 또는 부산물(*.o)을 모두 삭제하여 "깨끗한" 상태에서 다시 빌드할 수 있는 환경을 만든다. 이렇게 함으로써 다시 빌드를 수행할 때 이전에 빌드된 파일이 영향을 미치지 않도록 할 수 있다.

CC = gcc
TARGET = app.out
OBJS = main.o foo.o bar.o

all : $(TARGET)
$(TARGET) : $(OBJS)
	$(CC) -o $(TARGET) $(OBJS)
# ...(생략)...

clean:
	rm -f $(TARGET) $(OBJS) 

make clean 명령어를 사용하면 TARGET과 OBJS로 지정한 파일들이 모두 삭제된다.

Makefile 기본 패턴

통상적으로 널리 사용되는 Makefile의 기본 패턴은 아래와 같다.

CC       = <C컴파일러>
CFLAGS   = <C컴파일 옵션>
CXX      = <C++컴파일러>
CXXFLAGS = <C++컴파일 옵션>
LDFLAGS  = <링크 옵션>
LDLIBS   = <링크 라이브러리 목록>
TARGET   = <빌드 대상 이름>
OBJS     = <빌드 대상이 의존하는 파일 목록 DENPENDENCY>

all : $(TARGET)
$(TARGET) : $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

#... (생략)...

clean :
	rm -f $(TARGET) $(OBJS)

Makefile 공식 매뉴얼
https://www.gnu.org/software/make/manual/make.html


참고
https://www.tuwlab.com/ece/27193
https://modoocode.com/311

0개의 댓글