봉황대 in CS

[Chapter 2. 명령어: 컴퓨터 언어] 프로그램 번역 과정과 동적 링크 라이브러리 본문

Computer Science & Engineering/Computer Architecture

[Chapter 2. 명령어: 컴퓨터 언어] 프로그램 번역 과정과 동적 링크 라이브러리

등 긁는 봉황대 2022. 8. 23. 15:26

* 본 글은 '컴퓨터 구조 및 설계: 하드웨어/소프트웨어 인터페이스(Computer Organization and Design: The Hardware/Software Interface) 5th edition'의 내용과 2021학년도 1학기에 수강한 '컴퓨터 구조' 과목 강의 내용을 함께 정리하여 작성하였습니다.

 

프로그램의 번역 과정


C 프로그램을 컴퓨터가 실행할 수 있는 프로그램으로 변환하기 위한 4단계는 다음과 같다.

 

1. 컴파일 (Compile)

2. 어셈블 (Assemble)

3. 링크 (Link)

4. 로드 (Load)

 

C언어의 번역 계층

 

상위 수준 언어 프로그램은 어셈블리 언어 프로그램으로 컴파일되고

다시 기계어 형태의 목적 모듈로 어셈블 된다.

 

링커는 여러 모듈과 라이브러리 루틴을 사용하여 모든 외부 참조를 해결한다.

 

다음에는 프로세서가 실행할 수 있도록 로더가 기계어 코드를 적절한 메모리 위치에 넣는다.

 

 

어떤 시스템은 번역 시간을 줄이기 위해 몇 단계를 하나로 합치기도 하지만, 논리적으로는 이 네 단계를 거쳐야 한다.

(목적 모듈을 직접 생성하는 컴파일러도 있고, 링킹 로더를 써서 마지막 두 단계를 한꺼번에 처리하는 시스템도 존재)

 

컴파일러 (Compiler)

컴파일러는 C 프로그램 즉, 상위 언어 프로그램(high level language)을 어셈블리 언어 프로그램으로 바꾼다.

어셈블리 언어(assembly language)는 이진 기계어로 번역할 수 있는 기호화된 언어이다.

 

유의해야 할 점은 컴파일러는 종류에 따라 다양한 어셈블리어를 만들어낼 수 있다는 것을 기억해야 한다.

 

어셈블러 (Assembler)

어셈블러의 주된 역할은 어셈블리 프로그램을 기계어로 번역하는 일이다.

 

어셈블리 언어 프로그램을 목적 파일(object file)로 바꾸는데,

목적 파일에는 기계어 명령어, 데이터, 명령어를 메모리에 적절히 배치하기 위해 필요한 각종 정보들이 혼합되어 있다.

 

 

어셈블리 언어 프로그램을 구성하는 각 명령어를 이진수로 바꾸기 위해서는

레이블(함수 시작 주소, beq L1 등)에 해당하는 주소를 모두 알아야 한다.

 

어셈블러는 분기나 데이터 전송 명령에서 사용된 모든 레이블을 심벌 테이블(symbol table)에 저장한다.

 

 

UNIX 시스템의 목적 파일은 보통 다음과 같은 여섯 부분으로 구성된다.

 

  • 목적 파일 헤더
    목적 파일을 구성하는 각 부분의 크기와 위치를 서술한다.

  • 텍스트 세그먼트
    기계어 코드가 들어 있다.

  • 정적 데이터 세그먼트
    프로그램 수명 동안 할당되는 데이터가 들어있다.
    (정적 데이터와 동적 데이터 두 가지)

  • 재배치(relocation) 정보
    프로그램이 메모리에 적재될 때 절대 주소에 의존하는 명령어와 데이터 워드를 표시한다.

  • 심벌 테이블
    외부 참조같이 아직 정의되지 않고 남아 있는 레이블들을 저장한다.

  • 디버깅 정보
    각 모듈이 어떻게 번역되었는지에 대한 간단한 설명이 들어 있다.
    디버거는 이 정보를 이용해서 기계어와 C 소스 파일을 연관 짓고 자료구조를 판독한다.

 


