정적 링킹과 목적 파일

mydailylogs
|2022. 12. 2. 16:54

C언어에서 자주 사용하는 함수들을 패키징(하나로 묶음)하고 싶다면 어떻게 해야할까?
이를 수행하는 방법으로는 정적 라이브러리를 활용한 정적 링킹, 동적 라이브러리를 활용한 동적 링킹이 있다.

정적링킹

Unix ld와 같은 정적 링커들은 재배치 가능한 목적파일들과 명령줄 인자들을 입력으로 받아들여서 로드될 수 있고 실행될 수 있는 완전히 링크된 실행가능 목적파일을 출력으로 생성한다. 입력인 재배치가능 목적파일들은 다양한 코드와 데이터 섹션들로 이뤄져있다. 인스트럭션들은 한 개의 섹션에, 초기화된 섹션은 다른 섹션에, 초기화되지 않은 변수들은 또 다른 섹션에 들어 있다.

실행 파일을 만들기 위해서 링커는 다음의 두 가지 주요 작업을 수행해야 한다.

심볼 해석 symbol resolution

목적 파일들은 심볼들을 정의하고 참조한다. 심볼 해석의 목적은 각각의 심볼 참조를 정확하게 하나의 심볼 정의에 연결하는 것이다.

재배치 Relocation
컴파일러와 어셈블러는 주소 0번지에서 시작하는 코드와 데이터 섹션들을 생성한다. 링커는 이 섹션들을 각 심볼 정의와 연결시켜서 재배치하며, 이 심볼들로 가는 모든 참조들을 수정해서 이들이 이 메모리 위치를 가리키도록 한다.

이후에 오는 내용들은 앞서 설명한 두 가지 작업을 이해하기 위해 좀 더 상세하게 각 요소들을 설명한다.

다만 링커에 대해서 몇 가지 기억해야할 지점이 있는데, 목적파일들은 단지 바이트 블록들의 집합이라는 것이다. 이 블록들 중 일부는 프로그램 코드를 포함하고, 다른 블록들은 프로그램 데이터를, 또 다른 블록들은 링커와 로더를 안낸하는 데이터 구조를 포함한다. 링커는 블록들을 함께 연결하고 이 연결된 블록들을 위한 런타임 위치를 결정하며, 코드와 데이터 블록 내에 여러 가지 위치를 수정한다. 링커는 타켓머신에 대해 최소한의 지식과 함께 앞서 설명한 심볼 해석과 재배치 작업을 수행한다. 목적파일을 생성하는 컴파일러와 어셈블러는 이미 대부분의 작업을 마친 상황임을 기억하자.

목적 파일

목적 파일에는 세 가지 형태가 있다.

  1. 재배치가능 목적파일 Relocatable object file (.o)
    포맷에 컴파일 시에 실행 가능 목적파일을 생성하기 위해 다른 재구성가능 목적파일들과 결합될 수 있는 바이너리 코드와 데이터를 포함한다.
  2. 공유 목적파일 Shared object file (a.out)
    로드타임 또는 런타임 시에 동적으로 링크되고 메모리에 로드될 수 있는 특수한 유형의 재배치가능 목적파일이다.
  3. 실행가능 목적파일 Executable object file (.so)
    메모리에 직접 복사될 수 있고 실행될 수 있는 형태로 바이너리 코드와 데이터를 포함한다.

컴파일러와 어셈블러는 재배치가능 목적파일을 생성한다(공유 목적 파일을 포함). 이후 생성된 재배치가능 목적파일을 가지고 링커는 실행가능한 목적파일을 생성한다. 기술적으로 하나의 목적 모듈은 바이트의 배열이며, 목적파일은 디스크에 파일로 저장된 목적모듈이다. 이 둘을 엄격하게 구분하여 표현하며 링커를 설명하는 경우도 있으나, 해당 글에서는 두 용어의 구분이 크게 의미가 없다고 판단하여 해당 용어들을 적절히 혼용할 예정이다.

목적파일의 포맷은 시스템에 따라 다른다. Bell Lab에서 개발된 최초의 Unix 시스템은 a.out 포맷을 사용한다. 리눅스에서 gcc을 통해 실행 파일을 생성해보면 해당 이름의 실행파일을 얻을 수 있는데 앞서 설명한 이유가 있었음을 알 수 있다. System V Unix는 초기 COFF(Common Object File Format)라는 포맷을 사용하였고, 윈도우 NT는 COFF의 변형인 PE(Portable Execution file)라는 포맷을 사용했다고 한다. 이 모든 것을 지나서 System V의 후기 버전, BSD Unix의 변종, 솔라리스는 ELF(Execuatble and Linkable Format)라는 포맷을 사용하며 현재 대부분의 OS에서 해당 파일 포맷을 따라 설계가 되어있다.

