C언어 - Compiling Process에 대하여

resister_boy·2022년 11월 18일
0

프로그래밍 개념

목록 보기
2/3
post-thumbnail

컴파일(Compile)이란 무엇일까?

컴파일(Compile)은 인간이 이해할 수 있는 언어로 작성된 소스 코드를 컴퓨터, 엄밀한 의미에서는 CPU가 이해할 수 있는 언어로 변환하는 작업을 말합니다. 소스 코드는 컴파일 과정을 거쳐 기계어로 이루어진 실행파일이되며, 이를 실행하면 실행 파일 내용이 운영체제의 Loeader에 의해 메모리(주기억장치)에 적재되고 실행됩니다.

컴파일 과정(Compiling Process)

컴파일은 전처리(Pre-processing), 컴파일(Compilation), 어셈블(Assemble), 링킹(Link)이라는 4가지 단계로 구성되어있습니다. 일반적으로 이 4단계를 묶어서 컴파일(Compile) 또는 빌드(Build)라고 부르고 있습니다.

Compiling Process

전처리(Pre-processing)

전처리 단계에서는 전처리기(Preprocessor)를 통해 소스코드(.c)를 전처리된 소스코드(.i)로 변환합니다. 전처리 단계에서는 다음과 같은 세 가지 작업을 수행합니다.

1. 주석 제거

소스코드의 모든 주석을 제거합니다.

2. 헤더파일 삽입

#include 지시문을 만나면 이에 해당하는 헤더파일을 찾아 헤더파일에 있는 내용을 복사하여 소스코드에 삽입합니다. 즉, 헤더파일 자체는 컴파일되지 않고 소스코드 내에 전부 복사됩니다. 헤더파일에 선언된 함수원형은 나중에 링킹 단계에서 실제로 함수가 정의되어 있는 오브젝트 파일과 결합하게 됩니다.

3. 매크로 치환 및 적용

#define을 포함한 전처리 지시문을 통해 정의된 매크로(Macro)를 저장하고 소스코드에서 매크로를 만나면 미리 정의한 내용으로 치환합니다.

컴파일(Compilation)

컴파일 단계에서는 컴파일러(Compiler)를 통해 전처리된 소스코드(.i)를 어셈블리 코드(.s)로 변환합니다. 이 과정에서 컴파일러는 언어의 문법을 검사하고 Static한 영역(Data, BSS)에 메모리가 할당됩니다. 컴파일 단계는 아래의 세 단계로 구성되어 있습니다.

1. 프론트앤드

프론트앤드에서는 언어 종속적인 부분을 처리합니다. 소스코다가 해당 언어로 올바르게 작성되었는지 확인하고 미들앤드에 넘겨주기 위한 GIMPLE TREE(소스코드를 트리 형태로 표현한 자료구조)를 생성합니다.

2. 미들앤드

미들앤드에서는 아키텍쳐에 종속되지 않는 최적화를 수행합니다. 이는 CPU 아키텍쳐에 상관없이 할 수 있는 최적화를 의미합니다. 최적화를 수행한 뒤에는 백앤드에서 사용하게 되는 RTL(Resister Transfer Language: 고수준 언어와 어셈블리 코드의 중간 형태)를 생성합니다.

3. 백앤드

백앤드에서는 아키텍쳐에 종속되는 최적화를 수행합니다. 이는 아키텍쳐의 특성에 따라 수행하는 최적화를 의미합니다. 같은 기능을 수행하는 명령어라고 하더라도 CPU 아키텍쳐에 따라 효율적인 명령어로 대체하여 성능을 높이는 작업입니다. 이 최적화가 완료되면 어셈블리 코드가 생성되는데, 이는 최적화를 수행한 아키텍쳐만이 이해할 수 있는 코드가 되며, 다른 아키텍쳐로는 해석할 수 없습니다.

어셈블(Assemble)

어셈블(Assemble) 단계에서는 어셈블러(Assembler)를 통해 어셈블리 코드(.s)를 오브젝트 코드(.o)로 변환합니다. 오브젝트 코드는 기계어나 혹은 이에 RTL과 같은 바이너리 코드로 이루어져있습니다. 이때 오브젝트 파일 포맷이 등장합니다.

오브젝트 파일 포맷

오브젝트 파일 포맷은 오브젝트 코드와 관련된 데이터가 저장되는 일종의 양식입니다. 이전에는 개별 컴퓨터마다 고유의 포맷을 가지고 있었으나, 유닉스와 같은 운영체제가 출현하면서 COFF, ELF와 같은 포맷이 정의되면서 여러 시스템에서 사용되기 시작했습니다. 대표적으로 Linux와 MacOS에서는 ELF(Excutable and Linkng Format)이 사용되고, Windows의 경우 PE(Portable Executable)이 사용되고 있습니다. 오브젝트 파일 포맷은 기본적으로 아래와 같은 구조를 하고 있습니다.

1. 오브젝트 파일 헤더(Object File Header)
오브젝트 파일의 기본정보를 가지고 있는 헤더

2. 텍스트 섹션(Text Section)
기계어 또는 RTL과 같은 바이너리 코드를 가지고 있는 섹션

3. 데이터 섹션(Data Section)
데이터(전역 변수, 정적 변수)를 가지고 있는 섹션

심볼 테이블 섹션(Symbol Table Section)
소스 코드에서 참조하는 심볼의 이름과 주소가 정의되어 있는 섹션

