프로그래밍/C

[C언어 강좌-2] C언어 컴파일 과정

Peter Ahn 2016. 11. 21. 20:12
반응형

안녕하세요 피터입니다.


오늘은 지난시간에 이어 C언어의 컴파일 과정에 대해 설명드리겠습니다.

앞서 여러분이 작성했던 Hello world 코드가 컴퓨터에서 실행이 되려면 우선 컴파일(Compile) 과정을 거쳐야 합니다.  컴파일은 예전에 언급드렸던 것처럼 사람이 이해할 수 있는 언어로 되어 있는 프로그램 코드를 컴퓨터가 이해할 수 있는 언어로 변환하는 작업입니다. 


컴파일 과정은 프로그램을 만드는 데 있어서 반드시 알아야 하는 내용은 아니지만 이 과정을 머리속에 담고 있는 개발자는 앞으로 무수히 부딪히게 될 많은 문제나 오류들을 이해하는데 훨씬 큰 이점을 얻게 될 것입니다.

조금 생소하고 어렵게 느껴지실 수도 있지만 컴퓨터를 이해하는데 한걸음 다가간다고 생각하고 차근차근 따라와주세요.

이제부터 gcc를 통해 C언어로 작성된 코드가 어떻게 컴파일 되는지 살펴보겠습니다.



<gcc 컴파일 과정>


위 그림처럼 여러분들이 작성한 코드는 다양한 과정을 거쳐 실행 가능한 바이너리 파일(Binary file)로 만들어집니다. 이렇게 만들어진 실행파일을 실행하면 바이너리 파일의 내용들이 주기억장치(RAM)적재(Load)되어 시스템에서 동작하게 됩니다.  

그럼 단계별로 처리 과정을 간략하게 알아보겠습니다.


1. 전처리 과정

우선 전처리 과정을 보겠습니다.

전처리 과정은 크게 두 부분으로 나눌 수 있습니다.

  • 헤더 파일 삽입 
  • 매크로 치환 및 적용

C 소스 내에 헤더파일을 include 하는 것은 C 언어의 문법적 특성과 관련된 것으로, C 언어에서는 함수를 사용하기 전에 함수의 원형을 먼저 선언을 해야 합니다. 

따라서 어떠한 함수를 사용하려면 해당 함수가 정의되어 있는 헤더파일을 include 해야 합니다. 전처리기는 #include 구문을 만나면 해당하는 헤더파일을 찾아서 그 파일의 내용을 순차적으로 삽입합니다. 

헤더파일을 다 삽입하면 이제 매크로 치환 작업이 들어갑니다. #define 된 부분은, 심볼 테이블에 저장되고, 심볼 테이블에 들어있는 문자열과 같은 문자열을 만나면 #define 된 내용으로 치환합니다. 이때 #ifdef 와 같은 다른 전처리기 매크로들도 같이 처리됩니다. 

2. 컴파일 과정

전처리 과정이 끝나면 이제 컴파일 과정으로 들어갑니다. 

컴파일 과정은 크게 전단부, 중단부, 후단부로 나눌 수 있습니다. 전처리가 끝난 .i 파일을 컴파일 하면 .s 어셈블리 코드로 이루어진 파일이 만들어집니다. 

2.1 전단부 (Front-end)

전단부에서는 언어 종속적인 부분을 처리합니다. 따라서 이 단계에서는 C언어로 작성된 코드와 C++, Java 등의 다른 언어로 작성된 코드들이 각각 다른 모듈에 의해 처리되게 됩니다. 

이 단계에서는 소스코드가 올바르게 작성되었는지 분석하고, 중단부에 넘겨주기 위한 GIMPLE 트리 (소스 코드를 트리 형태로 표현한 자료 구조)를 생성하는 일을 수행합니다. 

  1. 어휘 분석: C 소스코드를 의미가 있는 최소단위(토큰 : Token)으로 나눕니다.
  2. 구문 분석: 토큰으로 파스 트리(Parse Tree)를 만들면서 문법적 오류를 검출합니다.
  3. 의미 분석: 파스 트리를 이용해 문법적 오류는 없지만 의미상 오류가 있는지 검사합니다. (함수의 매개변수를 잘못 사용했다거나 변수의 자료형(DataType)이 불일치 하는 것 등)
  4. 중간 표현 생성: 언어 독립적인 특성을 제공하기 위해 트리 형태의 중간표현(GIMPLE Tree)을 생성합니다.


