실행이 가능한 파일

파일의 실행에 대해서는 이전 문서에서도 적어놓은 내용이 존재한다. 다시 한번 적어보자면 컴퓨터에 저장된 모든파일들은 결국 2가지로 나뉘어진다.

실행이 가능한 파일과 (명령어가 들어가있는 소스파일을 기반으로 만들어진 파일) ,

단순한 문자열 데이터의 나열로만 되어있는 파일 (명령어가 존재하지않은 파일 - txt파일,html파일,오디오-이미지-영상파일등)

즉 실행이 가능한 파일이란 컴퓨터가 무언가의 작업을 진행하게끔, CPU에게 내리는 특정 명령문들과 그리고 그 명령에 필요한 특정 정보들이 들어가있는 파일이다. 그 이외의 파일들은 컴퓨터의 동작과는 관련없는 단순히 무언가의 정보만을 저장하기 위한 파일들 뿐이다.

본 문서에서는 프로그래머가 실행이 가능한 파일을 만드는 그 과정(빌드의 과정)이 어떠한 방식으로 진행되는지에 대해 다룰 것이다.

빌드의 과정을 이해하기위해선,

크게는 어셈블러, 컴파일러, 링커의 전반적인 내용을

작게는 컴파일러가 만들고, 링커가 참조해서 이용하는 오브젝트 파일에 대한 내용까지

알아야할 필요가 있다.

우선은 프로그래머가 작성한 명령문을 받아들이는 컴퓨터의 CPU에 관한 내용부터 시작한다.

CPU (기계어와 어셈블리어)

흔히 컴퓨터라고 했을때 떠오르는 이미지라고 하면 모니터와 키보드부터 시작해 여러 많은 장치들이 선으로 복잡하게 연결되어 있는 모습을 떠올리는게 일반적이겠지만 그것들은 결국 컴퓨터의 부속 기기일뿐이다.

컴퓨터의 알맹이는 CPU라고 부르는 조그만한 칩일 뿐이다. 프로그래머가 내리는 모든 명령은 결국 이 조그만한 CPU칩을 조작하는 것이다.

CPU, 프로그래머의 입장 vs 설계자의 입장

a0106759_49722d2fc0f0d.jpg

CPU라는 칩에는 해당 칩을 제작한 제조사별로 대동소이하게 특정 명령어 집합들이 내장되어있다. 프로그래머는 해당 명령어들을 이용해서 컴퓨터의 동작을 이끌어낸다.

CPU가 지니고 있는 명령어들의 도표 (어셈블리어)

명령어 1.png

명령어 2.png

위 도표의 명령어들은 니모닉(Mnemonic) 형태로 사람이 알아보기 쉽게 네이밍되어 있지만, 실제 CPU에 내장되어있는 명령문들은 오로지 1과 0의 패턴으로만 구성되어있다.
(컴퓨터는 하드웨어의 특성상 오로지 1과 0의 이진문자체계만으로 이루어져있다 == 기계어)

CPU가 가지고 있는 1과 0의 조합 패턴으로 이루어져있는 이 명령어 집합들을 사람이 알아보기 쉬운 니모닉(Mnemonic) 문자체계로 치환한 것이 바로 어셈블리어이다.

즉 어셈블리어는 CPU에 내장되어있는 1과 0의 명령어 패턴들(기계어)을 사람들이 알아보기 쉽도록 이름을 붙여준 것일뿐이기에 정확하게 기계어와 1:1로 대응한다.

그리고 사람이 작성한 이 어셈블리어를 CPU가 알아먹을 수 있게 기계어로 변환 해주는 것이 바로 어셈블러이다.

어셈블리어 사용의 예시 (위키백과)

어셈블리어.png

1과0만을 이용한(기계어만을 이용한) 코딩은 너무나도 비효율적이었기때문에 개발자가 알아보기 쉬운 어셈블리어라는 언어가 등장했고, 이 어셈블리어의 등장으로 코드 가독성의 획기적인 증진이 생겨나긴했지만 여전히 불필요하고 번거로운 코딩작업들은 그대로 남아있었다.

어셈블리어는 프로그래머가 코드를 작성할 때,

특정 데이터가 어떤 메모리영역에 올라갈지를 일일이 지정해줘야하고,

커널단의 시스템 명령어를(Ex.파이썬의 input(),print() 와 같은 입출력 함수) 실행시키기 위해 시스템 콜을 통한 인터럽트를 일일이 구현해줘야하는 등,

메모리와 운영체제 관련 부분까지 직접 코딩으로 설계해줘야하는 번거로움이 따랐다.

