※ CSAPP 7장을 참고했습니다.
하나의 프로그램을 구성하는 여러 .c 파일을 하나로 엮어주는 역할
링커는 소프트웨어 개발 시에 중요한 역할을 수행하는데, 그것은 이들이 독립적인 컴파일을 가능하게 하기 때문이다.
큰 규모의 응용프로그램을 한 개의 소스파일로 구성하는 대신, 별도로 수정할 수 있고, 컴파일할 수 있는 작은 모듈들로 나눌 수 있다.
이는 각 모듈 중 일부만 변경했을 때, 전체 파일이 아닌 해당 파일만 간단히 재컴파일하고, 링크만 해주면 된다.
하나의 프로그램 안에 쓰이는 두 개의 파일이 있다고 하자. 링커 프로그램은 각 파일이 바이너리 목적파일로 번역된 상태에서 실행가능 목적파일을 생성하기 위해 연결한다.
그런 다음, 쉘은 로더 loader라고 부르는 운영체제 내의 함수를 호출한다. 로더는 실행파일의 코드와 데이터를 메모리에 복사하고 제어를 프로그램의 시작 부분으로 전환한다.
: 소스코드를 컴파일하면 생성되는 중간 결과물로, 기계어로 번역된 코드, 데이터, 심볼 정보가 포함되어있다.
실행 가능한 상태는 아니고, 링커가 이 파일들을 결합하여 실행 파일을 만든다.
재배치 가능 목적파일 Relocatable object file
실행 가능 목적파일을 생성하기 위해 다른 재구성가능 목적파일들과 결합될 수 있는 바이너리 코드와 데이터를 포함한다.
실행 가능 목적파일 Executable object file
메모리에 직접 복사될 수 있고, 실행될 수 있는 형태로 바이너리 코드와 데이터를 포함한다.
공유 목적파일 Shared object file
로드타임 또는 런타임 시에 동적으로 링크되고 메모리에 로드될 수 있는 특수한 유형의 재배치 가능 목적파일이다.
컴파일 결과로 만들어지는 .o파일 같은 목적 파일

ELF header
파일을 생성한 워드 크기, 시스템의 바이트 순서를 나타내는 16바이트 배열로 시작한다.
링커가 목적파일을 구문분석하고 해석하도록 하는 정보를 포함하고 있다.
.text
컴파일된 프로그램의 머신 코드
.rodata
printf 문장의 포맷 스트링, switch문의 점프 테이블과 같은 읽기 전용 상수 데이터
.data
초기화된 전역/정적 변수
int x = 5;
.bss
초기화되지 않은 전역/정적 변수
목적파일에 실제 공간을 차지하지는 않는다.
static int count;
.symtab 심볼 테이블
변수, 함수, 라벨 등의 이름과 위치, 타입 정보가 담긴 테이블
링커는 이 테이블을 참고해서 이름 참조를 해결한다.
.rel.text
text 섹션 내 재배치 정보
다른 파일들과 연결할 때 수정되어야 하는 .text 위치들의 리스트
.rel.data
data 섹션 내 재배치 정보
일반적으로 초기값이 전역변수또는 외부에 정의된 함수의 주소인 초기화된 전역변수들 모두는 수정되어야 한다.
→ 재배치 정보란?
컴파일시 실제 주소를 알지 못한다.
예를 들어, printf() 함수를 main()에서 호출했을 떄, printf() 함수가 어디에 위치하였는지는 나중에 알 수 있다. 이를 실제 주소로 바꿔주기 위해서 표시를 남기는 것이다.
링커는 위와 같은 재배치 섹션을 보고 심볼 테이블을 참고해서 실제 주소로 주소를 확정한다.
| 용어 | 의미 |
|---|---|
| 재배치 가능 목적 파일 | 나중에 다른 목적 파일과 함께 결합할 .o 파일 |
| 재배치(Relocation) | 주소를 아직 정하지 않고 표시만 해두는 것 |
| 재배치 섹션 | 수정할 주소와 심볼 정보가 담긴 곳 |
| 링커 | 이걸 전부 읽고 실제 주소로 바꿔주는 작업 수행 |
심볼 테이블은 컴파일할 때 만들어진다. 재배치 가능 목적파일(.o파일)이 만들어지면서 이 안에 심볼 테이블도 생성된다.
링커는 각 .o 파일(.symtab 섹션)에 들어있는 심볼 테이블들을 모아서 정의된 심볼과 참조된 심볼을 매칭하고 재배치 정보를 사용해 주소를 완성한다.
심볼 하나하나에 대한 정보를 정리해놓은 전화번호부와 같다. 아래와 같은 구조를 가진다.