어셈블리어는 상위 수준 소프트웨어와의 인터페이스이므로 원래는 없는 명령어를 어셈블러가 독자적으로 제공할 수도 있다.

 

이 명령어들은 하드웨어로 구현이 되어 있지 않더라도 어셈블러가 알아서 처리하여 번역과 프로그래밍을 간편하게 해 준다.

이런 명령어들을 의사 명령어(pseudoinstructions)라고 한다.

 

* 의사 명령어 : 하드웨어가 지원하지 않는 어셈블리 언어 명령어를 마치 실제 있는 것처럼 어셈블러가 처리하는 명령어

 

 

대표적인 예시로, move 명령어를 들 수 있다.

 

MIPS 하드웨어는 move 명령어가 없지만, MIPS 어셈블러는 이 명령을 받아들인다.

move $t0, $t1	# register $t0 gets register $t1

 

MIPS 하드웨어는 레지스터 $zero의 값이 항상 0이 되도록 하는데,

$zero는 한 레지스터의 내용을 다른 레지스터로 복사하는 move 명령어를 구현하는 데 사용할 수 있다.

 

어셈블러는 해당 명령어를 다음 명령에 해당하는 기계어로 바꾼다.

add $t0, $zero, $t1	# register $t0 gets 0 + register $t1

 

앞서 언급했던 blt(branch on less than), bgt(branch greater than), bge(branch greater than or equal), ble(branch less than or equal) 명령어들도 어셈블러가 slt와 beq 또는 bne 명령어들로 바꾼다.

2022.08.19 - [Computer Science/Computer Architecture] - [Chapter 2. 명령어: 컴퓨터 언어] 판단을 위한 명령어, MIPS 명령어 형식 : J-format

 

또한, 먼 거리로 분기하는 명령어는 분기와 점프 명령어로 바꾸기도 한다.

 

 

결과적으로 의사 명령어는 실제의 하드웨어 구현보다 훨씬 더 풍부한 어셈블리 언어 명령어 집합을 제공한다.

이에 대한 대가는 레지스터 하나를 어셈블리 전용으로 유보해 두어야 한다는 것뿐이다.

 

링커 (Linker)

이제까지 설명한 대로 전체 프로그램을 한 번에 컴파일한다면,

코드 한 줄만 바뀌더라도 전체 프로그램을 전부 다시 컴파일해야 하는 매우 비효율적인 일이 일어난다.

 

위의 상황을 피하는 방법은 각 프로시저를 따로따로 컴파일, 어셈블하는 것이다.

고로 바뀐 프로시저만 다시 번역하면 된다.

 

 

이것을 실현하려면 링크 에디터(link editor) 또는 링커(linker)라고 불리는 시스템 프로그램이 추가로 필요하다.

 

이 프로그램은 따로따로 어셈블된 기계어 프로그램을 하나로 연결해주고,

정의되지 않은 레이블의 주소를 찾아내어 실행 파일을 만든다.

 

 

링커의 동작은 3단계로 이루어진다.

 

1. 코드와 데이터 모듈을 메모리에 심벌 형태로 올려놓는다.

2. 데이터와 명령어 레이블의 주소를 결정한다.

3. 외부 및 내부 참조를 해결한다.

 

링커는 각 목적 모듈의 재배치 정보와 심벌 테이블을 이용해서 미정 레이블의 주소를 결정하고,

외부 참조를 모두 해결하고 나면 각 모듈의 메모리 주소를 결정한다.

 

링커가 모듈을 메모리에 적재할 때 절대 참조(실제 메모리 주소)는 모두 실제 위치에 해당하는 값으로 재설정되어야 한다.

 

 

이렇게 링커는 컴퓨터에서 실행될 수 있는 실행 파일(executable file)을 생성한다.

실행 파일은 목적 파일 형식의 기능 프로그램이다.

 

이러한 실행파일은 미해결된 참조가 없어야 한다.

 

라이브러리 루틴과 같이 일부만 링크된 파일이 있을 수도 있는데,

이런 파일은 아직도 미해결 주소를 갖고 있으므로 목적 파일에 속한다.

 

