본문 바로가기
Code/C

[C언어] 컴파일, 링킹, 헤더파일, Makefile

by 코드포휴먼 2020. 9. 29.

컴파일, 링킹

소스파일
(*.c)
목적파일
(*.o)
실행파일
(a.out)
인간이 이해하는 프로그래밍 언어 기계어 (기계가 이해할 수 있는 언어)
gcc compiler가 필요하다.
기계어+시스템 라이브러리로 만든다.
gcc가 실행파일을 만들어준다.
(.out / .exe)

1) 컴파일(compile)
실행 파일을 만들기 위해서는 먼저 컴파일(compile) 과정이 진행된다.

컴파일(compile)은 단일 소스 코드 전체를 어셈블리어(기계어와 1 : 1 대응) 로 변환해준다.

cpu가 연산하기 위해서는 기계어의 명령이 있어야 한다.

(이 때, 목적코드라 불리는 .o 파일이 생성된다).

 

2) 링킹(linking)

이 과정이 끝나게 되면 링킹(linking)이 진행되는데, 말그대로 각기 다른 파일에 위치한 소스 코드들을 한데 엮어서 하나의 실행 파일로 만들어지는 과정이다.

실행파일은 목적파일 .o를 통해 만든 바이너리 파일이다.

링킹 과정에서 특정한 소스 파일에 있는 함수들이 어디에 있는지 찾는 과정을 거친다.

 

만약 str.c test.c 두 파일을 만들었다고 가정해본다.

만약 str.c에서 커스텀 함수를 작성하고, 그 함수를 test.c에서 사용했다면 test.c 상단에 사용한 함수의 원형을 명시한다.

이때 링커(링킹을 해주는 프로그램)은 'test.c에서 커스텀 함수를 호출하는 경우 str.c 에서 찾아라' 정도로 처리해준다.

덕분에 test.c 에서 다른 파일에서 작성한 함수를 호출하더라도 str.c 의 함수를 이용할 수 있게 되는 것이다.

 

사진출처 : 블로그 <모두의 코드> https://modoocode.com/87

 

 

헤더파일

헤더파일의 필요성

앞에서 본 것처럼 파일을 여러개로 나눠야할 경우가 많다.
#include 와 같은 명령들은 전처리기(Preprocessor) 라고 부르는데, 이러한 명령들의 특징은 컴파일 이전에 실행된다는 점이다.

이 명령은 우리가 지칭하는 파일의 내용을 정확히 100% 복사해서 붙여넣는다.

따라서 #include "str.h" 라는 명령은 str.h 에 있었던 내용으로 컴파일이 시작하기 전에 변경된다.
#include <stdio.h> 역시 똑같다. stdio.h 에 써있는 내용들이 정확히 복사되어 컴파일 이전에 코드에 붙어버린다.

< > 로 감싸는 헤더파일은 컴파일러에서 기본으로 지원하는 헤더파일의 경우이고,

" " 로 감싸는 헤더파일은 사용자가 직접 제작한 헤더파일의 경우이다.
만일 헤더 파일이라는 것이 존재하지 않았더라면, printf 함수를 이용하기 위해서 printf 함수의 원형을 써주어야 하는데 이는 매우 복잡하다.

_Check_return_opt_ _CRTIMP int __cdecl printf(
  _In_z_ _Printf_format_string_ const char* _Format, ...);

printf, scanf 함수와 같이 매 함수를 쓰기 위해서 위 모든 내용을 쓰는 것 대신에 헤더파일 include 하나로 간단하게 해결할 수 있다.

보통 헤더파일을 만들 때에는 그 헤더파일에 있는 함수들이 정의되어 있는 소스 파일의 이름을 따서 짓는다.

위 경우 str.h 에 선언되어 있는 함수들이 모두 str.c 에 정의되어 있으므로 헤더파일의 이름을 str.h 로 하였다.

 

 

Makefile

Makefile의 필요성

  • 반복되는 컴파일 작업이 지겹고 시간이 오래 걸리기 때문이다.
  • Makefile을 통해 수정된 파일만 컴파일 할 수 있다.
  • 대규모 프로젝트, 공동 프로젝트에서 반드시 필요하다.

아래의 간단한 코드를 예시로 들어본다.

// kor.c
#include "main.h"
void proc_kor()
{
	printf("Hello, I'm kor()\n");
}

// usa.c
#include "main.h"
void proc_usa()
{
	printf("Hello, I'm USA()\n");
}

//main.c
#include "main.h"
int main()
{
	printf("Hello, I'm main()\n");
	proc_kor();
	proc_usa();
	return (0);
}

//main.h
#include <stdio.h>
void proc_kor();
void proc_usa();

 

먼저 위의 네가지 파일을 가지고 목적파일만 만들어본다. (컴파일만 한다.)

// -c 옵션 : 소스파일(.c)로 목적파일(.o) 생성
gcc -c main.c kor.c usa.c

// 각각의 소스에 해당되는 목적파일 확인 가능
ls
kor.c kor.o main.c main.h main.o usa.c usa.o

 

목적파일은 링킹을 통해 바이너리 파일로 바꿀 수 있다.

// 목적파일(.o)을 링커 과정을 통해 실행파일로 생성
// 실행파일 이름을 주지 않으면 디폴트로 a.out으로 만들어짐
gcc -o app.out main.o kor.o usa.o

// 실행파일 app.out 확인가능
ls
app.out kor.c kor.o main.c main.h main.o usa.c usa.o

// app.out 실행해보기
./app.out
Hello, I'm main()
Hello, I'm kor()
Hello, I'm USA()

 

실행파일을 한번에 만들 수도 있다.

