본문 바로가기
정보보안/Reversing

[Reversing] Code to Execution(x86)

by Yoonsoo Park 2025. 4. 7.

먼저 compile 방식과 interpreter 방식의 차이를 알아보자.

컴파일 코드 (Compiled Code)

  • 설명: 소스 코드를 한 번에 기계어(바이너리)로 변환한 후 실행.
  • 컴파일러 사용: 예) gcc, javac
  • 속도: 보통 빠름 (이미 기계어로 변환되어 있어서)
  • 예시 언어: C, C++, Go, Rust
  • 장점:
    • 실행 속도가 빠름
    • 최적화가 잘 됨
  • 단점:
    • 코드 변경 시마다 다시 컴파일 필요
    • 디버깅이 약간 어려울 수 있음

인터프리터 코드 (Interpreted Code)

  • 설명: 소스 코드를 한 줄씩 읽어서 바로 실행함.
  • 인터프리터 사용: 예) python, node
  • 속도: 보통 느림 (한 줄씩 읽어서 실행하므로)
  • 예시 언어: Python, JavaScript, Ruby, Bash
  • 장점:
    • 빠르게 실행 결과 확인 가능
    • 디버깅이 쉬움
  • 단점:
    • 실행 속도가 느릴 수 있음
    • 코드 보안에 취약할 수 있음 (소스 코드 그대로 배포되는 경우)

 

우리가 알아볼 것은 C, C++ 과 같은 컴파일 기반 언어의 빌드 과정이다. 

 

컴파일 기반 언어의 빌드 과정은 

Pre-processeor => Compiler => Assembler => Linker 로 구성돼있다. 

 

각 단계를 하나씩 살펴보자.

 

Pre-processor(전처리기)

pre-processor는 흔히 #으로 표기하는 전처리 지시문들을 처리하는 역할을 한다. 대표적으로 #include(헤더 파일), #define(매크로 변환), #ifdef(조건부 컴파일) 등이 있다. 

 

pre-processor는 #include에서 명시한 헤더 파일(.h) 내부의 함수 선언, 전역 변수, 타입 정의 등을 현재의 소스 파일로 복사해오고, #define 에서 정의한 매크로를 실제 값으로 바꾼다(치환). 

 

예를 들면 #define PI 3.14 라는 매크로를 정의했을 때, 해당 소스코드 내에서 PI라는 텍스트를 전처리 과정에서 3.14로 치환하는 것이다. 

 

Compiler

 컴파일러는 전처리된 코드를 어셈블리어로 변환하는 역할을 한다. 간단하게 말하면 고급어(high-level language)를 저급어(low-level language)로 변환한다고 볼 수 있다. 컴파일 방식은 compiler, architecture에 따라 번역하는 방식이 다르다. 

 

Assembler

어셈블러는 저급어를 기계어(machine code)로 바꿔 object file(.o)을 생성하는 역할을 한다. 

 

Linker

마지막으로 링커는 여러 개의 오브젝트 파일과 라이브러리를 묶어서 실행 파일(.exe, ELF)를 생성하는 역할을 한다. 링크 단계에서 링커는 함수 호출 처리(printf 같은 외부 함수), 메모리 주소 할당, 심볼 테이블 연결과 같은 작업을 한다.

 

우리가 흔히 말하는 빌드(build)는 compile + link 라고 보면 된다.