https://juntheworld.tistory.com/26
컴파일 VS 인터프리터 (Compile VS Interpreter)
프로그램을 해독하는 방식 (소스코드를 기계어로 변환하는 방식)에는 컴파일과 인터프리터 방식 두가지가 있다. 대부분의 프로그래밍 언어가 컴파일 방식을 택하기 때문에 프로그램의 해독(소
juntheworld.tistory.com
예전에 이런 글을 썼었는데...
최근 JIT, 바이트 코드와 같은 개념이 현대 언어에 등장하면서
컴파일러와 인터프리팅을 굉장히 헷갈리게 만들고 있다.
이에 다시 정리한다.
컴파일러과 인터프리터의 구분은
단순히 통역, 번역 같은 추상적인 단어로는 이해할 수가 없다.
우선 컴파일러와 인터프리터는 고수준의 소스코드를 '어떻게' 저수준의 기계어로 바꿀까에 대한 방식의 차이다.
소스코드를 컴파일하는 현대 파이프 라인을 크게 3단계로 나누었을때
lexical Analysis (Scanning, Tokenization) -> AST 생성 -> Machine Code 생성
컴파일러와 인터프리터의 방식 차이는 AST를 만들고 난 이후에 갈린다.
그러니까.. 이 글은 위의 단계에서 AST 생성 이후 AST를 가지고 Machine Code를 생성하려는 상황을 집중하고있다고 보면 된다.
컴파일러
: (소스코드)AST를 보고 바로 기계어를 생성하는 것 (IR을 거치긴 함)
본래 프로그램의 실체라 함은 기계어 덩어리다. 이 관점에서 본다면 컴파일러가 생성한, 프로그램과 1대1 대응되는 기계어 코드뭉치가 자연스럽고 기본적이라고 할 수 있다.
이때 컴파일러는 AST를 IR이라고 하는 중간 언어로 표현하는데, 이때 IR에는 변수의 타입정보가 모두 확정되어있다. C나 C++ 같은 언어에는 이미 소스코드 딴에서 타입을 정의해주기 때문이다.
IR을 생성했다는 것은 이미 변수에 대한 타입정보를 완벽히 가지고 있다는 것을 방증한다. 그렇게 약속하기로 했다.(IR의 정의)
인터프리터
: 바이트 코드를 읽고 그에 대응하는 핸들러 함수를 호출하는 것
이때 바이트코드 핸들러 함수는 인터프리터(엔진)에 포함(이미 컴파일)되어있음
ex) python.exe node.exe 라는 인터프리터를 설치한다는 것은 바이트 코드 핸들러 함수들과 바이트 코드 opcode dispatcher를 설치한다는 의미와 같다. 이미 핸들러 함수 조각들이 인터프리터 내부에 기계어로 구현되어있음. 따라서 인터프리팅이라는 것은 bytecode를 미리 컴파일된 코드 조각들 중 어느 부분(어느 핸들러함수)에 대응시킬지 정하는 역할 뿐.
인터프리터는 "기계어로 이미 박제"된 바이너리
우리가 python.exe나 node.exe를 설치할 때, 우리는 이미 C++ 등으로 작성되어 특정 CPU용 기계어로 컴파일된 바이너리를 받는 것. 바이트코드 핸들러(Bytecode Handler): 인터프리터 내부에는 Add, Sub, Load 같은 바이트코드 하나하나를 어떻게 처리할지 정의된 함수들이 있음. 이 함수들은 개발자가 엔진을 만들 때 이미 기계어로 컴파일해둠
작동 원리: 인터프리터가 Add라는 바이트코드를 읽으면, 미리 컴파일되어 메모리에 올라와 있는 Add_Handler() 기계어 주소로 점프(Jump)해서 실행하는 방식
=> 결국 인터프리팅을 한다는 것은 바이트 코드에 대응되는 미리 컴파일된 코드들을 조립하는 개념
인터프리팅 : 바이트코드의 opcode를 미리 컴파일된 핸들러 함수에 대응시킨다는 것
1단계: 소스 코드 (JavaScript)
a + 1
2단계: 추상 구문 트리 (AST)
Node: BinaryExpression
Operator: +
Left: Identifier (a)
Right: Literal (1)
3단계: 바이트코드 생성 (Bytecode)
LdaNamedProperty a // 레지스터에 변수 a의 값을 로드 (Load Accumulator)
AddSmi [1] // 현재 값에 정수(Smi) 1을 더함 (Add Small Integer)
4단계: 바이트코드 핸들러 호출 (Dispatch)
// 인터프리터의 핵심 루프 (의사 코드)
void InterpreterLoop() {
while (true) {
Bytecode code = *pc++; // 다음 바이트코드를 읽음 (AddSmi)
// 핸들러 테이블에서 AddSmi에 대응하는 기계어 함수 주소를 찾아 점프!
dispatch_table[code]();
}
}
5단계: 핸들러 함수(기계어)실행
; AddSmi 핸들러 내부구현 (dispatch_table[code]로 호출됨)
; 1. 타입 체크 (Tagged Pointer 확인)
test rax, 0x1 ; rax(변수 a)의 하위 1비트가 0(Smi)인지 확인
jnz SlowPath ; 정수가 아니면 복잡한 연산(문자열 등)으로 점프
; 2. 실제 연산
add rax, 2 ; Smi는 비트 시프트되어 저장되므로 1 대신 2를 더함
; (V8에서 Smi는 Value << 1 상태임)
; 3. 다음 바이트코드로 이동
jmp NextBytecode ; 다시 인터프리터 루프로 복귀
인터프리터의 장점
- startup latency가 없음 ( vs 컴파일러 : 빌드시에 컴파일 -> 링크 -> 실행과정을 거침)
- 첫 줄을 실행하기까지의 시간이 거의 0에 가까움
- cross-platform (vs 컴파일러 : 기계어를 생성하는 것이기 때문에 당연히 플랫폼 종속적)
- bytecode를 이용해 소스코드 - ISA dependent한 Assembly 계층 사이에 하나의 추상화 계층을 더 만듬. 소스코드 - ByteCode - Assembly
인터프리터의 단점
- 매 코드마다 handler dispatch를 해야함
- 예를들어) a+b 를 할때
- 인터프리터 : opcode 읽고 -> 핸들러 호출 -> 실행 -> 복귀 -> opcode 읽고 -> 핸들러 호출 -> 실행 -> 복귀 -> ..
- 컴파일러 : add rax, rbx 그냥 한 줄 실행시켜버리고 마는거임
- 예를들어) a+b 를 할때
인터프리터에 대한 해석
- 인터프리터는 결국 논리적으로 구현된 CPU라고 표현할 수 있다.
- bytecode(opcode+operand로 구성됨)를 입력으로 받아서 Fetch -> Decode -> Execute의 과정을 거친다.
- 동적 타이핑의 저주 - 인간에게 편리함은 과연 마냥 좋은 가치일까?
- 고수준 언어 딴에서의 타입 정의를 하지않아도 된다는 것은(python과 node에는 변수 타입이 없다) 변수의 타입 결정을 저수준으로 내렸다는 점에서 인간에게 편리한 언어이다.
- 바이트코드를 처리하는 핸들러 함수는 바이트 코드의 opcode, operand만을 받고 핸들러 함수 내부에서 Type Check를 진행한다. 이때 Type Check, 최적화 과정에서 Type Confusion 과 같은 취약점이 다수 발견되었다.
엔진
: 프로그램 소스를 실행하기 위한 전체 시스템 (컴파일러 + 인터프리터 + etc..)
'공부 > JUN STUDY' 카테고리의 다른 글
| 인간은 프로그램의 동작을 완벽하게 이해할 수 있을까 (1) (5) | 2025.08.12 |
|---|---|
| node.js 서버 항상 켜두기 (pm2 사용방법) (0) | 2025.01.17 |
| L* Algorithm(L star Algorithm) 이란? - 개념/예제/이해하기 (1) | 2024.10.30 |
| fsb 이젠 좀 이해하기 (어떻게 arbitrary write가 되는가?) (0) | 2024.09.27 |
| VMware에서 vagrant up 할때 오류해결 & 실행법 (0) | 2024.09.26 |