gcc로 빌드하는 과정을 용이하게 해주는 Makefile

HiroPark·2023년 3월 31일
0

Jungle

목록 보기
5/10

1번 테스트를 통과하기 위한 코드를 짰는데, 분명히 맞게짰는데 테스트가 통과가 안됨.

rbtree *new_rbtree(void) {
  rbtree *p = (rbtree *)calloc(1, sizeof(rbtree));
  // TODO: initialize struct if needed
  node_t *newNode = (node_t *)calloc(1, sizeof(node_t));
  newNode->color = RBTREE_BLACK; // 루트니까 블랙으로 초기화 
  => 여기!!!!!!!
  p->nil  = newNode;
  p->root = newNode;
  return p;
}

gdb 디버거 사용법을 익혀가지고 해당 메서드를 디버깅해봤는데, 오잉? 여기!!!라고 표시된 곳에서 코드가 더 이상 진행되지 않고 끝나버림.

알고보니 make에 대한 무지에서 비롯된 거였음.

src/Makefile

.PHONY: clean 
# clean파일이 실제로 존재하지 않아도, make clean명령어를 실행 가능 

CFLAGS=-Wall -g

driver: driver.o rbtree.o # driver.o와 rbtree.o를 링크하여 driver라는 실행파일 만들어냄

clean: # make clean명령어를 실행시, driver와 .o 파일을 삭제
	rm -f driver *.o

test/Makefile

.PHONY: test # 타겟으로 작성한 이름과, 디렉토리 내에 파일 이름이 서로 같을 경우, 오작동을 일으키지 않기 위해 .PHONY 사용

CFLAGS=-I ../src -Wall -g -DSENTINEL 
# -I : include 디렉토리 지정 , 상위 디렉토리의 src 디렉토리를 포함시킴
# -Wall : 모든 경고를 표시
# -g : 디버깅 정보를 포함
# -DSENTINEL : SENTINEL이라는 매크로 상수를 정의

test: test-rbtree # test-rbtree를 실행하고, valgrind로 실행결과 검사
	./test-rbtree
	valgrind ./test-rbtree

test-rbtree: test-rbtree.o ../src/rbtree.o # test-rbtree는 test-rbtree.0../src/rbtree.o를 링크하여 실행파일 만듬 

../src/rbtree.o: # 위의 두개와 달리 명령어 타겟이 아니라, 명령어 실행시 의존성 파일을 생성하는 룰
	$(MAKE) -C ../src rbtree.o
	# ../src폴더에서 Makefile찾아서 rbtree.o를 빌드함

clean:
	rm -f test-rbtree *.o # makefile에서 생성된 파일들을 삭제 

과제 설명란에서 그냥 make test로 돌려서 테스트 통과하면 됩니다~ 라고 써있길래 그러려니 하고 넘겼는데, make test는

test: test-rbtree # test-rbtree를 실행하고, valgrind로 실행결과 검사
	./test-rbtree
	valgrind ./test-rbtree

요 라인의 "test"라는 타겟을 실행하기 위한 아이였음.

얘가 바로 밑의 "test-rbtree" 타겟을 실행하여 실행파일을 만들고, 해당 실행파일을 실행한 뒤, valgrind로 메모리 누수등을 체크하는 역할을 함.

근데 이렇게 되면, 이미 존재하는 test-rbtree.o와 src안의 rbtree.o를 가지고 실행파일을 만드는데, 이때 ../src/rbtree.o 의 경우 이미 있는 목적파일을 가져다 쓰기 때문에 이를 갱신해줄 필요가 있다고 느꼈습니다.(확실치 않음)

그래서 src로 가서 make clean으로 기존 목적파일을 삭제하고, "make"명령어로 실행파일을 다시 만들어줌(첫번째 타겟인 driver를 실행)

마찬가지로 test에서도 make clean과 make를 거쳐서 실행파일을 최신 버전으로 업데이트 해줌.
이후 테스트를 돌리니 성공.

이렇게 된거, 앞으로 계속 c와 gcc를 사용하게 될 건데, 공부가 필요하겠다 싶어서 이를 정리해보았다.

gcc

- GNU C Complier

텍스트 에디터로 ~.c 파일을 만들고, 컴파일러를 통해 실행파일을 만듬.

컴파일 과정

one.c 파일이 있다고 가정

#include <stdio.h>
#define MAX 10
int main()
{
	int a = MAX;
    
    printf("Hello world\n");
    return 0;
}

1. Preprocessing

  • including header file in code , 여기서는 stdio.h
  • 주석을 제거
  • 프로그램 내의 매크로 - 여기서는 MAX - 를 이에 해당하는 값으로 변경

gcc -E one.c

  • -E 를 통해 "전처리 과정만 실행" 시킴

2. Compilation

컴파일러는 프로그램을 어셈블리언어 코드로 변경
gcc -S one.c

-S를 통해 전처리 과정과 컴파일만 거치게 함

근데 컴퓨터는 0과 1만 알아들으니까, 이 어셈블리를 바이너리 포맷으로 변경해야 함

3. Assembler
gcc one.c -o one.o

one.c를 가지고 one.o라는 바이너리 목적 파일을 만듬.

4. Linking
다른 라이브러리의 기능을 사용하기 때문에, 이러한 외부 기능을 최종 실행파일에 포함시키는 과정

gcc one.c

전처리, 컴파일링, 어셈블링, 링킹
을 모두 거쳐서 실행파일을 만듬(기본적으로 a.out)

CSAPP 1장의 요 과정을 그대로 gcc명령어로 제어할 수 있다.

출력파일의 이름을 바꾸고 싶다면 -o (output)옵션을 사용함.

gcc one.c -o newName