2.2 중단부 (Middle-end)

중단부에서는 전단부에서 넘겨받은 GIMPLE 트리를 SSA(Static Single Assignment)형태로 변환한 후에 아키텍쳐 비종속적인 최적화를 수행한 후 최종적으로 후단부에서 사용하는 RTL(Register Transfer Language: 고급 언어와 어셈블리 언어의 중간 형태)을 생성합니다.

아키텍쳐 비종속적인 최적화란 서로 다른 CPU 아키텍쳐에 구애받지 않고 공통적으로 수행할 수 있는 최적화를 말합니다. 중단부에서는 SSA 기반으로 최적화를 수행합니다.

최적화가 왜 중요한가에 대해서 잠깐 설명드리면, 여러분들이 작성한 프로그램이 한번 컴파일 되고 나면 다시 컴파일 하기 전까지 변경이 불가능합니다. (물론 리버싱이라는 기법이 있긴 합니다만 여기에선 논외로 치겠습니다) 그렇게 때문에 최적화를 수행함으로써 컴파일 시간이 오래 걸릴지라도 프로그램의 수행 속도를 향상시켜 전체 시스템 성능의 효율을 지속적으로 높여주기 때문입니다.

SSA기반 최적화는 크게 지역 최적화, 전역 최적화, 루프 최적화로 나눌 수 있는데, 최적화에 관한 내용은 굉장히 내용이 방대하기 때문에 여기에선 간략하게 '이런 과정들이 있다' 정도로만 설명드리겠습니다.

최적화가 완료되면 후단부에서 최적화에 사용하기 위해 RTL(Register Transfer Language)구조로 변환합니다.


2.3 후단부 (Back-end)

후단부에서는 RTL Optimizer에 의해 아키텍쳐 비종속적인 최적화와 함께 아키텍쳐 종속적인 최적화가 수행합니다. 

아키텍쳐 종속적인 최적화는 각 프로그램 내의 명령어 중 아키텍처별로 좀 더 효율적인 명령어로 대체해 성능을 높이는 작업과 같이 아키텍쳐 특성에 따라 최적화를 수행하는 것을 말합니다.

이렇게 최적화를 마치게 되면 Code Generator 어셈블리어로 구성된 .s 파일이 만들어지게 됩니다.


3. 어셈블 과정

컴파일이 끝난 어셈블리 코드는 어셈블러에 의해 기계어로 어셈블됩니다. 

어셈블러에 의해 생성되는 목적코드(helloworld.o) 파일은 어셈블된 프로그램의 명령어(Instruction)데이터(Data)가 들어있는 ELF 바이너리 포맷(Binary format) 구조를 갖습니다. 

다음 단계인 링킹에서 링커가 여러개의 바이너리 파일을 하나의 실행 파일로 묶기 위해서 각 바이너리의 정보를 효과적으로 파악하기 위해서(명렁어와 데이터의 범위 등) 일정한 규칙을 갖게 형식화 해놓은 것입니다. 


4. 링킹 과정

어셈블리에 의해 ELF 포맷의 목적코드 파일들이 만들어지면 이제 링커가 나설 차례입니다. 

링커는 오브젝트 파일들과 여러분의 프로그램에서 사용된 표준 C 라이브러리, 사용자 라이브러리들을 링크(Link)를 합니다. 

printf() 함수나 scanf() 등의 표준 C 라이브러리 함수들은 여러분이 직접 구현하지 않아도 미리 컴파일이 되어 있기 때문에 링크하는 과정만 거치면 사용할 수 있습니다. (표준 C 라이브러리는 별도로 명시하지 않아도 자동으로 링크됩니다)

이렇게 링킹 과정이 끝나면 드디어 실행 가능한 실행파일이 만들어지게 됩니다.


여러분의 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

- Peter의 우아한 프로그래밍 강좌




반응형