시스템프로그래밍

[시스템프로그래밍] 컴파일러&인터프리터, 전처리기

bebeghi3356 2024. 11. 30. 19:55

[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

//example sample
#iclude <stdio.h>
int main() {
int sum = 0;
int i, num, average;
for(i = 0; i < 10; i++)
{
scanf("%d", &num);
sum = sum + num*10;
}
average = sum/10;
printf("average of %d numbers: %d\n", 10, average);
 
}

//매크로 사용
#iclude <stdio.h>
#define N 10

int main() {
int sum = 0;
int i, num, average;
for(i = 0; i < N; i++)
{
scanf("%d", &num);
sum = sum + num*10;
}
average = sum/N;
printf("average of %d numbers: %d\n", N, average);
 
}

//지역변수 사용(비교1)
#iclude <stdio.h>
int main() {
int n =10;
int sum = 0;
int i, num, average;
for(i = 0; i < n; i++)
{
scanf("%d", &num);
sum = sum + num*10;
}
average = sum/n;
printf("average of %d numbers: %d\n", n, average);
 
}

//상수 사용(비교2)
#iclude <stdio.h>
int main() {
const int N =10;
int sum = 0;
int i, num, average;
for(i = 0; i < N; i++)
{
scanf("%d", &num);
sum = sum + num*10;
}
average = sum/N;
printf("average of %d numbers: %d\n", N, average);
 
}

 

매크로는 단순 치환 기능을한다.

매크로 함수의 치환구문 예시

& #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

으로 중복삽입을 해결할 수 있다.

 

동작 원리

  1. 헤더 파일이 처음 포함되면 #ifndef 조건이 참(True)이므로, 파일 내용이 처리되고 매크로(파일명_H)가 정의됩니다.
  2. 같은 헤더 파일이 다시 포함되더라도 매크로(파일명_H)가 이미 정의되어 있기 때문에, #ifndef 조건이 거짓(False)이 되어 파일 내용이 무시됩니다.