컴퓨터가 프로그램을 뚝딱 만드는 과정

CS:APP의 정리를 다시 한번 내 언어로 하고 있다. 생각도 안하고 언어 전환도 안하고 판서만 해댄다면, 나는 그냥 오타를 내고 성능이 느린 LLM과 다를바가 없다. 이 내용은 챕터 1의 내용 초반부이다. 면접 5분전의 솔루션이 되게끔 최고의 가독성을 만들어보고자 애써보겠다.


소스코드는 이렇게 생겼어요

프로그램을 하나 만들어보겠다고 하면 소스파일을 쓸 수 있는 앱을 실행해야 할 것이다. 그러고 당신은 소스 프로그램을 작성 할 것이다.

  • 파일은 0, 1로 표시되는 비트들의 연속이며, 바이트라는 8비트 단위로 구성된다.
  • 각 바이트는 프로그램의 텍스트 문자를 나타낸다. 즉 1바이트 = 8비트로 한 글자 “F” 를 표현 할 수 있지

그리고 ASCII 코드를 통한 변환이 이루어진다. 영어, 숫자, 약간의 특수기호를 8비트에서 표현 할 수 있게 해주는 ASCII 코드로 전체 소스 코드를 일련의 숫자로 변환 할 수 있다.

return 0;
r e t u r n  공 백 !!! 0 ;
114 101 116 117 114 110 32 48 59

ASCII로만 이루어진 파일들을 텍스트 파일이라고 부른다. 그 외에는 바이너리 파일이라고 부른다.

소스파일은 아직 사람에게 보다 가까운 형태로 존재한다. 저장 자체가 숫자로 이루어진다고는 하지만, 여전히 기계는 곧이 곧대로 알아듣지 못한다. 그래서 기계가 알아 들을 수 있는 언어로 변환해야 한다. 운영체제 내지 실행 하고 있는 IDE, 언어 단위로 다양한 방법을 쓸 수 있다.

글에서는 C언어의 gcc라는 일련의 컴파일러 파이프라인을 사용해서 책의 내용을 적어 내려가겠다.

컴파일 시스템

  • 전처리 단계
    • C언어를 비롯한 다양한 소스코드는 외부에서 뭘 좀 참조를 해야한다. #include <stdio.h> 같은 내용이 예시다. 이 과정에서 작동하는 전처리기는 stdio.h라는 파일의 내용을 앞에 언급한 위치에 전부 복붙한다. 이렇게 하면 .i로 끝나는 파일이 만들어진다.
  • 컴파일 단계
    • 컴파일러는 아직은 텍스트 파일인 .i 파일을 .s로 번역한다. 내용의 구성은 어셈블리어로 전환되었다. 하지만 여전히 저장은 ASCII 코드의 형태로 구성되어 있다. 어셈블리어는 movl   $0, %eax 와 같이 읽을 순 있겠지만 쉽지 않은 언어이다.
  • 어셈블리 단계
    • 이제 .s 파일을 기계어로 번역하여 .o 파일로 만든다. 이제야 비로소 바이너리 파일이라고 불릴 수 있는 형태가 되었다. 텍스트 편집기로 열어보면 이제 사람이 읽기엔 영 아닌 모양이 되어 있을 것이다.
  • 링크 단계
    • 우리가 만든 프로그램이 사전에 만든 표준 라이브러리에 포함된 내용을 사용한다고 해보자. 예를 들어, printf를 사용한다고 하면 printf를 쓸 수 있게 어떤 형태로든 결합되어야 한다. 앞에서 만들어진 .o와 이미 존재하는 printf.o를 합침으로써, 드디어 실행 가능한 파일이 만들어지게 되었다.

 

링크 단계의 의의

처음부터 stdio.h를 사용하였듯, 컴파일러가 printf를 합쳐서 사용 할 수도 있었을 것이다. 이것을 링커 단계에서 하는 의의는

  1. 효율성 : 모든 프로젝트에 있어 컴파일 시간을 획기적으로 줄여준다.
  2. 모듈성 : printf가 어떻게 동작하는지는 알빠가 아니고 그냥 가져다 쓰면 된다. 이렇게 각 기능을 모듈화하는건 큰걸 하나 만드는게 있어 작업과 관리를 훨씬 편하게 만들어준다.

하지만 링커에서 하나 컴파일러에서 하나 합치는 과정은 같다고 생각했다. 이부분을 확인해보았는데..

  • 컴파일은 검수를 포함하는 과정이다. 즉, printf.o는 이미 검증된 파일인데, printf.i(또는 .c)를 동원해서 컴파일 단계에서 합치는 시도는 링크 단계에서 흔히 언급하는 "조립" 보다 훨씬 많은 시간이 필요하다.
  • 미리 컴파일 된 형태를 필요 할 때 "조립"만 하는 앞의 방식을 개별 컴파일(Separate Compliation)이라고 칭한다.