링커는 이 전화번호부(심볼 테이블)을 보면서 ‘아! 이 함수는 여기에 정의되어있구나, 나중에 연결해야지’ 하며 정보를 참고한다. 심볼을 연결하고 주소를 재배치해서 실행파일을 만든다.
이런 엔트리들의 목록이 심볼 테이블이다.
실행 파일을 만들기 위해 링커는 두 가지 주요 작업을 수행해야한다.
링커는 재배치 가능 목적파일들의 심볼 테이블로부터 정확히 한 개의 심볼 정의에 각 참조를 연결시켜서 심볼 참조를 해석한다.
컴파일러는 모듈당 단 하나의 지역 심볼 정의만을 허용한다.
링커는 정적 라이브러리 .a 파일 안에서 필요한 오브젝트 파일만 골라서 프로그램에 포함한다.
라이브러리는 순서가 중요하다.
main.o에서 lmath.a에 정의된 함수를 참조하려 한다면, 다음과 같이 라이브러리의 위치가 main.o 뒷 순서로 와야 한다.
gcc -lmath main.o # ❌ 링커는 먼저 libmath.a를 보고, main.o에서 참조를 모르니 무시함
gcc main.o -lmath # ✅ main.o에서 square 필요 → libmath.a에서 찾아 가져옴
링커는 왼쪽에서 오른쪽으로 심볼을 해결하기 때문에, main.o에서 특정 심볼이 필요하다는 걸 먼저 알아야libmath.a에서 찾을 수 있다. 반대로 하면 링커는 libmath.a를 봐도 “쓸 데가 없네?” 하고 넘겨버린다.
링커가 심볼해석 단계를 완료하면 코드 내 각 심볼 참조는 정확히 한 개의 심볼 정의에 연결된다. 이 시점에 링커는 입력 목적 모듈들 안에 코드와 데이터 섹션들의 정확한 크기를 알고 있다. 따라서 재배치 단계를 통해 입력 모듈들을 합치고 각 심볼에 런타임 주소를 할당한다.
아래 두 단계를 거쳐 재배치가 이루어진다.
섹션과 심볼 정의를 재배치한다.
같은 종류의 모든 섹션들을 하나의 통합된 섹션으로 합친다. 링커는 런타임 메모리 주소를 각 심볼들에 할당한다. 이 단계가 완료되면 프로그램 내의 모든 인스트럭션과 전역 변수들은 유일한 런타임 메모리 주소를 가진다.
섹션 내 심볼 참조를 재배치한다.
링커는 코드와 데이터 섹션 내의 모든 심볼 참조들을 수정해서 이들이 정확한 런타임 주소를 가리키도록 한다.
실행가능 목적파일의 포맷은 재배치 가능 목적파일의 포맷과 유사하다.
다수의 목적파일들을 하나의 실행가능 목적파일로 합쳐지는 과정을 살펴보았고, 최종적으로 이 프로그램을 메모리로 로드하여 실행하게 된다.

