[ BBB - yocto (5) ] makefile 작성법과 컴파일 자동화

1. 헤더파일 소스파일


우선 makefile에 들어가기 전에 헤더파일과 소스파일에 대해서 간단히 알아보자


- 헤더파일 

1) *.h로 끝남

2) 함수, 매크로, 구조체, 열거형...등을 다른 소스파일 혹은 헤더파일에서 인클루드(#include)하여 사용할 수 있도록 해주는 파일이라 생각하면 될거야


- 소스파일

1) *.c로 끝남

2) 로직의 동작 과정을 짜는 공간이라 생각하면 됨, 주로 함수의 동작을 코드로 적어놓은 파일이야


- 해더파일과 소스파일 사용시 주의사항

1) 중복 인클루드 방지

/* gpio.h */
#ifndef __GPIO__
#define __GPIO__

//대충 함수

#endif


이 과정을 보면, #ifndef 매크로와 #define매크로로 중복 인클루드 방지 처리를 해놓은 것을 볼 수 있어


저게 없으면 헤더파일을 중복하여 인클루드하는 경우 오류가 발생하지.


#ifndef [매크로 변수] : 매크로 변수가 정의되어 있지 않으면

#define [매크로 변수] : 매크로 변수를 정의한다.

#endif : #if 매크로의 끝


이렇게 하면 헤더 파일을 중복 인클루드 하면 매크로 변수가 정의되어 있어 #endif로 바로 점프하게 되어 헤더파일 중복 인클루드를 방지할 수 있어.


2) 전역 변수 선언하지 않기


중복 방지를 했다고 해서 컴파일하는데 오류가 안나는건 아니야 아래 코드를 봐보자.

/* gpio.h */
#ifndef __GPIO__
#define __GPIO__

int gpio;

#endif

파일이 이렇게 있어

> gpio.c gpio.h main.c

코드를 이렇게 짰다고 하자


겉보기에는 아무런 문제가 없어 보여


하지만 이것을 gpio.c에서 gpio.h를 포함하려고 하고,


main.c에서 gpio.h를 포함하려고하면 문제가 되어 버리지


main.c에서 gpio.h를 읽어, gpio 인트형을 만들고


gpio.c에서 gpio.h를 읽어, gpio 인트형을 만들려고 보니 이미 선언돼있네?


이래서 오류가 발생해,


이는 헤더파일 가드가 각 c파일에서 따로 적용되기 때문이지. 전처리기가  #define을 파일마다 각각 처리하기 때문이야.


 따라서 전역변수를 헤더파일에 선언하면 안돼.


그러면 한 파일의 전역변수를 갖다 다른 파일에서 써보고 싶어. 그러면 2가지방법이 있어


1. 함수로 전역변수 주소 넘겨주기

전역 변수 주소를 넘겨주는 기법이야


2. 함수로 전역변수 값 변경하기

함수로 변경하는 법이 있겠지?

#ifndef __GPIO__
#define __GPIO__

#include <stdio.h>
/**
 * @brief 전역변수 gpio 변수 설정
 * 
 * @param gpio_val 설정할 값
 */
void set_gpio_val(int gpio_val);

/**
 * @brief 전역변수 gpio 값을 반환
 * 
 * @return int gpio값
 */
int get_gpio_val();

/**
 * @brief 전역변수 gpio의 주소 값을 반환
 * 
 * @return void* (void 포인터는 자료형이 없는 포인터로 생각) gpio 주소 값값
 */
void *get_address_gpio();

#endif


#include "gpio.h"

int gpio;

void *get_address_gpio()
{
    return &gpio;
}

int get_gpio_val()
{
    return gpio;
}

void set_gpio_val(int gpio_val)
{
    gpio = gpio_val;
}


만약 여러 스레드를 사용중이라면 세마포어를 걸어줘야해





2. makefile


이전 강좌에서는 gcc와 arm-linux-gnueabihf를 사용해서 컴파일하여 바이너리 파일을 실행했었지


이번에는 이런 과정이 파일이 많아지게 되면 어떻게 되는지, 그리고 이것을 자동화 하려면 어떻게 해야하는지 알아볼거야


gcc -o main main.c
#or
arm-linux-gnueabihf-gcc -o main main.c

gcc를 사용하기 해서 다음과 같은 과정을 거쳐서 했었지?


그런데 만약에 파일이 많아지게 되면 어떻게 될까? 이 과정을 makefile로 간단하게 해보자


작업 디렉토리를 만들고

cd ~/source_code/
mkdir gpio
cd gpio
touch main.c gpio.c gpio.h main.h

대충 이렇게 코딩해두자

//main.c
#include <stdio.h>
#include "gpio.h"

int main()
{
    printf_test();
}


//gpio.h
#ifndef __GPIO__
#define __GPIO__

#include <stdio.h>

void printf_test();

#endif


//gpio.c
#include "gpio.h"

int gpio;

void printf_test()
{
    printf("hellow world!\n");
}

gpio.c gpio.h main.c main.h가 있는 경우

gcc -o main main.c gpio.c

이렇게 명령어를 쳐야하지.  


원래 컴파일을 하려면 gcc는 오브젝트 파일을 기반으로 해서 만들어


gcc가 c 파일을 컴파일하고, 이를 바탕으로 오브젝트 파일을 만들고 이를 링킹하여(오브젝트 파일을 묶어서) main이라는 바이너리 파일을 만들지.


gcc가 이를 자동화 하지만 이를 직접 살펴보자.


