[Operating System] 프로그램 컴파일

권영태·2025년 4월 28일

Operating System

목록 보기
8/20

프로그램 컴파일

프로그램이 CPU에서 실행되기 위해서는 메모리로 가져와야 하고, 프로세스의 컨텍스트 안에 배치되어야 한다.

컴파일 언어와 인터프리터 언어

  • 컴파일 언어: 컴파일러를 통해 소스 코드를 실행 가능한 기계어 코드의 실행 파일을 생성하는 언어
    • 전체 소스 코드를 한번에 읽고 모두 기계어로 번역한다.
    • 실행 속도가 빠르지만 OS와 CPU 아키텍처에 맞게 컴파일 되는 종속적인 단점이 존재한다.
  • 인터프리터 언어: 미리 기계어로 번역하지 않고 실행 중에 인터프리터 프로그램을 통해 소스 코드를 한 줄씩 해석해서 실행하는 언어
    • 매번 코드를 실행 코드를 해석해야 해 실행 속도가 느리지만, OS와 CPU 아키텍처에 구애받지 않는다.

프로그램 컴파일부터 실행까지의 과정

  • 소스 파일 -> 오브젝트 파일 -> 하나의 (이진) 실행 파일 -> 메모리에 프로그램 적재

소스 파일 -> 오브젝트 파일

컴파일러에 의해서 소스 코드가 기계어로 바뀌는 즉 컴파일되는 작업이다.

  • C언어로 만들어진 소스 파일(main.c)가 컴파일러에 의해 오브젝트 파일(main.o)로 컴파일 된다.
  • 변환된 오브젝트 파일은 어떤 물리적 메모리 위치에도 적재될 수 있어 '재배치 가능 오브젝트 파일'이라고도 한다.
  • 재배치 가능 오브젝트 파일은 추후 링커에 의해서 실제 주소로 적재된다.

오브젝트 파일 -> 하나의 실행 파일

링커에 의해서 여러 개의 오브젝트 파일(main.c)들과 여러 라이브러리들이 하나의 실행 파일(main)로 결합되는 작업이다.

  • 오브젝트 파일의 변수나 함수 호출 주소 등을 재배치(== 실제 주소 할당)한다.
  • 외부 라이브러리나 필요한 코드를 실행파일에 모두 합치는데 이를 정적 (라이브러리) 링크 방식이라고 한다.

    동적 라이브러리 링크 방식

    현대 시스템은 프로그램이 로드될 때 라이브러리를 동적으로 연결하는 동적 라이브러리 링크 방식을 채택한다. 동적 라이브러리 링크 방식은 바로 사용하지 않는 라이브러리를 미리 링크하지 않고, 사용 시 링크해 메모리와 디스크 공간을 절약할 수 있다.

하나의 실행 파일 -> 메모리에 프로그램 적재

커널 내부 로더에 의해서 프로그램이 메모리에 적재되는 작업이다.

  • /main 명령어나 GUI(더블클릭)이 수행되면 다음과 같은 작업이 일어난다.
    • shell이 fork()를 통해 새 프로세스 생성
    • shell이 exec()를 호출해 로더가 실행 파일을 메모리에 적재
    • 실행 파일의 진입점부터 실행 시작

      fork(), exec() 시스템 콜고 로더를 통해서만 프로세스를 적재할 수 있을까?

      특수한 경우 커널 개발자가 직접 메모리에 적재할 수 있지만 보통 로더에 의해서 적재된다.

링커와 로더의 차이!

역할의 차이로는 여러 오브젝트 파일과 라이브러리를 결합해 하나의 실행 파일로 만드는 링커와 하나로 결합된 실행 파일을 메모리에 적재해 실행을 준비하는 로더로 구분할 수 있다.
즉, 링커는 컴파일 이후와 실행 전 사이에 수행되고, 로더는 프로그램 실행 시점에 수행된다.

Java는 어떻게 컴파일 되고 실행될까?

Java는 Java 컴파일러에 의해 소스 코드(Main.java)가 바이트 코드(Main.Class)로 컴파일 된다.
컴파일 된 바이트 코드는 JVM이 실행하는데 인터프리터 방식 또는 JIT 컴파일러에 사용해 네이티브 코드로 변환시켜 실행할 수 있다.

JIT

JIT(Just-In-Time)은 Java 메서드가 처음 호출될 때 바이트 코드를 네이티브 머신 언어로 변환해 실행 속도를 높이는 컴파일러다. 변한된 네이티브 머신 언어는 캐싱되어 이후 재호출 시 바이트 코드 해석없이 캐싱된 네이티브 머신 언어로 바로 실행된다.

public class Hello {
    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            int result = compute(i);
        }
    }

    public static int compute(int value) {
        return value * value; 
    }
}

compute(0) 호출 시 compute(i) 바이트 코드를 네이티브 머신 언어로 변환시키고 이를 캐싱한다.
이후 compute(1)~compute(999999)는 캐싱된 네이티브 머신 언어를 갖고 바로 실행해 더 빠른 실행 속도를 보여준다.

Python은?!

Python은 CPython, Jython, PyPy등의 다양한 구현체가 있는데 각 구현체 별로 실행 과정이 다르다.

  • CPython은 바이트 코드를 인터프리팅해 실행하며, 가장 표준적인 구현체다.
  • Jython은 Java 바이트 코드로 변환해 JVM에서 실행하며, Java 환경과 통합을 강조한다.
  • PyPy는 성능 최적화를 위해 JIT 컴파일러에서 실행한다.

학습하며 정리한 글이기 때문에 혼용된 표현 또는 잘못된 내용이 있을 수 있습니다.
만약, 발견하신 경우 댓글을 통해 알려주신다면 진심으로 감사드립니다.

profile
GitHub : https://github.com/dudxo

0개의 댓글