[CS] Compile, Link, Load

Geon·2023년 5월 20일
1

CS

목록 보기
8/9
post-thumbnail

프로그램 속 소스코드는 Compile -> Link -> Load 의 순서로 메모리에 올려져 실행이 된다 (Compile ~ Link 까지의 과정을 컴파일 단계로 묶기도 한다.). 이 세 파트에서 무엇이 일어나는지 알아보자.

Compiler

특정 프로그래밍 언어(Hign-level Language)로 쓰여 있는 코드를 어셈블리어나 기계어 등의 Low-level Language로 번역해주는 프로그램이다. 이 때, 번역하는 과정이 'Compile'이며, Compile 전 원래의 코드를 '소스 코드(Source Code)'라 칭하고, 컴파일러에 의해 번역이 된 코드를 '목적 코드(Object Code)'라고 칭한다.

컴파일러가 번역할때는 아래와 같은 조건을 따라야 한다.
(1) 컴파일러는 컴파일 과정에서 프로그램의 뜻을 보존하여야 한다.
(2) 컴파일러는 입력으로 들어온 소스코드를 목적코드로 변환할때 실용적인 측면에서 이를 개선해야 한다.


두 조항 모두 당연한 말이지만, 하나씩 살펴보자.
(1)번 조항 : 우리가 1+1 연산의 결과를 출력하는 프로그램을 JAVA 코드로 작성해서 이를 실행했다고 가정해보자, 프로그램이 실행되는 과정에서 컴파일러가 이 코드의 의미를 멋대로 변경하여 1+2 연산을 하는 의미를 갖는 코드로 만들어서야 되겠는가? 이 조항은 그것에 대한 이야기를 하는것이다.
(2)번 조항 : 컴파일이 필요한 이유와 연결지어 생각하면 이해가 쉽다. 앞서 말했듯 컴파일은 고수준->저수준으로의 변환을 통해 기계가 이를 이해하게 하거나, 언어(JAVA) -> 언어(Python) 등 같은 수준에서 다른 언어로의 변환을 하기 위해 사용하곤 한다. 즉, 이러한 컴파일러의 목적이나 쓰임새가 없다면 굳이 변환을 할 이유가 없다는 것이다.

Linker

컴파일의 결과물로 나온 목적 코드들과 데이터, 라이브러리 등을 엮어서 실행 가능한 실행 파일(exe,dmg 등)으로 만드는 것을 Link, Link를 하는 프로그램을 Linker라고 칭한다.
본인은 해당 설명에서 라이브러리가 왜 필요할까? 라고 의문점이 들었는데, System.out.println()이라는 함수가 소스코드에 존재하는 상황을 가정하면 해당 함수를 CPU에서 읽었을 때 도대체 뭐하는 녀석인지 알 길이 없으므로 해당 함수에 대한 정보를 가지고 있는 다른 프로그램의 라이브러리에서 정보를 얻어야만 의미를 알 수 있기 때문일 것이다. 링크의 방식에는 정적 링크와 동적 링크가 있는데, 여기를 참고하자! 설명이 엄청 깔끔하다.

Loader

링커가 만든 실행 파일을 메모리에 올리는 것을 Load, 이를 수행하는 프로그램을 Loader라고 한다. Loader는 아래와 같은 4단계에 걸쳐 Load를 수행한다.

1) Allocation(할당)

실행 프로그램을 실행시키기 위해 메모리의 공간을 확보한다.

2) Linking(연결)

앞서 말한 Link가 필요한 경우처럼 다른 프로그램이나 프로그램의 라이브러리들과의 연결이 필요할 때 해당 타 프로그램의 메모리 시작주소를 호출한 부분에 등록하여 연결한다. (이때 Linker가 이를 도와준다.)

3) Relocation(재배치)

주로 메모리의 한계가 있을 때 일어난다. 메모리를 많이 차지하는 프로그램이 메모리로 올라갔을때, 공간을 효율적으로 쓰기 위해 기존에 메모리에 존재하던 프로그램을 다른 곳으로 재배치 시킨다. 즉, 식당에 사람이 없을때는 인원수에 상관없이 아무 테이블에 앉아도 되기에 2명이 4인 테이블에 가서 밥을 먹고 있던 중에, 4명 단체 손님이 오게되면 식당 측에서 기존의 2명에게 2인용 테이블로 옮겨달라고 요청하고, 4명을 4인 테이블에 앉히는 상황을 떠올리면 이해가 쉬울것이다.

