gcc로 컴파일 하는 방법
1. gcc 동작 과정
gcc 란?
원래는 GNU C Compiler를 의미 했지만 1999년부터 GNU Compiler Collection을 의미한다.
gcc가 실행시키는 프로그램
- 전처리기 : cpp
- 컴파일러 : cc1
- 어셈블러 : as
- 링커 : ld
단계별 처리 방법
1) 전처리 단계
소스 파일(file.c)에 gcc를 동작시키면 가장 먼저 전처리기인 cpp가 동작한다. cpp는 소스 파일의 #include와 #define과 같은 #으로 시작되는 전처리기 부분을 처리 한다. 즉, 필요한 헤더 파일을 삽입하고 실행 문장의 매크로를 상수로 변환한다. 소스 파일 file.c가 전처리기를 거치면 file.i라는 이름의 파일이 생성되지만 디스크에는 저장되지 않는다.
2) 컴파일 단계
컴파일러가 전처리된 파일(file.i)로부터 어셈블리어로 된 파일(file.s)을 생성한다. 그런데 일반적으로 다음 단계인 어셈블 단계를 바로 실행하므로 file.s 파일은 디스크에 저장되지 않는다.
3) 어셈블 단계
어셈블리어로 된 파일(file.s)을 기계가 직접 이해할 수 있는 기계어로 된 오브젝트 파일(file.o)로 변환한다.
4) 링크 단계
오브젝트 파일(file.o)은 printf, scanf와 같은 라이브러리 함수에 해당하는 코드가 없기 때문에 실행될 수 없다. 또한 여러 파일로 이루어진 프로그램의 경우에도 파일 간에 연결이 이루어지지 않아 실행될 수 없는데, 이러한 라이브러리 함수와 오브젝트파일들을 연결해 실행 파일을 생성하는 단계가 링크 단계이다.
컴파일을 할 때는 각 단계별로 옵션을 사용해 얼마나 진행되었는지 그 상황을 볼 수 있게 수동으로 제어할 수 있는데, 이에 대한 자세한 내용은 gcc 옵션에서 다룬다.
5) 파일 확장자에 따른 처리 방법
gcc는 파일 확장자에 따라 다음과 같이 처리 방법을 달리하는데, 한 가지만 설명하면 확장자가 .c인 경우 gcc로 전처리기, 컴파일, 어셈블, 링크 과정을 거쳐야 실행 파일이 완성된다는 것이다.
확장자 | 종류 | 처리방법 |
---|---|---|
.c | C 소스 파일 | gcc로 전처리, 컴파일, 어셈블, 링크 |
.C, .CC | C++ 소스 파일 | g++로 전처리, 컴파일, 어셈블, 링크 |
.i | 전처리된 C 파일 | gcc로 컴파일, 어셈블, 링크 |
.ii | 전처리된 C++ 파일 | g++로 컴파일, 어셈블, 링크 |
.s | 어셈블리어로 된 파일 | 어셈블, 링크 |
.S | 어셈블리어로 된 파일 | 전처리, 어셈블, 링크 |
.o | 오브젝트 파일 | 링크 |
.a, .so | 컴파일된 라이브러리 파일 | 링크 |
2. gcc 실행하기
gcc를 이용해 컴파일 하는 방법은 다음과 같습니다.
$ gcc 소스파일 이름
gcc를 이용해 컴파일에 성공하면 a.out이라는 파일이 생성된 것을 확인할 수 있는데 여기서 그냥 명령어로 a.out을 입력하면 명령어를 발견할 수 없다는 오류 메시지가 나온다. 이는 a.out가 저장된 디렉토리를 path로 설정하지 않았기 때문이다. path로 설정되지 않는 디렉토리에 있는 명령어는 디렉토리 위치를 지정해주지 않는 한 실행되지 않는다. 그러므로 a.out를 실행하려면 다음과 같이 해야 한다.
$ ./a.out
즉, 프로그램 이름 앞에 디렉토리 위치 정보인 ./를 추가해야 현재 디렉토리에 있는 명령어가 실행되는데, 여기서 마침표(.)는 현재 디렉토리를 의미하고, 슬래시(/)는 디렉토리를 구분하는 문자다.
3. gcc 옵션
옵션 | 의미 |
-E | 전처리를 실행하고 컴파일을 중단하게 한다. |
-c | 소스 파일을 컴파일만 하고 링크를 수행하지 않으며, 오브젝트 파일을 생성한다. |
-o | 바이너리 형식의 출력 파일 이름을 지정하는데, 지정하지 않으면 a.out라는 기본 이름이 적용된다. |
-I | 헤더 파일을 검색하는 디렉토리 목록을 추가한다. |
-L | 라이브러리 파일을 검색하는 디렉토리 목록을 추가한다. |
-l | 라이브러리 파일을 컴파일 시 링크한다. |
-g | 바이너리 파일에 표준 디버깅 정보를 포함시킨다. |
-ggdb | 바이너리 파일에 GNU 디버거인 gdb만이 이해할 수 있는 많은 디버깅 정보를 포함시킨다. |
-O | 컴파일 코드를 최적화시킨다. |
-ON | 최적화 N 단계를 지정한다. |
-DFOO=RAR | 명령라인에서 BAR의 값을 가지는 FOO라는 선행 처리기 매크로를 정의한다. |
-static | 정적 라이브러리에 링크한다. |
-ansi | 표준과 충돌하는 GNU 확장안을 취소하며, ANSI/ISO C 표준을 지원한다. 이 옵션은 ANSI 호환 코드를 보장하지 않는다. |
-traditional | 과서 스타일의 함수 정의 형식과 같이 전통적인 K&R(Kernighan and Ritchie) C 언어 형식을 지원한다. |
-MM | make 호환의 의존성 목록을 출력한다. |
-V | 컴파일의 각 단계에서 사용되는 명령어를 보여준다. |
1) 자주 사용되는 옵션
-o 옵션
C 소스 코드를 컴파일 할 때 생성되는 출력 파일 이름을 지정하는 옵션으로 사용법은 다음과 같다.
# -o 옵션 기능 : 생성되는 출력 파일 이름을 지정한다.
$ gcc -o 출력파일이름 소스파일이름
$ gcc -o file file.c 또는 $ gcc file.c -o file을 입력한다. 그럼 출력파일이 file이라는 이름으로 생성이 된다.
이를 실행하려면 $ ./file 이라고 실행하면 된다. -o 옵션을 생략하고 컴파일을 하면 실행 파일 이름은 a.out가 된다.
그런데 이럴 경우 두 가지 다른 소스를 차례로 컴파일할 때 먼저 생성된 실행 파일 a.out를 나중에 생성된 a.out가 경고없이 덮어쓰므로 주의해야 한다.
-E 옵션
컴파일의 첫 단계인 전처리까지만 실행한 결과를 화면에 출력한다.
# -E 옵션 기능 : 전처리까지만 실행하고 결과를 화면에 출력한다.
$ gcc -E 소스파일이름
$ gcc -E file.c로 컴파일을 하면 매우 방대한 내용이 소스파일 위에 붙는 것을 확인할 수 있다. 이처럼 -E 옵션만 주면 전처리된 결과가 화면에만 출력되고 파일로 저장되지는 않는다. 그러므로 파일을 저장하려면 -o 옵션을 함께 주어야 하고, 그러면 file.i라는 전처리된 파일이 디스크에 저장된다.
-c 옵션
-c 옵션은 전처리, 컴파일, 어셈블까지 실행하여 오브젝트 파일(.o)을 생성한다.
# -c 옵션 기능 : 전처리, 컴파일, 어셈블까지 실행하여 오브젝트 파일을 생성한다.
$ gcc -c 소스파일이름
file.c를 -c 옵션을 사용해 컴파일 하면 file.o라는 오브젝트 파일이 생성된다. 즉, -c 옵션을 주어 ‘소스파일이름.c'를 컴파일하면 오브젝트 파일 이름은 ’소스파일이름.o'가 된다.
$ gcc -c file.c
그리고 이 오브젝트 파일을 이용해 실행 파일을 생성하려면 다음과 같이 gcc를 이용하면 된다.
$ gcc file.o
그럼 a.out이라는 기본 출력 파일이 생성된다. 여기서 file 이라는 이름의 실행파일을 생성하려면 -o 옵션만 주면 된다.
$ gcc file.o -o file
-c 옵션은 하나의 프로그램을 여러 파일로 분리해 작성한 다음, 함께 컴파일 하는 분리 컴파일 시 많이 사용된다. 예를 들어 main.c와 hi.c 두 개의 소스로 구성된 프로그램을 살펴보자.
// main.c
extern void hi();
main() {
hi();
}
// hi.c
#include <stdio.h>
void hi() {
printf("Hi\n");
}
이 둘을 함께 컴파일 하는 방법은 다음과 같으며, 이를 분리 컴파일이라 한다.
$ gcc main.c hi.c -o test
$ ./test Hi
만약 다음과 같이 파일별로 컴파일 하면 컴파일 오류가 발생하는데, main.c 파일에 대한 오류는 호출하는 함수 hi라는 함수가 정의되지 않았기 때문이고, hi.c 파일에대한 오류는 main 함수가 없기 때문이다.
# 오류가 난다. 오류의 종류는 생략한다.
$ gcc main.c -o test
# 오류가 난다. 오류의 종류는 생략한다.
$ gcc hi.c -o hi.c
그러나 다음과 같이 각 파일별로 오브젝트 파일을 만들고 나중에 링크하는 것은 가능하다.
$ gcc -c main.c $ gcc -c hi.c
$ gcc main.o hi.o -o test
이런 식으로 분리 컴파일을 하게 되면 만일 hi.c 파일이 수정되면 main.c와 hi.c를 모두 컴파일할 필요 없이 hi.c 파일에 대한 오브젝트 파일만을 생성하고 링크하면 원하는 실행 파일이 생성된다.
# hi.c를 수정하고 나서 다시 컴파일
$ gcc -c hi.c
$ gcc main.o hi.o -o test
-I 옵션
-I 옵션은 C 소스가 표준 디렉토리가 아닌 위치에 있는 헤더 파일을 가질 때 그 디렉토리 위치를 지정해준다.
-I 옵션 기능 : 표준 디렉토리가 아닌 위치에 있는 헤더 파일의 디렉토리를 지정한다.
$ gcc 소스파일이름 -I디렉토리 이름
먼저 myheader.h 파일이 age.c 파일이 있는 디렉토리의 하위 디렉토리인 mydir에 있다고 하자.
// age.c
#include <stdio.h>
#include "myheader.h"
main() {
printf("%d\n", AGE);
}
// myheader.h
#define AGE 20
age.c 파일을 컴파일하면 다음과 같이 myheader.h 파일이 없다는 메시지의 컴파일 오류가 발생한다.
# 컴파일 오류가 발생한다.
$ gcc age.c
myheader.h 파일이 표준 디렉토리에 없기 때문에 오류가 발생하는 것이다. 이와 같이 표준 디렉토리가 아닌 디렉토리에 있는 헤더 파일을 이용하려면 -I 옵션으로 디렉토리 위치를 지정해야 한다. 즉, myheader.h 파일이 있는 mydir 디렉토리를 다음과 같이 지정해 컴파일하면 성공한다.
$ gcc age.c -Imydir
$ ./a.out 20
2) 라이브러리 지정 옵션
라이브러리란?
자주 사용되는 유용한 함수에 대한 오브젝트 파일을 모아둔 것이 라이브러리로, 사실 라이브러리에는 함수 목록(index)도 포함된다.
시스템에서 제공하는 라이브러리는 /usr/lib 디렉토리에 있으니 확인해보면 된다. 아주 많은 라이브러리가 있는데, 이름은 lib로 시작하고 ar 명령어에 의해 생성되므로 확장자는 .a다.
이 중에서 libc.a는 표준 라이브러리고 libm.a는 수치 연산 라이브러리다. ar 명령어를 이용하면 libc.a가 어떠한 오브젝트 파일로 이루어졌는지 알 수 있다. ar 명령어의 t 옵션은 .a 파일의 내용을 표시해주는 것이다.
$ ar t libc.a
라이브러리를 직접 만들어 보기 위해 우선 다음과 같이 plus.c와 minus.c 두 개의 파일을 mylib 디렉토리에 만들자.
// plus.c
int plus(int x, int y) {
return x+y;
}
// minus.c
int minus(int x, int y) {
return x-y;
}
plus.c와 minus.c 파일을 생성했으면 이들 파일에 대한 오브젝트 파일인 plus.o와 minus.o를 다음과 같이 생성한다.
$ gcc -c plus.c minus.c
$ ls minus.c minus.o plus.c plus.o
그러면 라이브러리 파일을 생성할 준비가 끝났다. 다음과 같이 ar 명령어를 이용하면 plus.o와 minus.o에 대한 libmy.a가 생성되는데, r옵션은 .a 파일을 생성한다.
$ ar r libmy.a plus.o minus.o
그리고 라이브러리 파일에 목록을 추가해야 하는데, 다음과 같이 s 옵션을 주어 ar 명령어를 실행하면 라이브러리 파일이 생성된다.
$ ar s libmy.a
-l 옵션
-l 옵션은 표준 라이브러리가 아닌 라이브러리를 사용하고자 할 때 그 라이브러리를 지정해준다.
# -l 옵션 기능 : 표준 라이브러리가 아닌 라이브러리를 지정한다.
$ gcc 소스파일이름 -l라이브러리이름
그럼 -l 옵션의 예를 살펴보면 다음과 같다.
// test1.c
#include <stdio.h>
#include <math.h>
main() {
//pow(x, y)는 x^y를 구하는 함수
printf("%g\n", pow(2, 3));
}
# 오류가 난다.
$ gcc test1.c
수치 연산 라이브러리의 이름은 libm.a다. 그러므로 이 라이브러리 이름을 -l 옵션 뒤에 지정해야 하는데, lib와 .a를 제외한 m만 쓴다.
즉, 다음과 같이 설정하고 컴파일하면 성공한다.
$ gcc test1.c -lm
$ ./a.out
이제 앞서 직접 만든 libmy.a는 표준 라이브러리가 아니므로 -l 옵션을 주어 지정해야 한다.
// test2.c
#include <stdio.h>
int plus(int x, int y)
int minus(int x, int y)
main() {
printf("%d %d\n", plus(2, 3), minus(2, 3));
}
# 컴파일 오류가 난다.
$ gcc test2.c -lmy
오류가 발생하는 이유는 링커인 ld가 라이브러리를 찾을 때 /lib, /usr/lib와 같이 정해진 디렉토리만 찾기 때문이다. libmy.a 라이브러리는 현재 작업 디렉토리의 하위 디렉토리인 mylib에 있으므로 링커가 찾지 못하는데, 이는 -L 옵션을 사용해 해결할 수 있다.
-L 옵션
-L 옵션은 사용할 라이브러리의 위치를 지정해주므로 사용자가 라이브러리 파일을 직접 만들어 사용하거나 새 라이브러리를 내려 받아 사용할 때 이용된다.
# -L 옵션 기능 : 사용할 라이브러리의 위치를 지정한다.
$ gcc 소스파일이름 -L라이브러리위치
컴파일 오류가 발생했던 test2.c를 다음과 같이 -L 옵션을 주어 libmy.a 라이브러리가 있는 디렉토리인 mylib를 지정하면 성공적으로 컴파일이 된다.
$ gcc test2.c -lmy -Lmylib
$ ./a.out
3) 디버깅 관련 옵션
# -g, -ggdb 옵션 기능 : 디버깅 정보를 삽입한다.
$ gcc -g 소스파일이름 gcc -ggdb 소스파일이름
-g 옵션에는 실행파일에 삽입될 디버깅 정보의 양에 따라 -g1, -g2, -g3와 같이 세 가지 단계가 있는데, 숫자 없이 -g 옵션을 주면 기본적으로 -g2의 디버깅 정보가 삽입된다.
-g 옵션의 단계
옵션 | 의미 |
-g1 | 역추적 스택 덤프 생성에 필요한 정보를 포함하지만 지역변수, 문장 번호를 위한 디버깅 정보는 삽입하지 않는다. |
-g2 | 확장 기호 테이블, 문장 번호, 지역과 외부 변수에 대한 디버깅 정보를 삽입한다. |
-g3 | -g2 옵션의 디버깅 정보와 모든 매크로 정의를 삽입한다. |
-g 옵션 없이 컴파일 할때와 -g 옵션을 주었을 때 생성된 실행 파일의 크기가 훨씬 커지는데, 그 이유는 디버깅 정보가 추가되었기 때문이다.
-ggdb 옵션도 -g와 거의 유사하나 특별히 다른 점이 있다면 디버깅 작업을 도와주는 추가 정보가 필요하고, gdb 외의 다른 종류의 디버거에서는 사용이 불가능하다는 것이다.
-g, -ggdb 옵션에 의한 디버킹 코드 삽입은 파일의 크기를 크게하기 때문에 최종 실행 파일을 생성할 때는 이러한 옵션을 주지 말고 컴파일하는 것이 바람직하다. 또 최적화된 코드는 디버깅을 어렵게 만들기 때문에 최적화 수행 전에 디버깅을 하는 것이 좋다.
4) 최적화 옵션
성능을 개선시키기 위해 코드를 최적화하면 불필요하거나 비효율적인 계산 과정이 효율적 게산 과정으로 대체되어 코드의 크기와 실행 시간을 줄일 수 있다. 대신, 컴파일 시간이 늘고 컴파일 과정에서 메모리 사용량이 늘어나는 단점이 있다.
# -O 옵션 기능 : 코드를 최적화 시킨다.
$ gcc -O 소스파일이름
-O 옵션은 -ON(숫자)을 써줌으로써 최적화 단계를 구분할 수 있는데, N 값은 gcc 버전마다 차이가 나며 값이 커질수록 더욱 최적화된 코드가 나온다. 일반적으로, -O1, -O2를 많이 사용하며, -O1, -O2, -O3에 의한 최적화 내용은 다음과 같다.
옵션 | 의미 |
-O1 | -O 옵션과 같은 단계의 옵션으로 최소한의 스레드 분기 동작 횟수를 줄이고, 호출된 각 함수 반환 시 스택에 인수를 모아 두었다 동시에 꺼내게 해준다. |
-O2 | -O1 단계의 최적화와 함께 프로세서가 다른 명령어의 결과나 캐시 메모리 또는 메모리의 데이터를 기다리는 동안 컴파일러가 다른 명령어를 실행하도록 한다. 컴파일 시간이 더 오래 걸리지만, 수정된 코드는 더 최적화되어 실행이 빨라진다. |
-O3 | -O2 단계의 모든 최적화와 루프 해체, 그 밖의 프로세서 전용 특징을 포함한다. |