728x90
반응형
[프로그램 빌드 도구]
전처리기 (Preprocessor)
- 소스 코드에서 #include, #define 등과 같은 전처리 지시어를 처리하고, 소스 파일을 전처리된 상태로 만든다.
- 전처리 결과는 전처리된 소스 파일로 나타난다.
컴파일러 (Compiler)
- 전처리된 소스 파일을 어휘 분석, 구문 분석, 의미 분석을 통해 어셈블리 언어(Assembly Code)로 변환한다.
- 이 과정에서 소스 코드가 어셈블리 코드로 변환된다.
어셈블러 (Assembler)
- 어셈블리 코드를 기계어(Machine Code)로 변환하고, 각 소스 파일마다 오브젝트 파일(Object File)을 생성한다.
- 이 파일은 CPU가 직접 이해할 수 있는 기계어 코드와 메타 데이터를 포함하고 있다. (확장자 .obj 또는 .o)
링커 (Linker)
- 여러 개의 오브젝트 파일(보통 소스 파일마다 하나의 오브젝트 파일이 생성됨)을 링킹하여 하나의 실행 파일을 생성한다.
- 이 과정에서 함수와 변수에 대한 외부 참조(External Reference)를 해결하고, 여러 오브젝트 파일을 결합하여 하나의 실행 가능한 프로그램으로 만든다.
[프로그램 빌드 과정]
1. 전처리 단계(전처리기)
전처리 단계에서는 주로 헤더 파일을 포함시키고, 매크로를 처리하며, 코드의 조건부 컴파일 등을 수행한다.
전처리기는 주로 다음과 같은 작업을 한다.
- 헤더 파일 포함 처리 (#include):
- #include 지시어를 만나면, 전처리기가 해당 헤더 파일의 내용을 소스 코드에 복사하여 붙여넣는다.
- 예를 들어, #include <stdio.h>라는 코드가 있을 때, stdio.h 헤더 파일의 모든 내용이 해당 위치에 복사된다.
- 헤더 파일에는 보통 함수 선언(프로토타입), 매크로 정의, 전역 변수 선언 등이 포함되어 있다.
- 즉, 전처리 단계에서 헤더 파일의 내용은 소스 코드에 실제 텍스트 형태로 대체되기 때문에, 전처리 후에는 모든 헤더 파일의 내용이 소스 파일에 합쳐진 상태가 된다.
- 매크로 처리 (#define):
- #define으로 정의된 매크로가 있을 때, 전처리기는 해당 매크로를 소스 코드에서 정의된 값으로 대체한다.
- 예를 들어, #define PI 3.14가 있으면, PI가 등장하는 모든 부분을 3.14로 대체한다.
- 조건부 컴파일 (#ifdef, #ifndef):
- #ifdef, #ifndef 같은 조건부 컴파일 지시어는 특정 조건에 따라 코드의 일부를 포함하거나 제외할 수 있도록 한다.
- 전처리기는 이 조건을 평가하여, 조건에 맞는 코드만 소스 코드에 포함한다.
전처리 후의 결과
- 전처리기가 모든 #include, #define 등을 처리하고 나면, 하나의 소스 파일이 만들어지게 된다.
- 이 소스 파일은 헤더 파일(.h) 의 내용이 모두 포함되어 있으며, 모든 매크로와 조건부 컴파일이 처리된 최종 소스 파일의 형태를 갖는다.
- 따라서, 전처리 후에는 헤더 파일이 따로 필요하지 않다.
- C++ 같은 언어의 경우 파일 분할&사용을 잘 고려하지 않으면(필요없는 헤더파일 포함 등) 전처리 단계에서 소스코드의 코드량이 너무 커져버려 용량이 늘어나고 성능의 이슈가 될 수 있다.
2. 컴파일 단계(컴파일러, 어셈블러) → 오브젝트 파일 생성
- 전처리된 소스 파일을 컴파일하면, 각 소스 파일(ex .c, .cpp)은 오브젝트 파일(.o, .obj)로 변환된다.
- 이 오브젝트 파일에는 소스 코드의 기계어 버전과, 외부 참조(External References)에 대한 정보가 포함된다.
- 외부 참조란, 다른 소스 파일에서 정의된 함수나 변수를 참조할 때 발생하는 것으로, 해당 함수나 변수의 메모리 위치는 아직 할당되지 않은 상태다.
3. 링킹 단계(링커)
링킹 단계에서는 여러 오브젝트 파일을 결합하여 하나의 실행 파일을 만든다.
링커는 다음과 같은 작업을 수행한다.
- 외부 참조 해결:
- 링커는 각 오브젝트 파일에서 사용된 외부 참조를 해결한다.
- 예를 들어, main.c 파일에서 util.c의 void func() 함수를 호출한다고 가정해보자. 컴파일 시 main.o는 func()에 대한 외부 참조를 가지고 있지만, 함수의 실제 정의는 없다.
- 링커는 main.o와 util.o를 결합하여 func()의 실제 정의를 찾아서 참조를 해결하고, 두 파일을 연결해준다.
- 라이브러리 파일 연결:
- 링커는 정적 라이브러리(.lib 또는 .a 파일)나 동적 라이브러리(.dll, .so 파일)와 연결하여, 오브젝트 파일이 필요로 하는 외부 참조를 라이브러리에서 찾고 이를 결합한다.
- 예를 들어, printf() 함수는 stdio.h 헤더 파일에 선언되어 있지만, 실제 printf()의 정의는 libc 라이브러리 파일에 있다.
- 링커는 printf()에 대한 참조를 해결하기 위해 libc 라이브러리를 참조하여 printf()의 기계어 코드를 포함시키거나, 동적 라이브러리의 주소를 참조하도록 한다.
- 최종 실행 파일 생성:
- 모든 오브젝트 파일과 라이브러리가 결합된 후, 링커는 최종적으로 운영체제가 실행할 수 있는 하나의 실행 파일(Executable File)을 생성한다.
- 이 실행 파일은 운영체제가 직접 로드하고 실행할 수 있는 형태로, 모든 참조가 해결된 상태이다.
- 단순히 기계어 코드뿐만 아니라, 프로그램이 실행되기 위해 필요한 메타 데이터와 시작 주소, 라이브러리 참조 정보 등을 포함한다.
실행 파일의 형태:
- 최종 실행 파일은 보통 다음과 같은 형태를 가진다:
- Windows: .exe 확장자의 실행 파일이 생성된다.
- Linux/Unix: .out, .elf 또는 확장자가 없는 실행 파일이 생성된다.
- macOS: .app 또는 실행 파일 형태로 생성된다.
[리눅스를 통한 빌드 과정 예시]
소스코드
- main.c
- util.c
1. 컴파일:
gcc -c main.c -o main.o
gcc -c util.c -o util.o
- 이 명령어는 각각 main.c와 util.c를 컴파일하여 main.o와 util.o 오브젝트 파일을 생성한다.
2. 링킹:
gcc main.o util.o -o my_program
- 이 명령어는 main.o와 util.o를 결합하여 my_program이라는 실행 파일을 생성한다.
추가로 Makefile을 작성하면, 저 과정들(컴파일, 링킹 등)을 Makefile 안에 정의해두고 make 명령어만으로 한 번에 빌드할 수 있다.
Makefile을 사용하면 반복적인 컴파일 및 빌드 작업을 자동화하고, 소스 파일이 많을 때 효율적으로 관리할 수 있다.
반응형