재배치가능 목적파일

그림 1 전형적인 ELF 재배치가능 목적파일

앞선 그림은 전형적인 ELF 재배치가능 목적 파일의 포맷을 보여준다. ELF 헤더는 이 파일을 생성한 워드 크기와 시스템의 바이트 순서를 나타내는 16바이트 배열로 시작한다. ELF 헤더의 나머지는 링커가 목적파일을 구문분석하고 해석하도록 하는 정보가 포함된다. 여기에는 ELF 헤더의 크기, 목적파일 타입(ex. 재배치가능, 공유, 실행가능), 머신명령어타입(ex. IA32), 섹션 헤더 테이블의 파일 오프셋, 섹션 헤더 테이블이 크기와 엔트리 수가 들어 있다. 여러가지 섹션들의 위치와 크기는 섹션 헤더 테이블로 나타내며, 이 테이블은 목적 파일의 각 섹션에 대해 고정된 크기의 엔트리를 갖는다.

ELF헤더와 섹션헤더 테이블 사이에는 섹션 컨텐츠가 들어있는데, 전형적인 ELF 재배치가능 목적파일에는 다음과 같은 섹션들을 포함한다.

.text: 컴파일된 프로그램의 머신코드

.rodata: printf 문장의 포맷 스트링, switch문의 점프테이블과 같은 읽기-허용 데이터

.data: 초기화된 C 전역변수 (지역 변수는 런타임 시에 스택에 저장되어 별도의 섹션에 저장되지 않는다)

.bss: 초기화되지 않은 전역변수 (해당 섹션은 실제로 데이터를 저장하지는 않고 그러한 데이터가 위치한 공간의 위치를 나타내기만 한다. 굳이 초기화되지 않은 전역변수까지 목적파일에서 실제 디스크공간을 차지할 필요는 없다)

.symtab: 프로그램에서 정의되고 참조되는 전역변수들과 함수에 대한 정보를 가지고 있는 심볼 테이블. 일부 프로그래머들은 심볼 테이블 정보를 얻기위해 -g 옵션을 통해 컴파일을 해야한다고 잘못 이해해고 있으나 사실 모든 재배치가능 목적파일은 .symtab에 이미 심볼 테이블을 가지고 잇다. 그러나 컴파일러 내부에 위치한 심볼 테이블과 달리 링커 쪽의 심볼 테이블은 지역변수에 대한 엔트리를 가지고 있지 않다.

.rel.text: 링커가 이 목적 파일을 다른 파일과 연결할 경우 수정되어야 하는 .text 섹션 내의 위치들의 리스트. 일반적으로 외부 함수를 호출하거나 전역변수를 참조하는 인스트럭션은 모두 수정이 되어야 한다. 반면 지역 함수를 호출하는 인스트럭션은 수정될 필요가 없다. 재배치 정보는 실행 가능 목적파일에는 포함하지 않으며, 사용자가 링커에게 이것을 명시적으로 포함할라고 지시하기 전에는 대개 해당 정보는 빠진다는 점에 유의해야 한다.

.rel.data: 해당 모듈에 의해 정의되거나 참조되는 전역변수들에 대한 재배치 정보.

.debug: 프로그램 내에서 정의된 지역변수들과 typedef, 프로그램과 최초 C 소스 파일에서 정의되고 참조되는 전역변수들을 위한 엔트리를 갖는 디버깅 심볼 테이블. 컴파일러 드라이버가 -g 옵션으로 불린 경우에 생성된다.

.line: 최초 C 소스 프로그램과 .text 섹션 내 머신 코드 인스트럭션 내 라인 번호들 간의 매핑. 컴파일러 드라이버가 -g 옵션으로 불린 경우에만 생성된다.

.strtab: .symtab과 .debug 섹션 내에 있는 심볼 테이블과 섹션 헤더들에 있는 섹션 이름들을 위한 스트링 테이블. 스트링 테이블은 널 문자로 종료된 스트링의 배열이다.

왜 초기화 되지 않은 데이터는 .bss라고 불리는가?

이는 본래 IBM 704 어셈블리 언어의 "Block Storage Start" 인스트럭션에 대한 약어였으며 이 약어가 굳어진 것이다. .data와 .bss 섹션들의 차이를 기억하는 간단한 방법으로 "bss"를 "Better Save Space"에 대한 약어로 생각해도 좋다.