프로그램은 어떻게 실행될까?
프로그래밍언어론 수업을 하면 첫주의 주제가 "프로그램은 어떻게 실행될까"라는 것이다. 보통 시스템소프트웨어 또는 시스템프로그래밍 과목에서 다루어지는 내용이다. 기본적이고 약간은 진부한 주제지만 C나 자바 코드를 짜서 컴파일하고 돌리는 개발 과정과 작성된 프로그램이 실행되는 과정을 잘 이해하는 것은 좋은 소프트웨어개발자가 되기 위해 매우 중요하다. 이 글에서는 가능한 한 쉽게 간략히 그 과정을 한번 따라가 보려고 한다. 재미없지만 중요한... 그런 내용이다.
먼저 컴파일 방식의 실행에 대해 살펴보고 인터프리터 방식의 실행에 대해서는 하단에서 따로 설명한다.
먼저 소스 프로그램을 작성하고 컴파일하는 과정이 필요하다. 편집기를 이용해서 언어의 구문에 맞게 작성하면 컴파일러가 그것을 실행할 수 있는 프로그램으로 바꾼다. 실행할 수 있는 프로그램이란 어떤 환경에서 실행되느냐에 따라 다른데, 언어에 따라 실행환경도 달라진다.
C 언어를 윈도우 환경에서 비쥬얼스튜디오로 컴파일했다면 그 윈도우 환경에 맞게 사용되는 칩의 기계어로 바꾸게 된다. 비쥬얼스튜디오는 알아서 칩을 파악하고 그에 맞는 기계어로 컴파일하는 기능을 가지고 있다. 가끔은 컴파일러에게 어떤 타겟을 만들 것인지 지정하기도 한다. 예를 들어 한때 32비트 또는 64비트가 섞여 있어서 그것을 선택하는 옵션으로 비쥬얼스튜디오의 설정 화면에서 타겟을 x86 또는 x64를 설정할 수 있었다. (이것은 리눅스의 gcc 환경에서도 비슷하게 적용된다. 이외에도 다양한 옵션으로 컴파일러의 설정을 바꿀 수 있다.)
이렇게 만들어진 기계어 명령문으로 된 프로그램을 "네이티브 코드" 또는 네이티브 프로그램이라고 한다. 윈도우 환경이라면 .exe 확장자를 가질 것이다 1. 이것은 하드웨어에 의해 (물론 OS의 도움을 받아서) 바로 실행할 수 있는 코드라는 의미에서 네이티브 라는 용어를 쓴다. 즉 CPU가 읽고 이해할 수 있는 프로그램이라는 뜻이다. 컴퓨터구조에서 배운 Instruction Fetch Cycle(명령문을 Fetch-Decode-Load-Execute하는 과정)에 따라 그 명령문을 하나씩 실행하게 된다. 즉 컴파일러가 만들어준 네이티브 프로그램에는 기계어 명령문들이 들어있고 그것을 CPU가 하나씩 읽어다가 IR(명령문 레지스터)에 넣고 ALU에 의해 실행하게 된다. (우리가 사용하는 폰뉴만 아키텍처에서는 프로그램 코드가 실행되기 전에 메모리에 로드 2되어야 할 것이다. 그러면 명령문의 주소를 이용하여 한 개씩 가져다가 해석해서 실행하게 된다.) 3
컴파일러가 하는 일은 고급 언어로 된 소스 프로그램을 컴퓨터가 이해할 수 있는 기계어 코드로 바꾸는 것이다. 기계어 코드는 변수나 함수 이름, 루프나 if 문, 클래스나 타입을 알지 못한다. 컴퓨터가 아는 것은 명령문 종류(load, store, add, sub, mul, div, gt, eq, lt, jump, goto 등등)와 기본 데이터 타입(int, float, double, byte 등) 뿐이다. 컴파일러는 프로그램에 나오는 모든 이름을 주소로 바꾼다. 변수 이름은 메모리의 변수할당 주소로, 함수 이름은 명령문 주소로 바꾸어야 한다. 또한 모든 타입을 다 기본 타입으로 풀어서 하나씩 차례로 읽고 쓰고 계산하게 바꾸어야 한다. 또한 메모리의 데이터는 계산이나 비교를 하기 위해서는 CPU 안에 있는 레지스터로 가져와야 한다. 메모리에서 레지스터로, 계산결과를 레지스터에서 메모리로 읽고 쓰는 것을 메모리 IO라고 한다. 이것이 기계어 코드에서는 매우 중요한 부분이 된다. 마지막으로 함수 호출이나 루프, if 문 등의 제어 로직을 명령문 주소와 jump나 goto 만으로 바꾸어주어야 한다. 이런 일을 하는 것이 컴파일러다. 그러므로 프로그래머는 컴파일러가 나의 소스코드를 어떻게 바꾸는지 어느 정도는 이해하고 있어야 한다.
다음으로 자바 언어의 실행에 대해 생각해 보자. 자바 언어는 두 가지 측면에서 C 언어와 다르게 실행된다.
- 자바 프로그램은 기계어 코드로 바뀌는 것이 아니라 "바이트 코드"라고 하는 특수한 형태의 명령문으로 컴파일된다. 이것은 보통 .class 확장자를 가진다.
- 자바 클래스파일(바이트코드)은 가상기계(Virtual Machine:VM) 상에서 실행된다. 즉 VM 프로그램이 먼저 실행되고 그 프로그램이 자바 클래스 파일을 읽어와서 CPU가 하는 일을 흉내내서(시뮬레이트) 명령문을 하나씩 가져와 해석하고 실행한다.
바이트 코드는 기계어 코드와 비슷하게 기본 연산과 goto나 점프를 가지고 레지스터 대신 스택처럼 생긴 곳에 계산할 값이나 계산결과를 저장한다 . 그러나 기본적으로 컴파일러가 하는 일은 메모리 IO나 레지스터의 처리를 제외하면 C 언어와 비슷하다고 볼 수 있다. 변수나 함수를 이름 대신 주소로 바꾸고 복잡한 타입이나 클래스는 모두 기본 타입의 연산으로 풀어서 바꾸어 주어야 한다.
바이트 코드의 실행 방식은 VM에 의해 소프트웨어 적으로 실행된다. 그러므로 실행하는 과정이나 단계도 상당히 다르다. 우선 C 언어와 같은 링크 과정을 거치지 않고 자바 프로그램은 .java 파일 단위로 .class 파일이 만들어지고 이것을 그대로 디렉토리의 위치에 두거나 .jar로 묶어서 관리한다. 그럼 링크에 해당하는 일은 누가 할까? VM이 로드하면서 그 일을 대신해 준다. 즉 하나씩 .class 파일을 필요할 때마다 읽어들이면서 다른 클래스에 있는 필드나 메소드를 참조하는 부분을 찾아서 메꿔넣는 식으로 처리해야 한다. 그리고 VM 내부에는 프로그램의 스택이나 힙 메모리에 대응하는 영역이 할당되고 그것을 VM이 관리하면서 프로그램을 실행한다. 즉 VM이 로더의 역할, 링커의 역할, 메모리 관리, 프로세스(스래드?) 관리 등 OS가 할 일을 대부분 해 준다.
다음 그림은 자바의 실행과정을 보여준다. C와 비교하여 어떤 차이가 있는지 확인해 보기 바란다.
한편 인터프리트 방식의 프로그램 실행에 대해 알아보자. 소스 코드를 다른 형태(네이티브 코드 또는 어셈블리 형태의 중간코드)로 컴파일러에 의해 바꾸어 저장한 후 그것을 실행하는 컴파일 방식이라면 인터프리트 방식은 사람이 작성한 소스 코드를 읽어 바로 실행하는 방식이다. 흔히 접하게 되는 인터프리트 방식은 웹브라우저에 의한 자바스크립트의 실행과 파이썬의 대화형 실행창에서 한 줄씩 코드를 넣을 때마다 실행하는 것이 있다.
파이썬의 IDLE 실행환경에서는 파이썬 코드를 한 문장 입력할 때마다 바로바로 결과를 출력하던가 그것이 실행되고 다음 코드의 입력을 기다리게 된다. 오류 메시지도 즉시 발생한다. 이러한 인터프리트 방식은 실행환경이 프로그램 코드를 한 문장씩 읽으면서 바로 해석하여 해당하는 액션을 실행하게 된다. 실행결과를 바로바로 볼 수 있어서 코드를 작성하면서 바로 테스트할 수 있는 장점이 있다. 또한 자바스크립트 같은 경우 어떤 코드가 내려올지 미리 알지 못하므로 페이지가 다운로드되면서 바로 인터프리트하여 실행하게 된다. 그러나 인터프리트하는데 시간이 걸리므로 컴파일 방식에 비해서는 느릴 수 밖에 없다.
- 어셈블리어는 기계어 명령문을 사람이 알아보기 좋게 add, sub 이런 식으로 바꾼 것으로 기계어와 1:1 대응한다. 그러므로 기계어를 어셈블리어로 이해해도 상관없다. [본문으로]
- 사실 exe를 만드는 것은 다음 단계인 링커가 하게 된다. 컴파일러는 각 .c 파일을 기계어로 바꾸는 일만 하고 (.obj 파일을 생성함) 다른 .c 파일에 있는 것을 참조(전역변수나 함수)하는 주소는 비워둔다. 링커는 여러 개의 .obj 파일을 읽어와서 비어있는 참조 주소를 채워주고 하나의 .exe를 만든다. [본문으로]
- 로더는 OS의 프로그램 실행 기능 중 한 단계로 하드디스크에 있는 파일(.exe)을 읽어와 메모리에 올리고 데이터를 위한 영역도 할당해 준다. 그것을 새로 생성된 프로세스에 배정해 줄 것이다. [본문으로]