본문 바로가기
Topos Uranos

요약

이 강에서는 프로그램이란 무엇이고 컴파일러는 무엇을 하는지 설명한다. 기계는 메모리에 있는 수에 대한 기초적인 연산만 실행하며, 소스 코드는 실행하기 전에 기계어로 번역되어야 한다. 두 파일로 이루어진 프로그램 하나로 C++에서 이 번역의 세 단계(전처리, 컴파일, 링크)를 차례로 따라가고, 컴파일할 때 일어나는 일과 실행할 때 일어나는 일을 각각의 오류 하나씩을 들어 구별한다. 또 컴파일 언어와 인터프리터 언어를 비교하고, C++의 역사를 C++23 표준과 C++26 표준까지 요약하며, 이 강좌가 가르치는 C++가 어떤 것인지 밝힌다.

학습 목표

  1. 소스 파일이 실행 파일이 되기까지의 경로를 도식으로 설명한다. 전처리, 컴파일, 링크의 각 단계에 무엇이 들어가고 무엇이 나오는지도 함께 설명한다.
  2. 어떤 사건이 컴파일, 링크, 실행 가운데 어디에 속하는지 분류한다. 예를 들어 구문 오류, 함수의 누락, 0으로 나누기가 그런 사건이다.
  3. 오류가 발견되는 시점과 프로그래머가 기계를 얼마나 보는지를 기준으로 컴파일 언어와 인터프리터 언어를 비교한다.
  4. 언어의 표준들을 시간 순서 속에 자리매기고, 이 강좌가 C++23을 쓰는 이유를 설명한다.

프로그램이란 무엇인가

프로그램은 기계가 실행할 수 있는 명령어의 나열로, 어떤 데이터를 어떤 결과로 바꾸기 위한 것이다. 앞 강인 문제에서 알고리즘으로에서 보았듯이, 프로그램보다 앞서 알고리즘, 곧 문제를 푸는 방법이 있으며, 그것은 사람이 따라갈 수 있도록 쓰인 것이다. 프로그램은 같은 방법을 기계를 위해 쓴 것이다. 기계는 문장도 의도도 이해하지 못하고 기초적인 지시만 이해한다. 메모리의 한 곳에서 다른 곳으로 수를 복사하고, 두 수를 더하고, 둘을 비교하고, 결과에 따라 다른 명령어로 건너뛰는 것 같은 지시이다.

컴퓨터의 메모리는 번호가 매겨진 칸이 길게 늘어선 줄이며, 각 칸에는 0부터 255까지의 수가 하나씩 들어 있다(이것이 바이트이다). 기계가 다루는 모든 것은 그곳에 수로 부호화되어 있다. 데이터도 그렇고, 명령어 자체도 그렇다. 그렇다면 실행 가능한 프로그램은 프로세서가 지시로 해석하는 수의 나열, 곧 기계어이며, 이는 프로세서에 따라 다르다. 데스크톱 컴퓨터에 흔한 x86-64 계열 프로세서와, 휴대전화와 최근의 Mac에 흔한 ARM 프로세서는 같은 수를 이해하지 않는다.

오늘날 기계어로 프로그램을 쓰는 사람은 없다. 사람들은 C++ 같은 고급 언어로 쓰는데, 그 명령어는 수학 기호와 비슷하며, 소스 코드(또는 그냥 소스)라고 부르는 텍스트 파일에 쓴다. 여기서 소스로부터 그와 동등한 기계어를 만들어 내는 번역자가 필요하다는 결론이 나온다. C++에서는 그것이 컴파일러이다.

예시프로그램 하나와 기계가 실행하는 것

이것은 C++로 쓴 완전한 프로그램이다. 아직 이해할 필요는 없다. 각 줄은 1.5강에서 설명한다. 그것이 텍스트이고 읽을 수 있다는 것, 그리고 그 아래에 실행할 때 출력한 내용이 나온다는 것만 보면 된다.

C++ 프로그램
// 1.5강에서 한 줄씩 설명한다.#include <print>​int main(){    std::println("안녕, 세상");    std::println("2 + 3 = {}", 2 + 3);}
출력
안녕, 세상2 + 3 = 5

번역의 다른 쪽 끝을 보기 위해, 정수 두 개를 더하기만 하는 함수를 생각해 보자(함수는 5단원에서 배운다). 이것은 완전한 프로그램이 아니다. 실행이 시작되는 지점인 main 함수가 없기 때문이다. 그래도 컴파일러는 이 함수를 따로 번역할 수 있다.

C++ 코드
int add(int a, int b){    return a + b;}

objdump 프로그램은 GCC 15가 Linux를 쓰는 x86-64 컴퓨터에서 이 함수를 위해 만들어 낸 기계어를 보여 준다. 각 줄에는 명령어의 위치, 16진수로 나타낸 바이트들, 읽기 쉬운 이름이 차례로 나온다.

터미널
$ g++ -std=c++23 -O1 -c add.cpp$ objdump -d add.o0000000000000000 <_Z3addii>:   0:	f3 0f 1e fa          	endbr64   4:	8d 04 37             	lea    (%rdi,%rsi,1),%eax   7:	c3                   	ret

정수 덧셈은 여덟 바이트로 줄어들었다. 첫 번째 명령어인 endbr64 명령어는 보안용 표시로, 아무것도 계산하지 않는다. 두 번째인 lea 명령어는 함수가 프로세서의 두 레지스터(%rdi 레지스터와 %rsi 레지스터)로 받은 두 수를 더하고, 결과를 세 번째 레지스터(%eax 레지스터)에 넣는다. 세 번째인 ret 명령어는 함수를 호출한 지점으로 돌아간다. _Z3addii 이름은 컴파일러가 “int 매개변수 두 개를 받는 함수 add”를 부호화한 형태이다.

이 예에서 두 가지를 관찰할 수 있다. 첫째, 기계어에는 a 이름과 b 이름도, 들여쓰기도, 주석도 남지 않는다. 이것들은 모두 소스를 읽는 사람을 위해 있던 것이고, 컴파일러는 어떤 명령어를 쓸지 정하는 데 이것들을 사용했다. 둘째, C++ 프로그래머가 쓴 것은 중개자 없이 몇 안 되는 명령어로 번역된다. 기계에 대한 이 가까움이 C++가 효율적인 이유이며, 이 강좌에서 보게 되듯이 그 위험 가운데 몇 가지의 이유이기도 하다.