.text, .rodata, .data 섹션들이 최종 런테임 메모리 주소로 재배치 되었다는 점을 제외하고는 재배치 가능 목적파일과 유사하다.
ELF 실행파일들은 연속적인 메모리 세그멘트에 매핑된 연속적인 실행 가능 파일들의 덩어리로 메모리에 로드하기 쉽도록 설계되었다.
쉘은 로더 loader라고 알려진 메모리 상주 운영체제 코드를 호출해서 이 프로그램을 실행한다.
로더는 1. 디스크로부터 실행 가능 목적파일 내의 코드와 데이터를 메모리로 복사하고 2. 이 프로그램의 첫 번째 인스트럭셔(엔트리 포인트)로 점프해서 프로그램을 실행한다.
이와 같이 프로그램을 메모리로 복사하고 실행하는 과정을 로딩이라고 부른다.

모든 실행 중인 리눅스 프로그램은 위와 유사한 런타임 메모리 이미지를 가진다.
프로그램이 실행될 때, 필요한 코드(공유 라이브러리)를 런타임에 메모리에 불러와서 연결
우리가 프로그램을 작성하고 컴파일하면 보통 .o 파일들이 생기고, 이걸 링커가 하나의 실행 파일로 만들어준다.
이때 사용하는 방식이 정적 링킹이다. 그래서 정적 링킹 방식은 파일 크기가 커지고, 같은 라이브러리를 쓰는 다른 프로그램과 코드를 중복해서 가지게 된다.
이 때, 공유하는 라이브러리를 공유해서 메모리를 아낄 수 있는 방법이 있다. 바로 동적 링킹(Dynamic Linking) 이다.
공유 라이브러리들은 정적 라이브러리의 단점들을 극복하는 현대의 혁신이다. 공유라이브러리는 런타임이나 로드타임에 임의의 메모리 주소에서 로드되고, 메모리에서 프로그램으로 연결될 수 있다. 이 과정이 동적 링킹이다.
그리고 이는 동적 링커라고 하는 프로그램에 의해 수행된다.
리눅스 시스템에서 공유 라이브러리를 .so 확장자로 나타낸다.
프로그램은 실행 파일만 있고, 자주 쓰는 함수들은 따로 저장된 .so (shared object) 파일에 있다.
프로그램이 실행될 때 운영체제와 링커가 공유 라이브러리(.so)를 찾아서 붙여준다.

[ 동적 링킹의 흐름 ]
그래서 링킹은 두 차례 수행될 수 있다.
이들이 하는 일을 조금 더 자세히 살펴보자.
링커는 오브젝트 파일들을 받아, 목적파일(=바이너리 파일)을 처리한다.
동적 링킹을 별도로 쓰는 이유가 뭘까?
공유 라이브러리는 어느 곳에서나 로드될 수 있으며, 다수의 프로세스에 의해 런타임에 공유된다. 응용프로그램들은 함수와 공유 라이브러리 내의 데이터들을 로드, 링크, 접근하기 위해 런타임에 동적 링커를 사용할 수 있다.
따라서 동적 링킹은 실행 시점에 공유 라이브러리를 메모리에 로드하고, 심볼을 연결하는 메커니즘으로 프로그램 크기를 줄이고, 코드 공유를 가능하게 하며, 유지보수를 쉽게 만들어주는 시스템 설계의 핵심 기법이다.
링커는 여러 개의 오브젝트 파일들을 모아, 하나의 실행 가능한 파일을 만든다.(ELF, EXE 등)
컴파일된 각 파일 안에 들어있는 심볼(함수, 변수 등)을 연결해서 완성시킨다.
이러한 점에서 보통 링커라고 칭하면 정적 링커를 의미한다는 것을 알 수 있다.
정적 링커는 컴파일 시점에 작동하며 완성된 실행파일(a.out)을 결과물로 낸다.
동적 링커는 프로그램 실행 시점에서 .so 공유 라이브러리들을 연결한다. 이 또한 링커라고 부르기는 하지만 정확히는 런타임 링커 혹은 동적 링커라고 불린다.