[1] 컴파일러&인터프리터
프로그래밍 언어의 구분
1. low-level Langueages
- 기계어 프로그램 > 실행 코드 or 목적 코드
- CPU 종류마다 고유의 기계어가 존재
2. High-level Languages
- 고급언어 프로그램> 소스코드
- 인간의 자연언어와 유사하게 표현 > 이해도 ^
- 프로그래머가 기계의 세부사항을 알 필요 x
- c, c++, C#, java, basic, python, Lisp 등등
번역
사용자가 작성하는 고급언어에서 컴퓨터가 이해할 수 있도록 기계어로 번역시킨다.
방법1. 컴파일러: 전체 프로그램을 먼저 번역한 후 실행
방법2. 인터프리터: 한 라인별로 번역한 후 바로 실행
컴파일 언어(c, c+, java)
&소스코드 -> 컴파일러(전처리(preprocess)> 컴파일(Compile)) -> 목적코드 -> 링커 -> 실행코드 -> 런타임&
이 과정은 프로그램이 실행 가능한 코드로 변환되는 과정이다.
1. **소스코드(Source Code)**
- 개발자가 작성한 프로그래밍 언어로 된 코드입니다.
- 예: C, C++, Java, Python 등.
2. **컴파일러(Compiler)**
- 소스코드를 기계어로 변환합니다.
- 이 과정에서 문법 오류를 검출하고, 소스코드를 목적코드로 변환합니다.
3. **목적코드(Object Code)**
- 컴파일러에 의해 생성된 중간 형태의 기계어 코드입니다.
- 이 코드는 완전하지 않아 바로 실행할 수 없습니다.
- 다른 라이브러리나 모듈과 결합이 필요합니다.
4. **링커(Linker)**
- 목적코드를 라이브러리와 결합하여 실행 가능한 코드(실행 파일)를 생성합니다.
- 예: C 언어에서 `printf` 함수는 외부 라이브러리와 연결됩니다.
- 링커 작업과정에서 운영체제별 특징이 있는 고유의 라이브러리 요구(*3) >> 프로세서 종속성을 일으킴 >> 자바에선 바이트코드로 번역되므로 해당 없음.
5. **실행코드(Executable Code)**
- 링커가 생성한 최종 실행 파일입니다.
- 운영체제에서 실행 가능한 형태입니다.
6. **런타임(Runtime)**
- 실행코드가 실제로 실행되는 동안의 환경입니다.
- 이 과정에서 메모리 할당, CPU 작업, 시스템 호출 등이 이루어집니다.
- 런타임 오류(Runtime Error)가 발생할 수도 있습니다(예: 메모리 접근 오류).
컴파일 언어의 특징은 기계어를 사용하는 컴파일 언어는 cpu에 의존적이므로. 컴파일된 코드가 특정 프로세서에 종속된다.
그 이유는 여러가지가 있는데 (1)컴파일러는 특정 프로세서 아키텍쳐(ISA)에 맞게 기계어 코드로 변환한다. 어떤 프로세서이냐에 따라(x86, ARM, RISC-V 등) 명령어 집합이 다르다고 한다. 만약 x86 아키텍처용으로 컴파일된 코드는 ARM 아키텍처에서는 실행되지 않는다. (2)엔디안(Endianness) 차이에 따라 실행이 불가능하다고 한다. 프로세서는 데이터를 메모리에 저장하거나 읽을 때 바이트 순서를 다르게 처리할 수 있다고 한다. 중요한 바이트를 먼저 저장하느냐 덜 중요한 바이트를 먼저 저장하느냐에 따라 방식이 달라짐(MSB, LSB)(나중에 엔디안에 대한 설명 추가적으로 적음) 만약 컴파일된 코드가 한 프로세서에서 다른 엔디안 방식의 프로세서로 옮겨진다면 데이터가 올바르게 해석되지 않는다. (3) 컴파일된 코드는 운영체제(OS)와의 인터페이스(시스템 호출, 라이브러리 등)에 따라 달라진다. 같은 프로세서 아키텍처라도 (*1) Windows, Linux, macOS에서 사용하는 실행 파일 포맷(EXE, ELF 등)이나 동작 방식이 다르기 때문이다.
>> 이로써 JAVA, .NET 같은 언어는 바이트코드로 번역하여 플랫폼에 독립적인 실행환경(가상 머신, JVM)을 제공한다. 이와같이 컴파일된 코드들은 여러 종류의 프로세서를 자유롭게 옮겨다니며 사용될 수 있다.(그러나 순수 기계어에 비해 실행 속도가 다소 느릴 수 있다)
#JAVA 번역과정(바이트코드)
javaCompiler javaInterpreter
.java---------->.class(바이트코드)----------->Machine Language
- 런타임 이전 컴파일러를 이용해 기계어가 아닌 바이트코드로 번역됨
- 컴파일 시점에 의해 컴파일 언어로 분류됨
- 바이트코드는 기계어, OS에 독립적이라는 장점
- VM이 각 환경에 맞는 실행코드 생성
인터프리터 언어 (python, javascript)
&소스코드 -> 런타임 시작 -> 인터프리터-> 바이트코드 -> VM -> 기계어코드&
### 1. **소스코드(Source Code)**
- 개발자가 작성한 고수준 프로그래밍 언어로 된 코드입니다.
- 예: Python, Java, JavaScript 등.
---
### 2. **런타임 시작(Runtime Start)**
- 프로그램 실행이 시작되는 시점입니다.
- 소스코드가 곧바로 실행되지 않고 인터프리터 또는 컴파일러가 동작합니다.
---
### 3. **인터프리터(Interpreter)**
- 소스코드를 한 줄씩 읽어서 실행하거나, 바이트코드로 변환합니다.
- 예: Python 인터프리터는 `.py` 파일을 실행할 때 바이트코드(`.pyc`)로 변환 후 처리합니다.
---
### 4. **바이트코드(Bytecode)**
- 소스코드를 중간 형태로 변환한 코드입니다.
- 플랫폼 독립적이며, 가상 머신(VM)이 실행 가능한 형태입니다.
- 예:
- Python은 `.pyc` 바이트코드를 생성.
- Java는 `.class` 바이트코드를 생성.
---
### 5. **가상 머신(VM, Virtual Machine)**
- 바이트코드를 해석하고 실행하는 소프트웨어 기반의 프로세서입니다.
- 예:
- **Java**: JVM(Java Virtual Machine)
- **Python**: CPython, PyPy 등.
- VM은 바이트코드를 프로세서가 이해할 수 있는 기계어로 변환하거나, 해석하면서 실행합니다.
---
### 6. **기계어 코드(Machine Code)**
- 가상 머신 또는 JIT(Just-In-Time) 컴파일러가 바이트코드를 기계어로 변환하여 실행합니다.
- 기계어는 프로세서가 직접 실행할 수 있는 명령어입니다.
---
### 특징:
- **이식성**: 바이트코드는 플랫폼 독립적이므로, 다양한 운영체제 및 프로세서에서 실행할 수 있습니다(VM만 있다면).
- **실행 속도**: 인터프리터 방식은 즉시 실행 가능하지만, 순수 기계어에 비해 느릴 수 있습니다. 이를 개선하기 위해 JIT 컴파일 방식을 사용하는 경우도 있습니다.
- java, python 모두 바이트코드를 사용하기 때문에 vm이 필요하다.
컴파일러와 인터프리터의 차이
컴파일러 | 인터프리터 | |
번역단위 | 전체 | 한줄 |
실행속도 | 상대적으로 빠름 | 상대적으로 느림 |
번역속도 | 상대적으로 느림 | 상대적으로 빠름 |
목적 파일(실행파일) 생성유무 | 생성 | 생성 안 함 |
메모리 할당 | 할당 받음 | 사용 안 함 |
[2] 전처리기
프로그램 처리과정
전처리기
소스코드가 컴파일되기 전에 실행되는 도구로, 소스코드를 전처리하여 컴파일러가 이해할 수 있는 형태로 변환함.
- #으로 시작
- 마지막에 ;을 붙이지 않음
- 한 줄을 넘길 경우, \를 사용하여 다음 줄에 이어짐을 표시함
종류
- 파일처리 : #include
- 문자열 또는 매크로 정의 : #define, #undef
- 조건부 컴파일 : #if, #ifdef, #ifndef, #else, #elif, #endif
- 에러 처리 : #error
- 행번호 제어 : #line
- 컴파일 옵션 처리 : #pragma
### **전처리기의 주요 역할**
1. **매크로 처리(Macro Processing)**
- 매크로 정의와 치환 작업을 수행합니다.
- 예:
```c
#define PI 3.14
```
소스코드에서 `PI`를 `3.14`로 치환.
2. **파일 포함(File Inclusion)**
- `#include` 지시자를 사용해 외부 파일(헤더 파일 등)을 포함시킵니다.
- 예:
```c
#include <stdio.h>
```
`stdio.h` 파일의 내용이 소스코드에 삽입됨.
3. **조건부 컴파일(Conditional Compilation)**
- 특정 조건에 따라 코드의 일부를 포함하거나 제외합니다.
- 예:
```c
#ifdef DEBUG
printf("Debugging mode\n");
#endif
```
`DEBUG`가 정의된 경우에만 해당 코드가 컴파일됨.
4. **주석 제거(Comment Removal)**
- 소스코드에서 주석을 제거합니다.
- 예:
```c
// This is a comment
```
주석은 최종 컴파일 단계에서 제거됨.
5. **심볼 정의 및 참조(Symbol Definition and Referencing)**
- `#define`으로 심볼을 정의하거나, `#undef`로 심볼 정의를 해제합니다.
- 예:
```c
#define MAX 100
#undef MAX
```
### **전처리기의 중요성**
- **코드 재사용성**: 헤더 파일을 사용하여 여러 파일에서 코드를 공유 가능.
- **가독성 향상**: 매크로와 조건부 컴파일을 통해 코드 가독성을 높임.
- **효율성**: 반복적인 코드를 제거하고, 플랫폼별로 조건부 코드를 쉽게 관리 가능.
c 프로그램에서 사용된 모든 id(키워드 제외)는 미리 정의되어야 한다.
- id는 변수명,함수명 등의 이름을 구별하기 위해 부여함
- 키워드(예약어)는 if, for, while, else 등과 같이 컴파일러에게 미리 알려져있는 단어이므로 정의할 필요 X
- 함수명 중에서 main은 프로그램의 시작위치를 가리키는 id로 약속되어있어 유일하게 정의가 피료없음
- 나머지 프로그래머가 지정하는 모든 id는 사용하기 전에 정의되어있어야 컴파일에러가 발생하지 않음
id 정의가 필요한 이유
: 컴파일은 고급언어로 작성된 프로그램은 컴퓨터가 처리하도록 기계어로 번역하는 과정임을 위에서 언급했다. 제대로 된 기계어를 생성하기 위해서 id가 일반 변수이름인지, 포인터 변수인지 배열이름인지 함수 이름인지 구별되어야한다. 예시로 변수 선언할 때 변수 타입에 따라 저장방법, 연산 명령어들이 달라진다. ( int, dounle, cahr에 따라 기계어의 형태가 모두 다름)
매크로 전처리기 #define
매크로는 단순 치환 기능을한다.
매크로 함수의 치환구문 예시
& #define 함수이름(인자) 치환구문 &
#include <stdio.h>
#define square(x) x*x
int main()
{
printf("square(5) : %d\n", square(5));
return 0;
}
치환 결과 : printf("square(5) : %d\n", 5 * 5);
함수와 매크로의 차이
>> 함수(Function)와 매크로(Macro)는 코드의 재사용성과 간결성을 높이는 데 사용되지만, 동작 방식과 특성이 다르다.
(1) 정의와 동작방식
구분 | 함수 | 매크로 |
정의 | 실행 가능한 코드블록 정의, 호출시 실행 | 전처리 단계에서 단순한 텍스트 치환으로 동작 |
처리시점 | 컴파일 시 처리됨 | 전처리기 단계에서 치환 |
(2) 매개변수 처리
구분 | 함수 | 매크로 |
매개변수 | 매개변수를 받아 실제값을 계산함 | 매개변수를 그대로 텍스트로 치환한다 |
예 | int add(int x, int y) {return x+y;} ... |
#define ADD(x, y) (x + y) ... |
매크로는 단순 치환방식이므로 괄호를 명확히 처리하지 않으면 오류가 발생한다.
### **3. 실행 속도**
| **구분** | **함수** | **매크로** |
|------------------|--------------------------------------------------------|---------------------------------------------------------|
| **속도** | 호출 시 함수로 이동하고, 스택 관리 등이 필요합니다. | 치환된 텍스트가 코드에 직접 삽입되므로 호출 오버헤드가 없습니다. |
---
### **4. 디버깅**
| **구분** | **함수** | **매크로** |
|------------------|--------------------------------------------------------|---------------------------------------------------------|
| **디버깅** | 함수 호출로 처리되므로 디버깅 시 추적하기 쉽습니다. | 매크로는 텍스트로 치환되므로 디버깅이 어렵습니다. |
---
### **5. 타입 안전성**
| **구분** | **함수** | **매크로** |
|------------------|--------------------------------------------------------|---------------------------------------------------------|
| **타입 체크** | 컴파일러가 매개변수 타입을 체크합니다. | 타입 체크가 없고 단순 치환이므로 오류가 발생할 가능성이 큽니다. |
---
### **6. 코드 크기**
| **구분** | **함수** | **매크로** |
|------------------|--------------------------------------------------------|---------------------------------------------------------|
| **코드 크기** | 함수는 코드가 한 번만 작성되고 호출될 때마다 참조됩니다. | 매크로는 치환된 코드가 반복적으로 삽입되므로 코드 크기가 증가할 수 있습니다. |
---
- 안전성과 가독성이 중요한 경우 사용 (특히 복잡한 계산, 디버깅이 필요한 경우). >> 함수
- 간단한 작업, 호출 오버헤드 제거가 중요한 경우 사용 (예: 상수 정의). >> 매크로
헤더가드
동일한 헤더 파일이 여러번 포함되어 발생하는 중복 정의 오류를 방지함
헤더파일의 중복삽입의 경우
#indef 파일명_H
#define 파일명_H
으로 중복삽입을 해결할 수 있다.
동작 원리
- 헤더 파일이 처음 포함되면 #ifndef 조건이 참(True)이므로, 파일 내용이 처리되고 매크로(파일명_H)가 정의됩니다.
- 같은 헤더 파일이 다시 포함되더라도 매크로(파일명_H)가 이미 정의되어 있기 때문에, #ifndef 조건이 거짓(False)이 되어 파일 내용이 무시됩니다.
'시스템프로그래밍' 카테고리의 다른 글
[시스템프로그래밍] 컴파일과 런타임 스택 (0) | 2024.12.01 |
---|