1.2 Programs Are Translated by Other Programs into Different Forms
1.2 프로그램은 다른 프로그램에 의해 다양한 형태로 변환된다
hello 프로그램은 사람들에게 읽고 이해할 수 있는 형태이기 때문에 고수준 C 프로그램으로 시작됩니다.
하지만 시스템에서 hello.c를 실행하려면, 각 C 문장이 다른 프로그램들에 의해 저수준 머신 언어 명령어들의 순서(0,1의 이진코드 형식)로 변환되어야 합니다.
이 명령어들은 실행 가능한 객체 프로그램(오브젝트 파일)이라는 형태로 포장되어 이진 파일로 저장됩니다.
오브젝트 프로그램은 실행 가능한 객체 파일(링커가 여러 오브젝트 파일을 결합하여 만든 최종 실행 파일)이라고도 불립니다.
유닉스 시스템에서 소스 파일을 오브젝트 파일로 변환하는 과정은 컴파일러 드라이버( 우리가 흔히 아는 GCC나, clang 같은 컴파일러들 ) 에 의해 수행됩니다.
linux> gcc -o hello hello.c
gcc 컴파일러 드라이버는 소스 파일 hello.c를 읽어 실행 가능한 오브젝트 파일 hello로 변환합니다.
이 변환 과정은 그림 1.3에 나타난 네 단계의 순서대로 수행됩니다.
이 네 단계(프리프로세서, 컴파일러, 어셈블러, 링커)를 수행하는 프로그램들을 컴파일 시스템이라고 합니다.
전처리 단계
프리프로세서(cpp)는 ‘#’ 문자로 시작하는 지시어에 따라 원본 C 프로그램을 수정합니다.
예를 들어, hello.c의 1행에 있는 #include <stdio.h> 명령은 프리프로세서에게 시스템 헤더 파일 stdio.h의 내용을 읽고, 그것을 프로그램 텍스트에 직접 삽입하라고 지시합니다.
그 결과는 또 다른 C 프로그램이며, 보통 .i 접미사가 붙습니다.
.i 파일
.i 파일은 이러한 전처리 지시어가 모두 처리된 후의 완전한 C 소스 코드로, 실제 코드에는 더 이상 전처리 지시어가 포함되지 않습니다. 이 파일은 컴파일러가 실제로 컴파일을 수행하기 전에, 그 이전 단계에서 처리된 결과물입니다.
컴파일 단계
컴파일러(cc1)는 텍스트 파일 hello.i를 어셈블리 언어 프로그램이 포함된 텍스트 파일 hello.s로 변환합니다.
이 프로그램에는 main 함수의 정의가 포함됩니다:
1 main:
2 subq $8,%rsp
3 movl $.LC0,%edi
4 call puts
5 movl $0,%eax
6 addq $8,%rsp
7 ret
이 정의에서 2번에서 7번까지의 각 라인은 저수준 머신 언어 명령어를 텍스트 형식으로 설명하고 있습니다.
즉, 이는 기계어 명령어를 어셈블리 언어로 표현한 것입니다. 이 코드가 실제로 수행하는 작업을 보여줍니다.
어셈블리 언어는 다양한 고급 언어를 위한 서로 다른 컴파일러들이 공통된 출력 언어로 사용하기 때문에 유용합니다. 예를 들어, C 컴파일러와 Fortran 컴파일러는 둘 다 같은 어셈블리 언어로 출력 파일을 생성합니다.
어셈블리 단계
그 후, 어셈블러(as)는 어셈블리 코드인hello.s를 기계어로 변환하고 , 이를 이동 가능한 객체 프로그램(relocatable object program)이라는 형태로 패키징한 후, 그 결과는 hello.o이란, 기계어 코드로 변환된 객체 파일로 변환됩니다.
이 파일은 main 함수의 명령어를 인코딩하는 17바이트를 포함하는 이진 파일입니다.
이동 가능한 객체 프로그램(relocatable object program)
이 객체 프로그램이 메모리의 어느 위치에 로드되더라도 실행이 가능하다는 의미입니다. 코드 내에서 사용하는 주소가 절대적인 고정 주소가 아니라, 링커나 로더가 적절한 메모리 주소를 할당하여 수정할 수 있도록( 객체 파일 내의 상대적인 주소를 실제 메모리 주소로 변경하는 작업이다. 링커는 이 과정을 통해 객체 파일들의 주소를 정확하게 연결하고, 로더는 실행 시 이 주소들을 실제 메모리에서 올바르게 처리하여 프로그램이 정상적으로 실행되도록 한다.) 되어 있습니다.
링킹 단계 (Linking)
링커는 여러 객체 파일들을 결합하여 최종 실행 파일을 생성하는 역할을 합니다.
객체 파일들은 독립적으로 실행할 수 없으며, 링커를 통해 하나의 실행 파일로 결합되어야 합니다.
링커는 각 객체 파일 내에서 필요한 메모리 주소를 수정하고, 다른 라이브러리나 외부 코드를 연결하여 최종적인 실행 파일을 생성합니다.
링킹 과정에서의 주소
링킹 과정에서 생성되는 객체 파일(Object File)에는 상대 주소 또는 심볼이 포함되어 있습니다. 객체 파일은 실제 메모리 주소를 모르고, 주소는 상대적인 위치로만 표현됩니다.
상대 주소와 절대 주소
- 상대 주소(Relative Address):
- 객체 파일에서 사용되는 상대 주소는 프로그램 코드 내에서 다른 명령어나 데이터와의 상대적인 거리로 저장됩니다.
- 예를 들어, 프로그램의 시작위치에서, 특정 객체 파일에 저장된 foo() 함수의 주소는 프로그램의 위치에서 부터, 몇 바이트 떨어져 있는지를 상대 주소로 기록합니다. 즉, 상대 주소는 보통 실제 메모리 주소가 아니라 현재 프로그램 시작 위치를 기준으로 한 상대적인 오프셋으로 저장됩니다.
- 상대 주소는 메모리 내 위치가 정해지지 않은 상태에서 참조되며, 나중에 링킹 과정에서 절대 주소로 변환됩니다. 즉, 이는 어디에 로드될지 모르기 때문에, 링커는 이를 실제 절대 주소로 변환해야 합니다.
- 절대 주소(Absolute Address):
- 절대 주소는 메모리에서 고유한 위치, 즉 실제 메모리 상의 주소를 의미합니다. 프로그램 실행 시 메모리에 로드된 후, 변수와 함수는 실제 메모리 주소를 가집니다. 이 절대 주소는 메모리 상에서 해당 위치가 어디인지를 나타냅니다.
- 절대 주소는 실행 파일이 메모리에 로드될 때 운영 체제의 로더가 결정합니다. 링커는 이 절대 주소를 할당하고, 해당 상대 주소를 절대 주소로 변환합니다.
객체 파일 (.o 또는 .obj)
이 객체 파일은 링커를 통해 하나의 실행 파일로 결합되기 전에, 어떤 메모리 주소에도 로드될 수 있도록 설계되어 있습니다.
정리
hello.c는 고급 C 프로그램으로 시작되며, 사람들이 읽고 이해할 수 있는 형태입니다. 하지만 시스템에서 이를 실행하려면, 각 C 문장이 저수준 머신 언어 명령어로 변환되어야 합니다. 이 명령어들은 실행 가능한 객체 파일(실행 파일)이라는 형태로 포장되어 이진 디스크 파일로 저장됩니다.
유닉스 시스템에서 소스 파일을 객체 파일로 변환하는 과정은 컴파일러 드라이버에 의해 수행됩니다. 이 과정은 프리프로세서, 컴파일러, 어셈블러, 링커의 네 단계를 거쳐 실행됩니다.
- 전처리 단계: 프리프로세서(cpp)는
#로 시작하는 지시어를 처리하여 소스 파일을 수정합니다. 이 결과는.i확장자를 가진 전처리된 C 소스 코드가 됩니다. - 컴파일 단계: 컴파일러(cc1)는
.i파일을 어셈블리 언어로 변환하고,.s확장자를 가진 파일로 저장합니다. 이 파일에는 저수준 머신 언어 명령어들이 포함됩니다. - 어셈블리 단계: 어셈블러(as)는
.s파일을 기계어 명령어로 변환하고, 이를 이동 가능한 객체 프로그램 형태로 패키징하여.o파일을 생성합니다. - 링킹 단계: 링커는 여러 개의 객체 파일을 결합하여 하나의 실행 가능한 파일로 만듭니다. 이 최종 파일은 실제로 CPU가 실행할 수 있는 형태로 변환된 실행 파일입니다.
주요 용어
- 오브젝트 파일 (Object File): 컴파일 과정에서 생성되는 중간 파일로, 기계어 코드가 포함되어 있지만 실행 가능하지는 않습니다.
- 이동 가능한 객체 프로그램 (Relocatable Object Program): 링커가 다른 객체 파일들과 결합될 수 있도록 설계된 파일로, 메모리 내 어느 위치에서든 실행할 수 있습니다.
- 실행 가능한 객체 파일 (Executable Object File): 링커에 의해 결합된 최종 실행 파일입니다. CPU가 직접 실행할 수 있는 상태로 변환됩니다.