요렇게

  • Wall 옵션 : 컴파일시에 경고 사인을 보내주는 옵션.

  • -c 옵션: 전처리, 컴파일, 어셈블까지 실행하여 .o 파일을 생성
    - gcc -c file.c
    결과물은 file.o가 된다

  • 해당 file.o를 이용해 실행파일(a.out)을 생성하려면 gcc file.o 를 실행하면 된다.
    이 -c 옵션은 "분리 컴파일" 시 사용됨

#include <stdio.h>  

void hi() {
    printf("Hi\n"); 
}

이렇게 서로가 필요한 두 .c파일이 있을때

extern void hi();  
main() {
    hi(); 
}

gcc one.c two.c -o executable

one.c와 two.c를 함께 컴파일

이들 각 파일을 개별로 컴파일하면 오류가 발생하지만
gcc -c one.c | gcc - c two.c로 각 파일별로 오브젝트 파일을 만들고,
gcc one.o two.o -o test 의 방식으로
나중에 링크하는 것은 가능하다

  • I 옵션 : 표준 디렉토리가 아닌 위치에 있는 헤더파일의 디렉토리를 지정
    $ gcc 소스파일 이름 -I 디렉토리 이름

밑은 정리된 버전이다(출처)

-o [파일명] [*.c] : 지정한 파일명으로 실행 파일을 저장한다.
	ex) gcc -o result.out main.c

-E : 전처리 단계를 수행한 후, 컴파일 과정을 거치지 않는다.
			실행 결과는 standard output에 출력된다.
-S : 컴파일 단계를 수행한 후, 어셈블 과정을 거치지 않는다. 
			실행 결과로 어셈블리어로 변환된 *.s 파일이 생성된다.
-c [파일명] [*.c] : 소스 코드를 컴파일 또는 어셈블하며, 링크를 하지 않는다.
									파일명으로 오브젝트 파일을 생성한다.
									ex) gcc -c ft_isalnum.c
-I [디렉토리명] : 디렉토리명에서 헤더 파일을 검색한다.

-l [라이브러리] : 라이브러리 파일과 링크한다. 접미사나 확장자(.a/.o)가 없어도 링크한다.
								ex) 라이브러리 파일이 libmath.a 일때 다음과 같이 작성
										gcc myfile.c -lmath -o myfile
-L [디렉토리명] : 디렉토리 내에서 라이브러리 파일을 찾는다.
-D [매크로상수명]=[값] : 매크로 상수를 정의하기 위한 옵션이다.
	ex) gcc -D BUFFER_SIZE=42 : BUFFER_SIZE 라는 매크로 상수의 값을 42로 설정한다.

Makefile

해당강의를 참조함
makefile을 사용하는 이유는.
1. 반복되는 컴파일 작업이 지겹고 시간이 오래걸려서
2. 수정된 파일만 컴파일 할 수 있어서

간단한 값을 프린트하는 kor.c main.c usa.c 와, 이들에 삽입되는 main.h 헤더파일이 있다고 가정해보자.

gcc -o app.out kor.c main.c usa.c
이렇게 바로 목적파일을 만들거나 혹은

gcc -c main.c kor.c usa.c
gcc -o app.out main.o usa.o kor.o

이렇게 매번 목적파일까지 만들고, 이를 수동으로 링킹해줘야 한다면

  • 파일 하나가 늘면 해당 과정을 또 반복해야 함
  • 파일 하나가 바꼈을때 전체를 다 컴파일해줘야 함

그래서 Makefile을 사용함

구조

target : dependency
	command

target을 만들고자 하는데, dependency를 가지고 이를 만듬
command는 탭으로 구분을 하고, dependency들을 가지고 command를 실행하여 타겟을 만드는 것.

all : app.out


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
~                                                                                                               ~                         

- all : 최종적으로 만들고자 하는 것

all 옵션이 없는 경우, 제일 첫번째 타겟만 실행시키고 종료함

  • 그래서 원하는 것을 제일 처음에 넣던가, 아니면 명확하게 all로 명시를 해줘야 함

이를 좀더 예쁘게 만들어보자

  • 반복되는 애들을 환경변수로 빼주자!
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

$ (CC)를 보면 이를 gcc로 바꿔줌
$ (TARGET) 도 마찬가지(이를 TARGET과 같은 의미인 $ @ 로 바꿀수도 있다)
$ (OBJS)는 디펜던시를 나타내는 makefile의 내부 변수.(공통적으로 쓰는) (OBJS도 %^ 기호로 바꿀 수 있다)

근데, 파일이 하나 늘어날때마다

뭐뭐.o :
        $(CC) -c 뭐뭐.c

하기가 너무 귀찮음

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

all : $(TARGET)


$(TARGET) : $(OBJS)
        $(CC) -o $(TARGET) $(OBJS)

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

make하는 디렉토리 내에 있는 .c 들을 다 읽어서 다 목적코드인 .o로 바꿔주겠다는 뜻

CFLAGS

컴파일할때 옵션을 주는 것. -Wall -g 이런것.

LDFLAGS

링크할때 옵션을 주는 것.
-lc는 기본 c라이브러리.
$^는 $(OBJS)와 같은 의미

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

CLFAGS = -wall
LDFLAGS = -lc

all : $(TARGET)


$(TARGET) : $(OBJS)
        $(CC) $(LDFLAGS) -o $(TARGET) $^

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

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

자, 이미 make를 해놓은 상태에서 만약 main.c 가 변경된다면(파일이 가진 타임스탬프 값이 변경돼서 컴파일러가 이를 파악한다면) ,

make
gcc  -c -o main.o main.c
gcc -lc -o app.out main.o kor.o usa.o

이렇게 바뀐 파일만 컴파일을해서 다시 바이너라 파일만 만든다.

profile
https://de-vlog.tistory.com/ 이사중입니다

0개의 댓글