이 과정을 보면 위 그림처럼 오브젝트 파일을 바탕으로 main이라는 바이너리 파일을 생성하지


gcc -o main.o -c main.c
gcc -o gpio.o -c gpio.c
gcc -o main main.o gpio.o

gcc로 수동 빌드해본게 위 코드야. 이것을 실행하면 잘 hellow world를 찍을거야


make 파일로 위 과정을 간략화해서 봐보자고

[빌드할 타겟]:[의존 파일]
    bash 명령어

여기서 만약 우리가 main.o를 빌드하려고 하면 main.c가 필요하지


타겟 : main.o, 의존 파일 : main.c


따라서 make 파일 규칙대로 적어주면

main.o:main.c
    gcc -o main.o -c main.c


이렇게 되면 우리는 main.o라는 오브젝트 파일을 main.c라는 애를 컴파일해서 만든것이 돼


따라서 위의 gcc로 만드는 과정을 전체 기술하면

all: gpio.o main.o
	gcc -o main main.o gpio.o

gpio.o: gpio.c
	gcc -o gpio.o -c gpio.c

main.o: main.c
	gcc -o main.o -c main.c

clean:
	rm -rf *.o main


clean은 'make clean' 명령어를 실행 했을 때 main과 오브젝트 파일을 지우기 위해 작성해


만약 main.o만 따로 빌드하고 싶어 그러면 'make main.o'를 치면 main.o만 빌드해


근데 이렇게 하드 코딩된게 지저분해 보이지 않니?


또 파일이 늘어났을 때마다 오브젝트 파일을 타겟으로 적어줘야 하니 매우 불편하지


그래서 몇가지 makefile의 변수를 사용해서 깔끔하게 만들어 줄 수 있어


CC: C 컴파일러 뭐 쓸건지

예) ([gcc], [arm-linux-gnueabihf])


CFLAGS: C 컴파일러 옵션

예) ([-Wunused] : 사용되지 않은 변수 경고처리)


LDFLAGS: 링커 옵션, 링크 패스 지정

예) ([-L/path/library] : 라이브러리 링크가 있는 경로 지정, [-lrt] : 리얼 타임 라이브러리 링크, [-lpthread] : pthread 라이브러리 링크)


CPPFLAGS: C 전처리기 옵션

예)([-D] : #define 같은 역할, [-I/path/include] : 인클루드 패스)


makefile 변수 저장하려면

[변수명]=[넣을 값]


makefile의 변수를 불러오려면

$([변수명]) 이런식이야


OBJ에는 만들 오브젝트 파일을, TARGET에는 빌드할 타겟을 넣어주지.


또 여기서 사용하는 인수에 대해 간략하게 설명할게 이 정도만 이해하자

$@ : 타겟 (main.o)

$^ : 의존성 파일 (main.c)

$< : 의존성 파일의 첫번째 (main.c)

$*: 확장자를 제외한 타겟의 이름 (타겟 이름이 main.o였으면 main임)


위 변수를 이용해서 makefile을 작성하면

CC=gcc
CFLAGS=-Wunused
LDFLAGS=-lrt -lpthread
CPPFLAGS=
OBJ=main.o gpio.o
TARGET=main

$(TARGET) : $(OBJ)
	$(CC) -o $@ $^ $(CPPFLAGS) $(LDFLAGS) $(CFLAGS)

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

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


%.o : %.c가 자동으로 OBJ있는 파일을 바탕으로 *.c 파일을 컴파일해서 오브젝트 파일을 뱉어줄거야

자 이렇게 해서 make를 단순화 할 수 있지, 하지만 makefile을 매번 수정하기 번거로울 때는 변수를 compile.sh이라는 쉘스크립트를 만들어서 변수를 빼고 선언해


makefile

$(TARGET) : $(OBJ)
	$(CC) -o $@ $^ $(CPPFLAGS) $(LDFLAGS) $(CFLAGS)

%.o: %.c
	$(CC) -o $@ -c $^ $(CFLAGS) $(LDFLAGS) $(CPPFLAGS)

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


compile.sh

export CC="gcc"
export CFLAGS="-Wunused"
export LDFLAGS="-lrt -lpthread"
export CPPFLAGS=""
export OBJ="main.o gpio.o"
export TARGET="main"

make -j

chmod +x compile.sh하고 실행하면 될거야


번외)

위 오브젝트 파일을 돌리는 과정은 멀티스레드로 진행돼.


그렇기 때문에 빌드가 상당히 빠르고, 소스코드가 바뀐 파일만 컴파일해서 효율적이지


그러나 이제 컴퓨터 스펙이 좋아졌기 때문에 c 파일이 엄청 많아진다 해도 c 컴파일하는데 많은 시간이 걸리지는 않지.


어플단에 성능에 영향을 주는 것도 아니고


따라서 귀찮으면 그냥 이렇게 makefile과 compile.sh를 작성해도 괜찮아.


어짜피 gcc가 알아서 오브젝트 파일을 컴파일 하거든


makefile

all :
	$(CC) -o $(TARGET) $(SRC) $(CPPFLAGS) $(LDFLAGS) $(CFLAGS)

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


compile.sh

export CC="gcc"
export CFLAGS="-Wunused"
export LDFLAGS="-lrt -lpthread"
export CPPFLAGS=""
export SRC="main.c gpio.c"
export TARGET="main"

make -j

댓글

이 블로그의 인기 게시물

[ BBB - yocto (9) ] 장치트리(DEVICE TREE)

[ BBB - yocto (11) ] uart