로더 (Loader)

로더는 목적 프로그램을 메인 메모리에 적재해서 실행할 수 있게 하는 시스템 프로그램이다.

운영체제는 디스크에 있는 실행파일을 메모리에 넣고 이를 시작시킨다.

 

UNIX 시스템에서 로더(loader)는 이 일을 다음 순서로 진행한다.

 

1. 실행 파일 헤더를 읽어서 텍스트와 데이터 세그먼트의 크기를 알아낸다.

2. 텍스트와 데이터가 들어갈 만한 주소 공간을 확보한다.

3. 실행 파일의 명령어와 데이터를 메모리에 복사한다.

4. 주 프로그램에 전달해야 할 인수가 있으면 이를 스택에 복사한다.

5. 레지스터를 초기화하고 스택 포인터는 사용 가능한 첫 주소를 가리키게 한다.

6. 기동 루틴으로 점프한다.

    이 기동 루틴에서는 인수를 레지스터에 넣고 프로그램의 주 루틴을 호출한다.

    주 프로그램에서 기동 루틴으로 복귀하면 exit 시스템 호출을 사용하여 프로그램을 종료시킨다.

 

동적 링크 라이브러리 (Dinamically Linked Library, DLL)


프로그램 실행 전에 라이브러리를 링크하는 방법은 다음의 단점들이 존재한다.

 

1. 라이브러리 루틴이 실행 코드의 일부가 된다.

    따라서 신판 라이브러리가 나오더라도 정적으로 링크된 프로그램은 라이브러리의 교체가 불가능하다.

2. 실행 파일에서 호출되는 라이브러리 루틴들은 이 호출의 실행 여부와 상관없이 전부 적재해야 한다.

 

이 단점들 때문에 동적 링크 라이브러리가 등장하게 되었다.

 

 

동적 링크 라이브러리 방식(DLL)에서는 프로그램 실행 전에는 라이브러리가 링크되지도 않고 적재되지도 않는다.

대신 프로그램과 라이브러리 루틴은 전역적 프로시저의 위치와 이름에 대한 정보를 추가로 가지고 있다.

 

 

초기의 DLL에서는 로더가 동적 링크를 실행시켰다.

동적 링커는 파일에 저장된 추가 정보를 이용해서 적절한 라이브러리를 찾고 모든 외부 참조를 갱신한다.

 

하지만 이 초기 DLL의 단점은 호출될 가능성이 있는 모든 라이브러리 루틴을 링크시킨다는 것이다.

 

 

따라서 모든 루틴을 실제로 호출된 후에 링크시키는 연(lazy) 프로시저 링키지형의 DLL이 개발되었다.

 

이것에는 일련의 간접 접근(indirection) 기법이 사용된다.

이 과정은 프로그램 끝에 있는 더미 루틴(dummy routine)들을 호출하는 전역 루틴에서부터 시작된다.

 

1.

라이브러리 루틴을 처음 호출할 때는 프로그램이 더미 엔트리를 호출하고 간접 점프를 따라간다.

더미 엔트리는 원하는 라이브러리 루틴을 표시하기 위해 레지스터에 숫자를 넣고 동적 링커/로더로 점프하는 코드를 가리킨다.

 

2.

링커/로더는 원하는 루틴을 찾아서 재사상하고, 이 루틴을 가리키도록 간접 점프 위치에 있는 주소를 바꾼다.

그러고 나서는 그 주소로 점프한다.

 

이 루틴이 끝나면 원래 호출한 위치로 돌아온다.

따라서 다시 라이브러리 루틴을 호출하면 추가로 돌아다니는 일 없이 해당 루틴으로 간접 점프한다.

 

 

요약하자면, DLL은 동적 링킹에 필요한 정보를 위한 추가 공간을 필요로 하지만

전체 라이브러리를 복사하거나 링크할 필요가 없다.

 

어떤 루틴을 처음 호출할 때는 오버헤드가 매우 크지만, 그다음부터는 간접 점프 하나만 하면 된다.

라이브러리에서 되돌아올 때는 추가 오버헤드가 없다.

 

 

반응형
Comments