Compiler
Compiler 컴파일러는 C 프로그램을 어셈블리 언어 프로그램으로 바꾼다. 어셈블리 언어 프로그램은 컴퓨터가 이해할 수 있는 명령의 기호 형태이다. 상위 수준 언어 프로그램은 어셈블리 언어보다 문장 수가 훨씬 적으므로 프로그래머의 생산성이 높아진다.
1975년 당시에는 메모리가 부족하고 컴파일러가 비효율적이었기 때문에 운영체제와 어셈블러는 어셈블리 언어(assembly language)로 작성하였다. 그러나 오늘날에는 DRAM 칩의 용량이 수백만 배나 커져서 프로그램 크기 문제도 완화되었다.
Assembler
Assembler Pseudoinstruction
Assembly Language 어셈블리 언어는 상위 수준 소프트웨어와의 인터페이스이므로 원래는 없는 명령어를 어셈블러가 독자적으로 제공할 수 있다. 이 명령어들은 하드웨어로 구현이 되어 있지 않더라도 어셈블리가 알아서 처리하여 번역과 프로그래밍을 간편하게 해 준다. 이런 명령어들을 의사 명령어(pseudoinstruction)라고 한다.
MIPS 하드웨어는 레지스터 $zero의 값이 항상 0이 되도록 한다. 그러므로 $zero는 한 레지스터의 내용을 다른 레지스터로 복사하는 move 명령어를 구현하는 데 사용할 수 있다. MIPS 하드웨어에는 move 명령어가 없지만 MIPS 어셈블러는 이 명령을 받아들인다.
move $t0, $t1 # register $t0 gets register $t1
어셈블러는 이 명령어를 다음 명령에 해당하는 기계어로 바꾼다.
add $t0, $zero, $t1 # register $t0 gets 0 + register $t1
MIPS 어셈블러는 blt(branch on less than) 명령어를 slt와 bne 2개의 명령어로 바꾼다. 이 외에도 bgt, bge, ble 명령어가 이렇게 처리된다. 먼 거리로 분기하는 명령어는 분기와 점프 명령어로 바꾸기도 한다. 명령어의 필드 크기는 16비트로 제한되어 있지만 MIPS 어셈블러는 레지스터에 32비트 상수를 넣는 일도 해 줄 수 있다.
Producing an Object Module
어셈블러는 어셈블리어 언어 프로그램을 목적 파일(object file)로 바꾼다. object file에는 기계어 명령어, 데이터, 명령어를 메모리에 적절히 배치하기 위해 필요한 각종 정보들이 혼합되어 있다.
어셈블리 언어 프로그램의 각 명령어를 이진수로 바꾸기 위해서는 레이블에 해당하는 주소를 모두 알아야 ㅎ한다. 어셈블러는 분기나 데이터 전송 명령에서 사용된 모든 레이블을 symbol table에 저장한다. 이 테이블은 symbol과 그 주소를 저장한다.
UNIX 시스템의 object file은 보통 다음과 같은 여섯 부분으로 구성된다.
- object file header: object file을 구성하는 각 부분의 크기와 위치를 서술한다.
- text segment: 기계어 코드가 들어 있따.
- static data segment: 프로그램 수명 동안 할당되는 데이터가 들어 있다.
- relocation information: 프로그램이 메모리에 적재될 때 절대 주소를 사용해야 하는 명령어와 데이터 워드를 표시.
- symbol table: 외부 참조같이 아직 정의되지 않고 남아 있는 레이블들을 저장한다.
- debugging information: 각 모듈이 어떻게 번역되었는지에 대한 간단한 설명이 들어있다. 디버거는 이 정보를 이용해서 기계어와 C 소스파일을 연관 짓고 자료 구조를 읽을 수 있는 상태로 만든다.
Linker
지금까지 설명한 대로라면 어떤 프로시저를 한 줄이라도 고치면 전체 프로그램을 다시 compile하고 aseemble해야 한다. 이렇게 처음부터 다시 한다면 컴퓨터의 자원이 심각하게 낭비된다. 특히 표준 라이브러리 루틴의 경우는 이런 낭비가 심각한데 그것은 전혀 바뀌지 않는 루틴들을 매번 compile하고 assemble해야 하기 때문이다.
이를 피하는 방법은 각 프로시저를 따로따로 compile, assemble하는 것이다.
어떤 procedure가 바뀌면 바뀐 procedure만 다시 translate하면 된다. 이렇게 하려면 링크 에디터(link editor) 또는 링커(linker)라고 부르는 시스템 프로그램이 추가로 필요하다. 이 프로그램은 따로따로 어셈블된 기계어 프로그램을 하나로 연결해주는 일을 한다.
Linking Ojbect Modules
Linker의 동작은 세 단계로 이루어진다.
- 코드와 데이터 모듈을 메모리에 symbol 형태로 올려놓는다.
- 데이터와 명령어 레이블의 주소를 결정한다.
- 외부 및 내부 참조를 해결한다.
링커는 각 objcet module의 재배치 정보와 symbol table을 이용해서 미정의 레이블의 주소를 결정한다.
프로그램 전체를 다시 compile하고 assemble하는 대신 링커를 써서 번역된 모듈을 연결하면 시간이 절약된다.
링커는 컴퓨터에서 실행될 수 있는 실행 파일(executable file)을 생성한다. 이 파일은 대개 object file과 같은 형식을 갖는데, 해결되지 않은 참조는 없다. 라이브러리 루틴같이 일부만 링크된 파일이 있을 수도 있다. 이런 파일은 아직 해결되지 않은 주소를 갖고 있으므로 object file에 속한다.
- Could leave location dependencies for fixing by a relocating loader
• But with virtual memory, no need to do this
• Program can be loaded into absolute location in virtual memory space
Loader
Loading a Program
디스크에 실행 파일이 준비되면 운영체제가 디스크에서 실행 파일을 읽어서 메모리에 넣고 동작시킬 수 있다.
UNIX 시스템의 로더(loader)는 이 일을 다음 순서로 진행한다.
- 실행 파일 헤더를 읽어서 text와 data segment의 크기를 알아낸다.
- text와 data가 들어갈 만한 address를 확보한다.
- 실행 파일의 명령어와 데이터를 메모리에 복사한다.
- 주 프로그램에 전달해야 할 인수가 있으면 이를 스택에 복사한다.
- 레지스터를 초기화하고 스택 포인터는 사용 가능한 첫 주소를 가리키게 한다.
- start-up routine으로 점프한다.
start-up routine에서 인수를 인수 레지스터에 넣고 프로그램의 main routine을 호출한다.
main 프로그램에서 start-up routine으로 복귀하면 exit을 call하여 종료시킨다.
Dynamically Linked Libraries
Dynamically Linked Libraries(DLL)은 프로그램 실행 전에는 라이브러리가 링크되지도 않고 적재되지도 않는다.
대신 프로그램과 라이브러리 루틴은 전역 프로시저의 위치와 이름에 대한 정보를 추가로 가지고 있다.
초기의 DLL에서는 로더가 동적 링커를 실행시킨다.
동적 링커는 파일에 저장된 추가 정보를 이용해서 적절한 라이브러리를 찾고 모든 외부 참조를 갱신한다.
Lazy Linkage
초기 DLL의 단점은 호출될 가능성이 있는 모든 라이브러리 루틴을 링크시킨다는 것이다. 실제로 호출되는 것만 링크시킨다면 더 좋을 것이다. 이런 관점에서 lazy procedure 지연 프로시저 Linkage의 DLL이 개발되었다.
여기서는 모든 루틴을 실제로 호출된 후에 링크시킨다.
Lazy Linkage에서는 간접접근(indirection) 기법이 사용된다.
이 과정은 프로그램 끝에 있는 더미 루틴(dummy routine)들을 호출하는 전역 루틴에서부터 시작된다.
전역 루틴 하나당 더미 엔트리가 하나씩 있는데 이 더미 엔트리에는 간접 점프가 하나씩 있다.
라이브러리 루틴을 호출할 때는 프로그램이 더미 엔트리를 호출하고 간접 점프를 따라간다. 더미 엔트리는 원하는 라이브러리의 루틴을 표시하기 위해 레지스터에 숫자를 넣고 동적 링커/로더로 점프하는 코드를 가리킨다. 링커/로더는 원하는 루틴을 찾아서 다시 매핑하고, 이 루틴을 가리키도록 간접 점프 위치에 있는 주소를 바꾼다. 다음으로 그 주소로 점프하고, 루틴의 실행이 끝나면 원래 호출한 위치로 돌아온다. 그 이후에 다시 라이브러리 루틴을 호출하면 추가로 돌아다니는 일 없이 해당 루틴으로 바로 간접 점프한다.
요약하면, DLL은 Dynamically linking에 필요한 정보를 위한 추가 공간을 필요로 하지만 전체 라이브러리를 복사하거나 링크할 필요는 없다. 어떤 루틴을 처음 호출할 때는 오버헤드가 매우 크지만, 그다음부터는 간접 점프 하나만 하면 된다. 라이브러리에서 되돌아올 때는 추가 오버헤드가 없다.
Starting a Java Program
Java 프로그램도 C처럼 실행할 수 있다. 하지만 Java를 만든 목적은 다른 곳에 있다.
그중 하나가 실행시간은 느리더라도 어느 컴퓨터에서나 안전하게 실행시킬 수 있게 하자는 것이다.
- binary 형태의 Java 바이트코드로 컴파일된다. 모든 주소는 컴파일러가 정의한다.
- 이렇게 되면 JVM이라 불리는 인터프리터에서 실행할 준비가 된 것이다.
- JVM은 프로그램이 실행될 때 Java 라이브러리 내의 원하는 메서드와 링크한다.
- 더 좋은 성능을 위해서 JVM이 JIT 컴파일러를 부를 수 있다.
- 이 컴파일러는 메서드를 선택적으로 컴파일하여 실행할 컴퓨터의 기계어로 바꾼다.
- Java를 어셈블리 언어로 컴파일하는 대신에 interpret 하기 쉬운 Java bytecode 명령어 집합으로 일단 컴파일한다.
- 이 명령어 집합은 Java 언어와 비슷하게 설계되었기 때문에 컴파일 작업은 아주 단순한다.
- 최적화는 거의 수행하지 않는다.
- C 컴파일러처럼 Java 컴파일러도 data type을 검사해서 각 type에 적합한 연산을 생성한다.
- Java 프로그램은 이 bytecode 형태의 binary version으로 배포된다.
JVM(Java Virtual Machine)이라 불리는 소프트웨어 Interpreter가 Java bytecode를 실행한다.
Interpret(인터프리터)란 명령어 집합 구조를 시뮬레이션하는 프로그램이다.
Java는 translation이 매우 간단해서 컴파일러가 주소를 채워 넣을 수 있거나 JVM이 실행 중에 주소를 찾을 수 있기 때문에 별도의 어셈블리 단계가 필요 없다.
Interpreter의 장점은 Protability(이식성)이다.
JVM이 있었기 때문에 대부분의 사람들이 Java 프로그램을 작성하고 실행할 수 있었다.
Interpreter의 단점은 Lower Performance이다.
Interpreter의 성능이 놀랍게 발전한 결과 이제는 중요한 응용에서도 Interpreter를 사용할 수 있게 되었다.
하지만 컴파일된 C 프로그램보다 10배나 느리기 때문에 어떤 Application에서는 좋지 못하다.
Java 개발의 다음 단계는 Portability를 훼손하지 않으면서 실행 속도를 개선하기 위해서 프로그램이 실행되는 도중에 translation 하는 컴파일러를 만드는 것이었다.
JIT(just in time) 컴파일러는 실행 중인 프로그램의 특성을 파악해서 많이 사용되는 메서드를 찾아내서 기계어로 컴파일한다. 컴파일된 부분을 저장해 두면 다음번 실행할 때는 더 빨라질 것이다.
Interpretation과 Compilation의 이러한 조화는 시간이 지날수록 더 좋아지므로, 자주 실행되는 Java 프로그램은 거의 Interpret하는 Overhead가 없다.