프로세스에서 할당받은 메모리의 구조

page1-149px-Program_memory_layout.pdf.jpg

어셈블리어로 코딩된 "Hello World" 출력 프로그램

123.png

따라서 위와같은 번거로운 작업들을 생략시킨, 더욱 범용적이고 추상화된 언어들이 등장하기 시작했는데 그것이 바로 C , C++, 파이썬, 자바와 같은 고급언어들이다.

이렇게 탄생한 새로운 언어들 역시도 어셈블러와 같이 원문 코드를 기계어로 번역해주는 과정이 당연히 필요한데 바로 이 과정을 수행하는 것이 컴파일러이다.

(물론 어셈블러도 궁극적으로 컴파일러에 속한다. 하지만 일반적인 컴파일러들은 코딩된 원문을 먼저 어셈블리어로 번역한 뒤, 어셈블러를 통해 다시 기계어로 번역하는 과정을 거치고 있기에 용어의 세분화가 필요하다)

[사실 C언어의 등장배경을 읽어보면, CPU 제조사의 명령어셋을 그대로 따라가는 어셈블리어의 CPU 의존성 한계를 극복하고자 C언어가 개발되었다고 한다. 각 제조사별 제각각으로 만들어진 CPU는 내장된 명령어들의 구조도 각기 다르기 때문에, 코딩의 체계또한 다를 수 밖에 없다. 즉 A사 CPU를 사용하는 컴퓨터에서 만든 코드는 B사 CPU를 사용하는 컴퓨터에서는 돌아갈 수 없는 것. 이를 극복하고자 C언어라는 통합된 규칙체계를 갖는 언어를 탄생시키고자 한 것이다. (밑 그림참조) 뭐 최근에는 CPU의 제조사가 다르더라도 웬만해선 모두 규격화된 동일체계의 명령어 집합을 이용하고 있기 때문에 이 부분은 딱히 신경 쓸 필요 없을 것 같다.]

C로 통합된 코딩규칙 아래 컴파일러를 두어 어셈블리어의 CPU 의존성 문제를 해결

컴파일러.png

컴파일러

실행가능한 파일을 만들기위해 진행되는 컴파일의 과정은,

원문 소스 -> 어셈블리어로 컴파일 (C 컴파일러) -> 기계어 컴파일 (어셈블러)

의 3가지 과정을 거치게 된다.

프로그래머가 코딩한 원문 소스는 C컴파일러를 통해 어셈블리어로 변환된 뒤, 다시 어셈블러를 통해 기계어로 변환된다. 그럼 어셈블러는 그저 단순하게 어셈블리어를 기계어로 바꿔주기만 하는 녀석일까?

그렇지 않다. 사실 실행이 가능한 파일이든, 그렇지 않은 파일이든 컴퓨터에 저장되는 모든 파일들은 파일포맷이라는 특정한 저장 규칙을 따르고 있다. 포맷이란 일종의 저장틀로서 데이터들을 어떤 순서로 어떻게 저장하여 보관할 것인지를 규격화 시켜놓은 것이다.

(EX. 이미지 파일이란, 특정 이미지에 대한 픽셀 정보 및 관련 데이터들을 묶어 저장해놓은 파일이다. 그런데 만약 이 이미지파일의 데이터 저장방식을 사용자가 자신의 입맛대로 아무렇게나 지정한다면 어떻게 될까? 해당 이미지파일의 데이터를 어떻게 해석해야하는지 다른사람들은 알 수 있는 방도가 없기에, 그 파일을 만든 본인 이외의 사람들은 파일을 올바르게 읽어내지 못한다. 따라서 공통된 저장규칙을 정해놓고 해당 방식에 따라 파일을 저장하고, 또 해당 방식대로 데이터를 읽어나가도록 규정한 것이 바로 포맷 규칙이다.)

실행파일 또한 윈도우에서는 PE 포맷, 리눅스에서는 Elf 포맷이라는 특정한 규격을 따르고 있다. 그리고 어셈블러는 어셈블리어 번역 업무뿐만아니라, 데이터들을 PE or ELF 포맷 규격에 맞게 저장하는 일들을 동시에 행하고 있다.

결국에 프로그래머가 작성한 코드는 어셈블리어 변환을 거쳐 다시 기계어로 변환되고, 최종적으로 실행파일 포맷 규칙에 맞게 적절하게 구분되어 저장된다.

이 모든 과정을 컴파일러를 통해 수행하는 것을 컴파일이라고 부르는 것이다.