컴파일, 그리고 더 자세한 이야기

Junhyeok Yun·2022년 9월 28일
2

C++

목록 보기
1/5
post-thumbnail

오늘의 목표

오늘은 컴파일에 대한 자세한 정의와 그 과정을 알아볼 것이다.


컴파일의 정의

컴파일이란 고수준 언어(high-level programming language | C/C++, C#, Java ...)를 컴퓨터가 이해할 수 있는 기계어(low-level programming language | machine language)로 변환하는 작업

CPU는 이진수(binary code)를 사용하는 기계어밖에 이해하지 못하지만 기계어는 사람이 읽고 작성하기에는 매우 불편하다. 이 때문에 사람이 다루기 쉬울 뿐 아니라 다양한 자료구조나 알고리즘을 효율적으로 표현할 수 있는 고수준 언어가 생겨났고, 컴파일이라는 과정이 필요해진 것이다.


컴파일의 전반적인 과정

컴파일의 전반적인 과정을 보기 쉽게 그림으로 나타내면 다음과 같다.

이제부터 각 과정에 대해서 자세하게 알아보자.


1. 전처리(Pre-processing)

전처리기 구문(#으로 시작하는 구문)을 처리하고 주석을 제거한다.

전처리 과정(Pre-processing)은 전처리기(Pre-processor)를 통해 소스 코드 파일을 전처리 된 소스 코드 파일로 변환하는 과정이다.
이 때,

  • 컴퓨터가 코드를 해석하는데 주석은 필요하지 않으므로 소스 코드에서 주석을 모두 제거하고
  • #include 문으로 포함시킨 헤더 파일을 찾아 헤더 파일에 있는 모든 내용을 복사해서 소스 코드에 삽입하고
  • #define 문으로 정의된 매크로를 저장한 뒤 같은 문자열을 만나면 정의된 매크로로 치환한다.

이를 확인해보기 위해 "Hello, World!"를 출력하는 간단한 소스 코드를 작성 후 g++로 전처리 과정만 진행시켜 보았다.

전처리 과정만 진행시키기 위해서는 -E 옵션을 주면 되고, 실행 결과로 나온 main.i파일을 vim으로 확인해 본 결과

g++ -E main.cpp -o main.i

위와 같이 헤더 파일의 내용이 그대로 복사되어 소스 코드에 삽입된 것을 볼 수 있다.

단, 여기서 주의할 점은 보통 코드를 작성할 때 헤더파일에는 함수의 원형을 선언하고, 함수의 구현을 담당하는 코드 파일을 따로 작성하므로 전처리 과정을 통해 헤더파일로부터 함수의 원형은 복사되지만 함수를 구현한 내용은 포함되어 있지 않다.

따라서 이 전처리 된 소스코드 파일을 컴파일과 어셈블리 과정을 통하여 오브젝트 파일로 만들더라도 독립적으로 실행할 수 없고 링킹이 필요한 것이다.


2. 컴파일(Compile)

소스 코드의 문법을 검사하고 Static 영역의 메모리(text, data, bss) 할당을 수행한다.

컴파일 과정(Compilation)은 컴파일러(Compiler)를 통해 전처리 된 소스 코드 파일을 어셈블리어 파일로 변환하면서 위의 작업을 수행한다.

이를 확인해보기 위해 전처리 된 소스 코드를 g++로 컴파일 시켜보았다.
컴파일한 어셈블리어 파일을 얻기 위해서는 -S 옵션을 주면 되고, 실행 결과로 나온 main.s파일을 vim으로 확인해 본 결과

g++ -S main.i -o main.s

위와 같이 전처리 된 소스 코드 파일이 어셈블리어로 바뀐 것을 볼 수 있다.

💡Tip

gcc, g++ 컴파일러는 .s 확장자와 .S 확장자를 구분한다.
.s 확장자는 전처리가 필요 없는 파일, 컴파일러가 출력한 어셈블리어 코드에 사용되고
.S 확장자는 전처리가 필요한 파일, 개발자가 직접 전처리기 구문(#include, #define, #if...)을 추가하여 작성한 어셈블리어 코드에 사용된다.
(gcc, g++ 컴파일러는 .S 파일을 컴파일 할 때 전처리 코드를 해석할 수 있다.)


3. 어셈블리(Assembly)

어셈블리어 코드 파일을 기계어, 즉 오브젝트 코드로 변환하는 작업

어셈블리 과정(Assembly)은 어셈블러(Assembler)를 통해 어셈블리어 파일을 오브젝트 파일로 변환하는 과정이다.

이를 확인해보기 위해 컴파일 후 얻은 어셈블리어 파일을 어셈블리 과정을 오브젝트 파일로 변환시켜 보았다.
오브젝트 파일을 얻기 위해서는 -c 옵션을 주면 되고, 실행 결과로 나온 main.o파일을 hexdump로 확인해 본 결과

g++ -c main.s -o main.o

hexdump -C main.o

위와 같이 이진수(binary code)의 기계어로 되어있음을 볼 수 있다.


4. 링킹(Linking)

오브젝트 파일들과 라이브러리 파일들을 묶어 하나의 실행 파일로 만드는 작업

오브젝트 파일은 오브젝트 코드, 즉 기계어로 구성된 파일이며 아래와 같은 구조를 가진다.

  • Object File Header : 오브젝트 파일의 기초 정보를 가지고 있는 헤더
  • Text Section : 기계어로 변환된 코드가 들어 있는 부분
  • Data Section : 전역 변수, 정적 변수가 들어있는 부분
  • Symbol Table Section : 소스 코드에서 참조되는 심볼들의 이름과 주소가 정의 되어 있는 부분
  • Relocation Information Section : 링킹 전까지 심볼의 위치를 확정할 수 없으므로 심볼의 위치가 확정되면 바꿔야 할 내용을 적어놓은 부분
  • Debugging Information Section : 디버깅에 필요한 정보가 있는 부분

여기서 주목해야 하는 부분은 Symbol Table SectionRelocation Information Section이다.

심볼(Symbol) 은 함수나 변수를 식별할 때 사용하는 이름으로 Symbol Table 안에는 오브젝트 파일에서 참조되고 있는 심볼 정보를 가지고 있는데
이 때 오직 해당 오브젝트 파일의 심볼 정보만 가지고 있어야 하기 때문에 다른 파일에서 참조되고 있는 심볼 정보의 경우 심볼 테이블에 저장할 수 없다.

즉, 전처리 과정에서 언급한 것 처럼 오브젝트 파일 안에 함수를 구현한 내용이 없으므로 외부에서 참조하는 함수에 대한 심볼 정보는 가지고 있지 않은 것이다.

따라서 실행 파일을 만들기 위해서는 이 오브젝트 파일을 포함하여 함수를 구현한 오브젝트 파일(혹은 라이브러리)를 연결시키는 작업이 필요한 것이고 이것을 링킹이라고 부르는 것이다.

링커(Linker)는 먼저 각 오브젝트 파일에 있는 심볼 참조를 어떤 심볼 정의에 연관시킬지 결정하는 심볼 해석(Symbol Resolution) 과정을 거친 후
오브젝트 파일에 있는 데이터의 주소나 메모리 참조 주소를 알맞게 재배치하는 재배치(Relocation) 과정을 진행한다.

이는 링커가 오브젝트 파일을 모아서 하나의 실행파일을 만들 때, 각 오브젝트 파일에 있는 데이터나 메모리 참조 주소가 실행 파일에서의 주소와 다르기 때문이다.

이를 위해 오브젝트 파일 안에 Relocation Information Section이 존재하는 것이며 링킹 과정에서 같은 세션끼리 합쳐진 후 재배치가 일어난다.


참고자료

profile
개발 공부 일지

0개의 댓글