[Study][컴퓨터 밑바닥의 비밀] 1. 프로그래밍 언어부터 프로그램 실행까지, 이렇게 진행된다.

이형걸·2025년 5월 11일
1

Study

목록 보기
5/5

이 책의 첫 단원인 프로그래밍 언어부터 프로그램 실행까지, 이렇게 진행된다. 에서는 제목에서 부터 알 수 있듯이 인간이 작성하는 고수준 언어(C, C++, Java 등)이 어떻게 저수준 언어로 번역이 되고, 그것을 어떻게 컴퓨터가 이해하고 명령대로 수행하는 것 까지 과정을 설명합니다.

  1. 프로그래밍 언어를 발명한다면? (feat. 0110110 에서 int a = 1; 까지)
  2. 고수준 언어인 소스 코드(xxx.c, xxx.cpp, xxx.java)를 만들었으면 저수준 언어로 번역해야지? 컴파일러(compiler)는 어떻게 동작할까?
  3. 저수준 언어인 대상 파일(xxx.o, xxx.obj, xxx.class(운영체제 별로 확장자가 다를 수 있다)을 어떻게 병합하지? 링커(linker)
  4. 이 모든 것이 추상화다. 컴퓨터 과학에서 추상화가 중요한 이유

이 책에서는 대부분 C, C++ 언어를 예시로 설명합니다.
개인적인 평으로는 대학교 학사 과정에서 4학년 과목으로 컴파일러가 있었는데, 수강하지 않은게 살짝 후회됐습니다ㅎㅎㅎ

1. 프로그래밍 언어를 발명한다면?

처음에 인류는 간단한 스위치(on-off)를 조합하면 복잡한 불 논리(boolean logic)(ex: 0110010011)을 표현할 수 있다는 것을 발견하고, 이를 기반으로 CPU 를 만들었습니다!

그래서, 프로그래머는 이 CPU 가 이해할 수 있도록 코드를 직접 0 과 1로 구성된 명령어를 작성합니다.

011101100011
110100111101

하지만, 이러한 기계어는 작성하기 너무 불편하기 때문에 이를 인간이 이해하고 해석할 수 있는 단어와 대응시켜 명령어를 만들었습니다.
CPU 는 아주 뛰어나고 빠른 계산기와 같기 때문에 계산기와 비슷한 가산 명령어, 점프 명령어 등 몇가지 명령어를 만들어냅니다.

이렇게 어셈블리어(assembly language)가 탄생합니다.

sub $8, %rsp
mov $.LC0, %edi
call puts
mov $0, %eax

이전 보다는 많이 편해졌지만 인류는 xx해줘를 좋아하기 때문에, 저수준 언어인 어셈블리어 에도 불편함을 느꼈기 때문에 이를 발전시켜 인간이 더욱 사용하기 쉽고 이해하기 편리한 고수준 언어를 발명하고자 했습니다.

인간이 하는 대부분의 언어는 매우 추상적이고 고수준인 반면에, 컴퓨터는 조금만 어렵게만 말해도 이해를 못하는 단순한 계산기기 때문에 최대한 쉽고 잘게 나누어서 표현해줘야 합니다.
간단한 명령 조차 컴퓨터 한테는 수십줄의 세부사항들로 나뉘어질 수 있죠.

이 세부사항이 규칙 또는 패턴으로 가득한 것을 발견합니다.

  • 단도직입적인 명령어에는 문(statement)라고 명명하고
  • 조건에 따라 특정행동을 실행할지 결정하는 것은 if, else
  • 일정한 명령어를 계속 반복하는 것에는 while
  • 명령어에 개별적인 세부사항만 있을 때는 해당 세부사항만 따로 떼어내어 parameter
  • 나머지 공통된 명령어들을 합쳐서 func
// 조건에 따른 이동
if ...
 blablabla
else ...
 blablabla

// 순환
while ...
 blablabla

// 함수와 파라미터
func abc(...)
 blablabla

blablabla 에는 또 다시 statement 들이 반복됩니다.

if: if expr statement else statement
for: while expr statement
statement: if | for | statement

이러한 반복되는 것을 인간은 수학에서의 수열재귀 개념을 이용해서 나타내게 됩니다.

또한, 위에 나와있는 표현들을 고급지게 구문(syntax) 라고 명명했습니다.

재귀 구문에 따라 작성된 코드가 나무 줄기에는 나뭇가지가 있고, 그 나뭇가지에는 잎이나 또 다른 나뭇가지가 있는 트리(tree) 와 같은 것을 발견하고, 코드도 똑같이 트리(tree) 구조로 표현합니다.

이를, 구문 트리(syntax tree)로 표현합니다.

이렇게 우리는 모든 코드를 트리 형태로 표현할 수 있습니다.

코드가 계속 반복되는 구조이기 때문에 트리의 리프 노드(leaf node)를 기계 명령어로 번역하면 그 결과를 부모 노드에 똑같이 적용할 수 있습니다.

이렇게 변역하는 프로그램을 우리는 컴파일러(Compiler)라고 명명합니다.

세계에는 또 각양각색의 다양한 CPU 가 있기 때문에, 그에 맞춰 각양각색의 컴파일러를 만들기는 너무 힘든 작업입니다. 그래서, 인간은 직접 표준 명령어 집합을 정의해서 CPU 의 기계 명령어 실행 과정을 모방하는 프로그램을 작성하여 사용합니다.

이것이 바로 한번의 코드 작성으로 어디서나 코드를 실행하는 것입니다.

CPU 시뮬레이션 프로그램가상 머신(virtual machine) 또는 인터프리터(interpreter)라고 부릅니다.

컴파일러는 언어 구문에 따라 코드 구문(syntax)을 분석해서 구문 트리(syntax tree)로 만들고,
이 구문 트리를

  • C/C++ 언어처럼 기계 명령어로 번역하여 CPU 로 직접 넘기거나
  • Java 처럼 byte code 로 변환한 후, 가상 머신(virtual machine)으로 넘겨 실행합니다.

이제 이 컴파일러에 대해 좀 더 알아봅니다.

2. 고수준 언어인 소스 코드를 저수준 언어로 번역하는 컴파일러는 어떻게 작동하는가?

  • 소스 파일(source file): xxx.c, xxx.cpp, xxx.java
  • 대상 파일(target file): xxx.o, xxx.obj, xxx.class
  1. 어휘 분석(lexical analysis)
  2. 구문 분석(parsing)
  3. 의미 분석(semantic analysis)
  4. 중간 코드(IR Code) 생성
  5. 어셈블리어 코드 변환
  6. 기계 명령어로 변환
  7. 기계 명령어 데이터를 대상 파일에 저장

컴파일러는 번역가다.

int a = 1;
int b = 2;

while (a < b) {
	b = b - 1;
}

당연하게도 CPU 는 이런 고수준의 추상적인 표현을 직접 이해할 수 없다. 컴파일러가 이를 번역해줘야 한다.

먼저, 각 항목을 잘개 쪼개어 토큰(Token) 으로 만들어준다.

토큰이란 각 항목에 정보를 추가로 결합한 것을 뜻한다. 그럼 위의 코드가 아래와 같이 변한다. 각각의 줄이 하나의 토큰이다. T로 시작하는 왼쪽 열은 토큰 의미이고, 오른쪽은 각 토큰이 가지는 값이다. 이렇게

소스 코드에서 토큰을 추출하는 과정어휘 분석(lexical analysis)이라고 한다.

T_Keyword    int
T_Identifier a
T_Assign     =
T_Int        1
T_Semicolon  ;
T_Keyword    int
...
T_While      while
T_LeftParen  (
T_Identifier a
T_Less       <
T_Identifier b
T_RightParen )
T_OpenBrace  {
T_Identifier b
T_Assign     =
T_Identifier b
T_Minus      -
T_Int        1
T_Semicolon  ;
T_CloseBrace } 

이렇게 생성된 토큰(Token)들을 어떻게 처리해야 할까요?

앞에서 나왔듯이 코드는 구문(syntax)의 형태로 작성되어 있다. 이와 마찬가지로 컴파일러도 똑같이 구문에 따라 토근을 해석하면 된다.

아래는 while 키워드 토큰을 만나면 수행하는 과정이다.

이를 해석(Parsing) 이라고 한다. 한마디로, while 문을 만나면 어떻게 행동해야 하는지 행동 지침서와 같다.

WhileStatement
       ├─ 'while'
       ├─ '('
       ├─ Expression
       ├─ ')'
       └─ Statement    ← 루프 바디(Block or 단일문)

구문에 따라 해석해 낸 구조는 구문 트리로 표현하고,

트리를 생성하는 전체 과정구문 분석이라고 한다.

이 생성된 구문 트리에 이상이 없는지 확인을 하는데, 이 이상이 없는지 확인하는 과정을

의미 분석(semantic analysis)라고 한다.

구문 트리를 탐색한 결과를 바탕으로 좀 더 다듬어진 형태의

중간 코드(Intermediate Reperesentation Code, IR Code)를 생성한다.

a = 1
b = 2
goto B
A: b = b - 1
B: if a < b goto A

중간 코드를 어셈블리어 코드로 변환한다.

	mov     rax, 1      ; a = 1
    mov     rbx, 2      ; b = 2
    jmp     B           ; goto B

A:  dec     rbx         ; b = b - 1
B:  cmp     rax, rbx    ; compare a, b
	jl      A           ; if a < b goto A
	ret                 ; 함수 종료

컴파일러는 이 어셈블리어 코드를 기계 명령어로 변환한다.

마지막으로 이 기계 명령어 데이터를 대상 파일에 저장합니다.

profile
현명하고 성실하게 살자

0개의 댓글