4) Loading(적재)

프로그램을 할당된 메모리에 실제로 옮기는 것이다.
로더의 종류는 주로 아래와 같이 나뉜다.

Compile And Go Loader

  • 별도의 로더 없이 컴파일러가 로더의 기능까지 수행하는 방식이다.
  • Linking 기능은 수행하지 않고 Allocation, Relocation, Loading 작업을 모두 컴파일러가 담당한다.

Absolute Loader(절대 로더)

  • 목적 코드를 기억장소에 적재시키는 기능만 수행하는 로더로, 로더 중 가장 간단한 프로그램으로 구성되어 있다.
  • 메모리 주소 할당, 연결을 프로그래머가 직접 지정하기 때문에 한번 지정한 주기억장소의 위치의 변경이 어렵다.

Direct Linking Loader(직접 연결 로더)

  • 가장 일반적인 로더로, 로더의 기본 기능 네 가지를 모두 수행하는 로더이다.
  • Relocation Loader(재배치 로더), Relative Loader(상대 로더)라고도 한다.

Dynamic Loading Loader(동적 적재 로더)

  • 프로그램을 한꺼번에 적재하는 것이 아니라 실행 시 필요한 부분만을 적재하고 나머지 부분은 보조기억장치에 저장해두는 것으로, 호출 시 적재(Load-On-Call)라고도 한다.
  • 적재할 프로그램 크기가 메모리 크기보다 크거나, 공간을 상당히 차지할 때 유리한 방식이다.



C Compile procedure

나는 현재 JAVA위주로 공부하고 있지만, 아무래도 기계와 가까운 언어인 C언어 기준으로 Compile 과정이 설명되어 있는 자료가 많고, 이 또한 알아두어야 한다고 생각하기에 먼저 C 언어의 컴파일 과정부터 가볍게 살펴보고 자바의 방식과 비교해보자


1. 전처리기(preprocessor)
컴파일러가 소스 코드를 컴파일하기 전에 전처리기가 실행된다. 전처리기는 소스 코드 내에서 #으로 시작하는 전처리 지시문을 처리하며, 이를 통해 헤더 파일을 포함하거나 매크로를 정의하거나 조건부 컴파일 등을 수행한다.

2. 컴파일러(compiler)
전처리된 소스 코드가 컴파일러에 의해 컴파일된다. 컴파일러는 소스 코드를 분석하고, 중간 언어 코드(Assembly code, object code)를 생성한다. 이 중간 언어 코드는 컴퓨터에서 직접 실행할 수 없다.

3. 어셈블러(assembler)
생성된 중간 언어 코드를 어셈블러가 어셈블하여 목적 코드(object code)를 생성한다. 목적 코드는 컴퓨터에서 직접 실행 가능하다. 하지만 아직 링크 과정이 필요하다

4. 링커(linker)
목적 코드와 라이브러리 코드를 링크하여 실행 가능한 바이너리 파일을 생성한다. 링커는 모든 함수와 변수에 대한 참조를 해결하고, 실행 파일에 필요한 모든 코드와 데이터를 결합하여 하나의 실행 가능한 파일을 만든다.


JAVA Compile procedure

자바 컴파일 방식의 특이한 점은 중간에 'Byte Code'로 변환을 한다는 것이다. JVM이 읽을 수 있는 바이트코드로 변환을 해놓고, 이것을 JVM에게 주면 JVM이 거기서 한번 더 변환하여 실행파일로 만드는 형태이다. 운영체제의 종류와 상관없이 JVM이 이 중간에 한번 변환된 바이트코드를 읽어서 실행하면 되기때문에 자바가 운영체제에 독립적이라는 이야기를 하는 것이다.


1. JAVA Code
개발자가 자바 소스코드(.java)를 작성