// 목적파일과 실행파일 한번에 만들기
gcc -o app.out main.c kor.c usa.c

ls
app.out kor.c main.c main.h usa.c

지금까지 했던 과정은 파일이 추가되거나 바뀔 때마다 계속 해줘야 한다.

하지만 Makefile을 이용한다면 수고가 현저히 줄어든다.

 

 

Makefile의 구조

Makefile의 구조는 아래와 같다.

TARGET : DEPENDENCY
	command
  • TARGET : 만들려는 파일
  • DEPENDENCY : 필요한 재료 (필요한 것이 없다면 안 적어도 된다.)
  • command : shell 커멘트에 명령을 준다. (Tab으로 구분.)

 

예시로 만든 Makefile이다. 추후 make 명령어로 실행시킬 수 있다.

// Makefile
app.out : main.o kor.o usa.o
	gcc -o app.out main.o kor.o usa.o
    
main.o : 
	gcc -c main.c 
    
kor.o :
	gcc -c kor.c
    
usa.o :
	gcc -c usa.c
    
    
// shell
make
gcc -c main.c
gcc -c kor.c
gcc -c usa.c
gcc -o app.out main.o kor.o usa.o

ls
app.out kor.c kor.o main.c main.h main.o Makefile usa.c usa.o

만약 위의 코드에서 app.out 을 만들려는 코드가 제일 마지막에 간다면 main.o만 만들고 종료한다.

따라서 아예 첫 부분에 작성하거나 all 옵션을 주면 된다.

 

 

all에는 최종으로 만들고 싶은 파일을 명시한다.

all 옵션이 없는 경우 제일 첫 번째 Target만 실행시키고 종료한다.

아래의 코드는 위의 코드와 실행결과가 같다.

// Makefile
all : app.out 
    
main.o : 
	gcc -c main.c 
    
kor.o :
	gcc -c kor.c
    
usa.o :
	gcc -c usa.c
    
app.out : main.o kor.o usa.o
	gcc -o app.out main.o kor.o usa.o

 

 

이번에는 컴파일 명령을 환경변수화 시켜서 컴파일 명령, 바이너리 파일 이름을 변수화했다. 

(os마다 다른 컴파일러가 존재한다.)

변경사항이 발생하면 환경변수 부분의 값만 바꿔주면 된다. 

아래의 코드는 위의 코드와 실행결과가 같다.

// Makefile
CC = gcc
TARGET = app.out
OBJS = main.o kor.o usa.o

all : $(TARGET)
    
$(TARGET) : $(OBJS)
	$(CC) -o $(TARGET) $(OBJS)
    
main.o : 
	$(CC) -c main.c 
    
kor.o :
	$(CC) -c kor.c
    
usa.o :
	$(CC) -c usa.c

환경변수를 선언하고, 선언한 변수는 $(변수)로 사용한다.

OBJS는 오브젝트 파일로서, 공통적으로 쓰는 Makefile의 내부 변수다.

 

 

중복되는 용어를 개선해보겠다.

또한 오브젝트파일이 추가되면 .c 소스파일을 컴파일하는 명령어를 또 작성해야 하는 형태인데, 이를 개선해본다.

// Makefile
CC = gcc
TARGET = app.out
OBJS = main.o kor.o usa.o

all : $(TARGET)
    
$(TARGET) : $(OBJS)
	$(CC) -o $@ $^
    
.c.o :
	$(CC) -c -o $@ $<

shell에서 다음과 같은 단축 표현을 지원한다.

  • $@ : $(TARGET) 
  • $^ : $(OBJS)
  • $< : .c 소스파일
  • .c.o : Makefile이 위치한 공간에 있는 .c 파일을 .o 오브젝트로 바꿔준다. 
           .o 파일을 TARGET으로 잡아준다.

 

 

CFLAGS, LDFLAGS라는 Makefile 내부변수를 활용해본다.

clean이라는 타겟을 제작해본다.

// Makefile
CC = gcc
TARGET = app.out
OBJS = main.o kor.o usa.o

CFLAGS = -Wall
LDFLAGS = -lc

all : $(TARGET)
    
$(TARGET) : $(OBJS)
	$(CC) $(LDFLAGS) -o $@ $^
    
.c.o :
	$(CC) $(CFLAGS) -c -o $@ $<
    
clean : 
	rm -f $(OBJS) $@
    
    
// shell
make
gcc -c main.c
gcc -c kor.c
gcc -c usa.c
gcc -o app.out main.o kor.o usa.o

make clean
rm -f main.o kor.o usa.o app.out
  • CFLAGS : 컴파일 할 떄 옵션을 줄 수 있다. 
  • LDFLAGS : 링크할 때 옵션(라이브러리)을 줄 수 있다. (ex. lc, lopenssl)

 

Makefile을 통해 수정된 파일만 컴파일해서 바이너리파일을 다시 만드는 기능도 누릴 수 있다.

// shell
touch main.c   // 타임스탬프 변경 (컴파일러는 수정했다고 판단)
make
gcc -Wall -c -o main.o main.c
gcc -lc -o app.out main.o kor.o usa.o

 

이처럼 Makefile을 만들어놓으면 무척 편리하다.


출처

컴파일, 링킹, 헤더파일

블로그 <모두의 코드>, "씹어먹는 C 언어 - <18 - 1. 파일 뽀개기 (헤더파일과 #include) >", modoocode.com/87

Makefile
유투브 <시골사는개발자>, "Makefile 시작하기", www.youtube.com/watch?v=jnJL6ppn26Q

 

추가로 보면 좋은 것들

Makefile 한글 문서

임대영, "GNU Make 강좌", doc.kldp.org/KoreanDoc/html/GNU-Make/GNU-Make.html#toc8

댓글