재배치 정보 섹션(Relocation Information Section)
링킹 전까지 심볼의 위치를 확정할 수 없으므로 심볼의 위치가 확정될 때 바꾸어야 할 내용이 정의되어 있는 섹션

디버깅 정보 섹션(Debugging Information Section)
디버깅에 필요한 정보가 정의되어 있는 섹션

여기에서 중요한 부분은 심볼 테이블 섹션과 재배치 정보 섹션이다. 심볼(Symbol)은 함수나 변수를 식별할 때 사용하는 이름으로 심볼 테이블 안에는 오브젝트 코드에서 참조하고 있는 심볼의 정보(이름과 메모리 주소)를 가지고 있습니다. 이때 오브젝트 코드가 있는 각 파일은 해당 파일의 심볼 정보만 가지고 있기 때문에 다른 파일에서 참조하는 심볼의 정보에는 접근할 수 없습니다.

여기까지 소스 코드를 컴파일하여 어셈블리 코드로 변환하고 이를 어셈블러를 통해 오브젝트 코드로 변환했습니다. 이제 오브젝트 코드를 실행파일로 만들면 소스코드를 실행할 수 있게 됩니다. 이제 링크(Link) 단계가 남았습니다.

만일 우리가 하나의 소스코드만을 가지고 이 작업을 진행했다면, 링크 단계가 필요하지 않겠지만, 여러 개의 파일에 코드가 모듈화되어있다면, 링크 단계를 거쳐야 실행파일이 될 수 있습니다. 이는 각 파일이 해당 파일의 심볼 정보만 가지고 있기 때문에 여러 개의 파일이 서로 의존하는 경우 정상적으로 빌드가 되지 않기 때문입니다. 마지막으로 링크(Link) 단계를 살펴보겠습니다.

링크(Link) 단계에서는 링커(Linker)를 통해 여러 개의 오브젝트 코드(.o)와 라이브러리 파일을 링크하여 하나의 실행파일을 생성합니다. 이때 라이브러리를 링크하는 방법에 따라 정적 링킹(Static Linking)과 동적 링킹(Dynamic Linking)으로 나뉘게 되며, 정적으로 링킹하는 라이브러리를 정적 라이브러리라고 하고, 동적으로 링킹하는 라이브러리를 동적 라이브러리라고 합니다.

정적 라이브러리(Static Library)

정적 라이브러리는 정적 링킹 과정에서 링커가 프로그램이 실행되는데 필요한 부분을 라이브러리에서 찾아 실행 파일에 복사하는 방식의 라이브러리를 의미합니다.

동적 라이브러리(Dynamic Library)

동적 라이브러리는 동적 링킹 과정에서 링커가 라이브러리의 내용을 복사하지 않고 해당 내용의 주소만 가지고 있다가 런타임에 실행 파일과 라이브러리가 메모리에 위치될 때 해당 모듈의 주소로 가서 필요한 것들을 가져오는 방식의 라이브러리를 의미합니다.

링커의 역할

심볼 해석(Symbol Resolution)

심볼 해석(Symbol Resolution)은 각 오브젝트 파일의 심볼 테이블에 위치한 심볼의 정보를 연결시키는 과정을 말합니다. 여러 개의 오브젝트 파일에 같은 이름의 함수 또는 변수가 정의되어 있을 때 어떤 파일의 어떤 함수를 사용할지 결정하는 역할을 합니다.

재배치(Relocation)

재배치(Relocation)은 오브젝트 파일에 있는 데이터의 주소나 코드의 메모리 코드의 메모리 참조 주소를 배치하는 과정을 말합니다. 링커가 컴파일러가 생성한 오브젝트 파일을 모아 하나의 하나의 실행 파일을 생성할 때, 각 오브젝트 파일에 있는 데이터의 주소나 코드의 메모리 참조 주소가 링커에 의해 합쳐진 실행 파일에서의 주소와 다르기 때문에 이를 수정해주는 것이라고 할 수 있습니다. 이것을 위해 오브젝트 파일 안에 재배치 정보 섹션(Relocation Information Section)이 존재하는 것이며, 이 과정에서 같은 세션끼리 합쳐진 후 재배치가 일어납니다.

Conclusion

이렇게 우리의 소스코드는 전처리, 컴파일, 어셈블, 링크라는 4단계를 거쳐 실행파일이 됩니다. 각 과정에서 소스코드는 전처리된 소스코더, 어셈블리 코드, 오브젝트 코드, 마지막으로 기계어로 변환되어 실행됩니다. 이렇게 C언어가 컴파일 되는 과정을 적어보았지만, 아직까지 개괄적인 내용이라고 생각합니다. 아직까지 전처리 우선순위나 오브젝트 파일 포맷과 심볼 등 공부해야 할 것이 많은 것 같습니다. 이 글은 아래 링크들을 참조하여 작성되었습니다. 틀린 부분이 있다면 댓글로 남겨주세요.

Reference

https://bradbury.tistory.com/226

https://reakwon.tistory.com/52

https://gracefulprograming.tistory.com/16

https://www.it-note.kr/263

https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC

https://blog.naver.com/PostView.naver?blogId=techref&logNo=222221869206&from=search&redirect=Log&widgetTypeCall=true&directAccess=false

https://simsimjae.tistory.com/235

profile
좋은 제품을 만드는 사람

0개의 댓글