2. Compliler
자바 컴파일러(Java Compiler)가 자바 소스파일을 컴파일. 이때 나오는 파일은 자바 바이트 코드(.class)파일로 아직 컴퓨터가 읽을 수 없고, 자바 가상 머신(JVM)이 이해할 수 있는 코드

3. Byte Code
컴파일된 바이트 코드를 JVM의 클래스로더(Class Loader)에게 전달

4. Class Loader
클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올린다. 이후 아래의 과정이 진행된다.

클래스 로더 세부 동작
1. 로드
클래스 파일을 가져와서 JVM의 메모리에 로드

2. 검증
자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사

3. 준비
클래스가 필요로 하는 메모리를 할당. (필드, 메서드, 인터페이스 등등)

4. 분석
클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경

5. 초기화
클래스 변수들을 적절한 값으로 초기화

6.실행엔진(Execution Engine)
JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행. 이때, 실행 엔진은 아래 두가지 방식으로 나뉜다.

Interpreter
바이트 코드 명령어를 하나씩 읽어서 해석하고 실행. 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가진다.

Just-In-Time Compiler
인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식으로 동작. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠르다.



Compiler vs Interpreter

프로그래밍 언어가 컴파일러 방식이냐 인터프리터 방식이냐에 대한 이야기를 많이 들어봤을 것이다. 이 둘의 차이를 간단히 알아보자. (반드시 두 방식중 한가지 방식만 채택해야 하는 것은 아니다. 위에서 살펴봤듯이 자바는 프로그램을 실행하는 과정에서 컴파일과 인터프리팅 방식이 모두 쓰일 수 있다.)


Compiler
프로그램의 Source Code 전체를 스캔하여 이를 Object Code로 변환을 하고나서 이를 실행하는 방식인 반면,
Interpreter
우선 바로 실행하여 Source Code의 한줄 한줄씩을 그자리에서 기계어로 변환하면서 변환한 것을 바로 실행하는 방식으로 동작한다.

즉, 컴파일러는 Source Code -> Object Code -> 실행 파일로 변환하고 이것을 메모리에 올리지만, 인터프리터는 Source Code 상태 자체로 바로 메모리에 올려서 한줄 한줄씩 실행하는 것이다

장단점

Compiler
컴파일러의 경우 Source Code 전체를 Object Code로 바꾸고, 그것에 Link 작업을 더하여 실행파일로 만들고 나서 메모리에 올리기때문에 프로그램이 실행되기 전까지 초기 작업에 더 많은 시간과 자원을 쓴다는 단점이 있다. 또한 컴파일러는 전체 Source Code를 컴파일 한 후에 에러를 알려주기 때문에, 에러를 수정하고, 수정이 잘 되었는지 확인하기 위해서는 또 전체 소스 코드를 컴파일 하는 작업을 거쳐야 한다. 이는 컴파일에 상당한 시간이 소모되는 덩치가 큰 프로그램의 경우 상당히 번거로운 경우가 될 수 있다.
하지만 이렇게 초반에 기계어로 실행파일까지 만들어 놓았기 때문에, 실행할때는 인터프리터보다 빠르게 실행할 수 있다는 장점이 있다.

Interpreter
인터프리터의 경우 앞서 설명했듯 변환과 실행을 실행하면서 동시에 진행하기 때문에 프로그램의 실행 속도는 컴파일러를 사용하는 방식에 비해 느릴수 밖에 없다. 즉 속도 부분에서 실행까지에 도달하는 시간은 인터프리터가 빠르지만, 실행이 되고나서 부터는 컴파일러의 승리라는 것이다. 하지만 인터프리터의 경우 Source Code를 한줄씩 실행하기 때문에 에러를 한줄 단위로 바로바로 알려주어 실시간으로 코드 수정이 가능하단 점에서 개발 편의성이 높다는 장점을 가진다.

References:
https://ko.wikipedia.org/wiki/컴파일러
https://jess2.tistory.com/72
https://gyoogle.dev/blog/computer-language/Java/컴파일%20과정.html
youtube '2018년- 1학기 중앙대 김중헌 교수의 컴파일러' 강의

profile
별에 별 지식 저장해놓고 꺼내먹기📚

1개의 댓글

comment-user-thumbnail
2024년 4월 19일

😆👍👍👍

답글 달기