본문 바로가기
CS/컴퓨터 구조

[Chapter 2.7 컴퓨터 구조 및 설계] 프로그램 번역과 실행 컴파일러와 어셈블러,Linker와 Loader,동적 링크 라이브러리

by 베어 그릴스 2022. 7. 11.
320x100
본 정리는 CS422-컴퓨터 구조 및 설계 : 하드웨어/소프트웨어 인터페이스. David A. Patterson, 존 헤네시 책을 바탕으로 하고 있음을 미리 알립니다.

 

프로그램 번역과 실행 과정


C 프로그램이 컴퓨터가 실행할 수 있는 프로그램으로 변환되는 과정

 

 

 

 

컴파일러


C언어 즉, High level language에서 컴파일러에 의해 어셈블리어 혹은 (요즘 대부분) 어셈블리 언어 프로그램으로 변환된다.

 

유의해야 할 점은 컴파일러는 같은 기능을 하지만 다양한 어셈블리어를 만들어낼 수 있다는 것을 기억해야 한다. ( 1:다 구조)

 

 

 

어셈블러


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

 

어셈블러는 어셈블리 언어 프로그램을 목적 파일(object file)로 바꾼다.

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

 

어셈블리 언어 프로그램을 구성하는 각 명령어를 이진수로 바꾸기 위해서는 레이블(함수 시작 주소, beq L1 등)에 해당하는 주소를 모두 알아야 한다.

 

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

 

 

어셈블리어는 상위 수준 소프트웨어와의 인터페이스이므로 원래는 없는 명령어를 어셈블러가 독자적으로 제공할 수도 있다. 이 명령어들은 하드웨어로 구현이 되어 있지 않더라도 어셈블러가 알아서 처리하여 번역과 프로그래밍을 간편하게 해 준다. 이런 명령어들을 의사 명령어(Pseudoinstructions)라고 한다.

 

ex)

move $t0, $t1
↓
add $t0, $zero, $t1

blt $t0, $t1, L
↓
slt $at, $t0, $t1
bne $at, $zero, L

 

즉, 컴파일러가 move 명령어를 줘도 어셈블러 자체적으로 add 명령어로 바꿔서 처리하게 되고 이외에도, 명령어 수치 필드 크기 제한 때문에 16비트로 들어오는 상수도 MIPS 어셈블러는 레지스터에 32비트 상수를 넣는 일을 한다.

 

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

 

이에 대한 대가로, $at 레지스터 하나를 어셈블리 전용으로 유보해둔다.

 

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

  • 목적 파일 헤더 : 목적 파일을 구성하는 각 부분의 크기와 위치를 서술한다.
  • 텍스트 세그먼트 : 기계어 코드가 들어있다.
  • 정적 데이터 세그먼트 : 프로그램 수명 동안 할당되는 데이터가 들어있다.
  • 재배치 정보 : 프로그램이 메모리에 적재될 때 절대 주소에 의존하는 명령어와 데이터 워드를 표시한다.
  • 심벌 태이블: 외부 참조같이 아직 정의되지 않고 남아있는 레이블들을 저장한다.
  • 디버깅 정보: 각 모듈이 어떻게 번역되었는지에 대한 간단한 설명이 들어 있다. 디버거는 이 정보를 이용해서 기계어와 C 소스파일을 연관 짓고 자료구조를 판단한다.

 

Linker


전체 프로그램을 한 번에 컴파일한다면, 코드 한 줄만 바뀌더라도 전체 프로그램을 전부 다시 컴파일해야 하는 매우 비효율적인 일이 일어난다. 이것을 피하는 방법은 각 프로시저를 따로따로 컴파일,어셈블하는 것이다. 바뀐 프로시저만 다시 컴파일, 어셈블 하면 된다. 이것을 실현하려면 링커(linker)라고 불리는 시스템 프로그램이 추가로 필요하다. 이 프로그램은 따로 어셈블 된 기계어 프로그램을 하나로 연결한다.

 

링커의 3단계 동작

  1. 코드와 데이터 모듈을 메모리에 심벌 형태로 올려놓는다.
  2. 데이터와 명령어 레이블의 주소를 결정한다.
  3. 외부 및 내부 참조를 해결한다.

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

 

이러한 실행파일은 미해결 된 참조가 없어야 하고, 라이브러리 루틴과 같이 일부만 링크된 파일이 있을 수도 있는데, 이런 파일은 아직도 미해결 주소를 갖고 있으므로 목적 파일에 속한다.

 

Loader


운영체제는 디스크에 있는 실행파일을 메모리에 넣고 이를 시작시킨다. UNIX 시스템에서 로더(loader)는 이 일을 다음 순서로 진행한다.

  1. 실행 파일 헤더를 읽어서 텍스트와 데이터 세그먼트의 크기를 알아낸다.
  2. 텍스트와 데이터가 들어갈 만한 주소 공간을 확보한다.
  3. 실행 파일의 명령어와 데이터를 메모리에 복사한다.
  4. 주 프로그램에 전달해야 할 인수가 있으면 이를 스택에 복사한다.
  5. 레지스터를 초기화하고 스택 포인터는 사용 가능한 첫 주소를 가리키게 한다.
  6. 기동 루틴으로 점프한다. 이 기동 루틴에서는 인수를 레지스터에 넣고 프로그램의 주 루틴을 호출한다. 주 프로그램에서 기동 루틴으로 복귀하면 exit 시스템 호출을 사용하여 프로그램을 종료시킨다.

 

동적 링크 라이브러리(DDL : dynamically linked library)


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

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

 

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

 

지연 프로시저 링키지형의 DDL은 실제로 호출되는 것만 링크시키기 위해 모든 루틴을 실제 호출된 후에 링크시킨다.

 

지연 프로시저 링키지형의 DDL은 간접접근 기법을 사용한다.

 

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

전역 루틴 하나당 더미 엔트리 하나씩인데, 이 더미 엔트리들은 간접 점프를 가지고 있다.

 

라이브러리 루틴을 처음 호출할 때는 프로그램이 더미 엔트리를 호출하고 간접 점프를 따라간다. 더미 엔트리는 원하는 라이브러리 루틴을 표시하기 위해 레지스터에 숫자를 넣고 동적 링커/로더로 점프하는 코드를 가리킨다. 링커/로더는 원하는 루틴을 찾아서 재사상하고, 이 루틴을 가리키도록 간접 점프 위치에 있는 주소를 바꾼다. 그러고 나서는 그 주소로 점프한다. 이 루틴이 끝나면 원래 호출한 위치로 돌아온다. 따라서 다시 라이브러리 루틴을 호출하면 추가로 돌아다니는 일 없이 해당 루틴으로 간접 점프한다.

728x90