# 7장 링커

#### **심볼 해석 규칙**

* 코드랑 데이터 모아서 메모리에 로드 가능하고 실행 가능한 파일로 만드는 과정
* 컴파일할 때나 실행될 때 등등 여러 상황에서 수행될 수 있다
* 별도로 수정하고 링크해서 큰 규모의 응용 프로그램을 별도로 수정할 수 있게된다

#### **링커를 배워야 하는 이유**

* 큰 프로그램을 작성하는데 도움됨 : 모듈, 라이브러리, 맞지 않는 라이브러리 버전 때문에 링커 에러를 발생시킬 때가 있다. 어떻게 링커가 참조를 해결해나가는지, 라이브러리가 뭔지 모르면 링커 에러날 때 당혹스러울 것.
* 위험한 에러 회피 : 리눅스 링커가 심볼 참조를 해결할 때 하는 결정들은 프로그램의 정확성에 영향을 조금 줄 수 있다. 전역변수를 중복해서 정의한 프로그램도 기본 설정의 경우 경고 메시지 없이 링커를 통과할 수 있다. 이렇게 되면 런타임 동작 때 혼란스럽게 작동하고 디버깅도 어렵다.
* 언어의 변수 영역 규칙 구현 이해 : 전역 변수와 지역 변수의 차이는 무엇인가? static을 사용해서 변수나 함수를 정의할 때, 이것은 무슨 의미인가? 등을 알 수 있다.
* 중요한 시스템 개념 이해 : 링커가 만든 실행 가능 객체 파일은 로딩과 프로그램 실행 같은 중요한 시스템 함수, 가상메모리, 페이징, 메모리 매핑에서 중요한 역할을 하게 됨
* 공유 라이브러리 이해

***

## 7.1 컴파일러 드라이버

대부분의 컴파일 시스템은 사용자를 대신해서 언어 전처리기, 컴파일러, 어셈블러, 링커를 필요에 따라 호출하는 컴파일러 드라이버를 제공한다.

<figure><img src="/files/6el6kERKaNXUnRReZU4e" alt=""><figcaption></figcaption></figure>

↑ (드라이버가 ASCII 소스 파일에서 예제 프로그램을 실행 목적 파일로 번역할 때 드라이버의 동작 내용) ↑

1. C 전처리기(cpp) : C 소스 파일 `main.c`를 ASCII 중간 파일인 `main.c`로 번역
2. C 컴파일러(ccl) : `main.i`를 어셈블리어 파일 `main.s`로 번역
3. 어셈블러(as) : `main.s`를 재배치 가능한 바이너리 목적파일 `main.o`로 번역
4. 링커 프로그램(ld) : 필요한 시스템 목적파일들과 함께 실행 가능 목적파일 `prog`을 생성하기 위해 `main.o`와 `sum.o`을 연결한다
5. 실행 : `linux> ./prog`으로 실행시키면 쉘은 로더(loader)라고 부르는 운영체제 내의 함수를 호출하며, 로더는 실행파일 `prog`의 코드와 데이터를 메모리로 복사하고, 제어를 프로그램의 시작 부분으로 전환.

***

## 7.2 정적 연결 (Static Linking)

컴파일 후에 소스 코드와 라이브러리 코드를 결합해서 단일 실행 파일을 생성하는 과정.\
실행 파일이 실행될 때 추가적인 연결이나 로딩작업이 필요하지 않아서 "정적"이라는 용어가 사용됨.

#### **링커가 실행파일 만드는 과정**

1. **심볼 해석**(symbol resolution)

* 각 목적 코드 파일은 함수, 전역 변수, 정적 변수(static으로 선언된 변수) 등 여러 심볼을 정의하고 참조함
* 심볼 해석 과정에서는 각 심볼 참조를 정확히 하나의 심볼 정의와 연결함
* 링커는 각 목적 파일의 심볼 테이블을라이브러리들 간의 의존성이 있는 경우, 의존성을 만족시키기 위해 명령줄에 라이브러리를 여러 번 반복해서 나타내야 할 수도 있 검사하여 정의되지 않은 심볼을 찾아내고, 이를 다른 파일에서 정의된 심볼과 매칭시킴

2. **재배치**(relocation)

* 컴파일러와 어셈블러는 각각의 목적 파일을 생성할 때, 이들이 메모리의 주소 0번지에서 시작한다고 가정함.
* 그러나 실행 파일이 생성될 때는 각 목적 파일이 실제 메모리의 어느 위치에 놓일지 결정해야 함.
* 그 과정이 재배치임. 각 심볼 정의에 실제 메모리 주소를 할당하고, 해당 심볼에 대한 모든 참조를 그 주소로 수정함.

목적 파일들은 단지 바이트 블록들의 집합임

***

## 7.3 목적파일 (Object Files)

**목적파일** : 컴파일러와 어셈블러가 소스 코드를 기계어로 변환하여 생성하는 파일

목적파일에는 **세 가지 형태**가 있음

1. 재배치 가능 목적파일 (Relocatable object file) : 바이너리 코드와 데이터를 포함하고 있음. 컴파일할 때 다른 재배치 가능 목적파일과 결합하여 실행 가능한 목적파일을 만드는 데 사용됨
2. 실행 가능 목적파일 (Executable object file) : 바이너리 코드와 데이터를 포함. 메모리에 직접 복사되어 운영 체제에서 직접 실행할 수 있음.
3. 공유 목적파일 (Shared object file) : `1.`의 특수한 형태. 메모리에 로드되어 동적으로 링크될 수 있음. 일반적으로 공유 라이브러리에 사용됨.

***

## 7.4 재배치 가능 목적파일 (Relocatable Object Files)

현대의 x86-64 리눅스와 유닉스 시스템들은 목적파일 형식으로 Executable and Linkable Format (ELF)을 사용함

**ELF 헤더** : 파일의 전체적인 형식을 설명 (ELF 헤더 크기, 목적파일 타입, 머신 타입 등)

**섹션 헤더 테이블** : 목적파일의 각 섹션에 대한 정보를 설명

#### **ELF 파일의 주요 섹션**

1. `.text` : 컴파일된 프로그램의 기계 코드가 포함됨.
2. `.rodata` : 읽기 전용 데이터가 포함됨. (ex. printf 문의 형식 문자열, switch 문의 점프 테이블)
3. `.data` : **초기화된** 전역 변수와 정적 변수가 포함됨. 지역 변수는 런타임 스택에 저장되고 `.data`나 `.bss` 섹션에는 안 나타남
4. `.bss` : **초기화되지 않은** 전역 변수와 정적 변수가 포함됨. 실제 목적파일에서 공간을 차지하진 않음. 위치만 표시함. 런타임 시 이 변수들은 메모리에서 0으로 초기화됨.
5. `.symtab` : 프로그램에서 정의되고 참조된 함수와 전역 변수에 대한 정보들이 포함됨.
6. `.rel.text` : `.text` 섹션에서 다른 객체 파일과 결합될 때 수정되어야 하는 위치 목록
7. `.rel.data` : 이 모듈에 의해 정의되거나 참조되는 전역변수들에 대한 재배치 정보.
8. `.debug` : 프로그램에 정의된 로컬 변수와 typedef, 전역 변수, 원본 C 소스 파일에 대한 디버깅 심볼 테이블. (컴파일러가 `-g` 옵션으로 호출될 때만 포함됨)
9. `.line` : 최초 C 소스 프로그램과 `.text` 섹션 내 기계어 명령어들의 라인 번호들 간의 매핑. (컴파일러가 `-g` 옵션으로 호출될 때만 포함됨)

***

## 7.5 심볼과 심볼 테이블

#### **심볼(Symbols)**

심볼은 프로그램에서 함수, 변수, 상수와 같은 엔티티를 나타내는 이름이다.\
각 심볼은 프로그램 내에서 고유한 엔티티를 참조하며, 이 엔티티의 위치(주소)와 다른 속성들을 정의한다.\
링커가 심볼을 통해 프로그램의 여러 부분을 결합하는데, 이는 다음과 같은 세 가지 유형으로 분류할 수 있다.

1. **전역 심볼** (Global Symbols)

* 모듈에서 정의되고 다른 모듈에서 참조할 수 있는 심볼
* 비정적 C 함수와 전역 변수

2. **외부 심볼** (External Symbols)

* 모듈에서 참조되지만 다른 모듈에서 정의된 심볼
* 비정적 C 함수와 다른 모듈에 정의된 전역 변수

3. **로컬 심볼** (Local Symbols)

* 모듈 내에서만 정의되고 참조되는 심볼
* 정적 C 함수와 static으로 정의된 전역 변수
* 로컬 심볼은 모듈 내에서만 보이므로 다른 모듈에선 참조할 수 없음

지역 링커 심볼들은 지역 프로그램 변수와는 같지 않다.\
`symtab`에 있는 심볼 테이블은 지역 비정적 프로그램 변수들에 대응되는 심볼을 전혀 포함하지 않음.\
이들은 런타임에 스택에 의해 관리되며 링커와는 관련이 없다.

대신, static으로 정의된 지역 변수는 스택이 아닌 `.data`나 `.bss` 섹션에 공간이 할당되고, 심볼 테이블에는 고유한 이름으로 로컬 심볼이 생성된다.

#### **심볼 테이블(Symbol Tables)**

각 재배치 가능 목적 모듈 m은 m에 의해서 정의되고 참조되는 심볼들에 대한 정보를 포함하는 심볼 테이블을 갖고 있음.\
심볼 테이블은 어셈블러가 컴파일러에서 내보낸 심볼을 사용하여 구축함.\
심볼 테이블은 `.symtab` 섹션에 포함됨. 이 테이블은 각 항목이 배열 형태로 구성되어 있음

들어있는 정보들 : 이름, 값, 크기, 유형(데이터인지 함수인지), 바인딩(지역인지 전역인지)

***

## 7.6 심볼 해석 (Symbol Resolution)

링커는 자신의 입력 재배치 가능 목적파일들의 심볼 테이블로부터 정확히 한 개의 심볼 정의에 각 참조를 연결시켜서 심볼 참조를 해석.\
동일한 모듈 내에 정의된 지역 심볼에 대한 참조를 해석하는 것은 비교적 간단함.\
하지만 전역 심볼들에 대한 참조를 해결하는 것은 더 복잡함.

컴파일러가 현재 모듈에서 정의되지 않은 심볼을 발견하면, 해당 심볼이 다른 모듈에서 정의되었을 거라고 가정하고 링커에게 그 심볼의 처리를 맡김. 링커가 해당 심볼을 찾을 수 없으면 에러 메시지를 출력하고 종료함. 예를 들어, 다음 코드를 컴파일하고 링크하려고 할 때,

```
void foo(void);

int main() {
    foo();
    return 0;
}
```

컴파일러는 성공적으로 실행되지만, 링커는 `foo`에 대한 정의를 찾을 수 없어 다음과 같은 오류 메시지를 출력한다.

```
/tmp/ccSz5uti.o: In function ‘main’: undefined reference to ‘foo’
```

### 7.6.1 링커가 중복으로 정의된 전역 심볼을 해결하는 방법

#### **심볼 해석 규칙**

1. 강한 심볼이 여러 개 있는 경우 에러를 발생시킴
2. 강한 심볼과 여러 개의 약한 심볼이 있는 경우 강한 심볼을 선택
3. 여러 약한 심볼이 있는 경우, 임의의 약한 심볼을 선택함

컴파일 할 때, 컴파일러는 각 전역 심볼을 어셈블러로 강하게 또는 약하게 보내며, 어셈블러는 이 정보를 재배치 가능 목적파일의 심볼 테이블에 묵시적으로 인코딩한다.

강한 심볼 : 함수들과 초기화된 전역변수들\
약한 심볼 : 비초기화된 전역변수

### 7.6.2 정적 라이브러리와 링크하기

링커가 재배치 가능 객체 파일들을 읽어들이고, 이를 결합하여 출력 실행 파일을 생성할 때 정적 라이브러리를 사용하면 필요한 함수들만 사용자들에게 제공해서 용량을 줄일 수 있다.

정적 라이브러리는 관련 객체 모듈들을 하나의 파일로 패키징한 것으로, 링커가 이 파일을 입력으로 받아서 프로그램에 필요한 객체 모듈만을 복사하여 사용한다.

### 7.6.3 링커가 참조를 해석하기 위해 정적 라이브러리를 이용하는 방법

정적 라이브러리를 사용할 때 링커가 이에 대한 참조를 해석하면서 혼란이 발생하기도 한다.

링커는 심볼 해석 단계에서 컴파일러 드라이버의 명령줄에 나타난 순서대로 재배치 가능 객체 파일과 아카이브를 왼쪽에서 오른쪽으로 순서대로 스캔한다. 이 과정에서 링커는 세 개의 집합을 관리한다.

* E : 병합할 재배치 가능 객체 파일들의 집합
* U : 아직 정의되지 않은 심볼들의 집합
* D : 이전 입력 파일에서 정의된 심볼들의 집합

1. 객체 파일 처리 : 입력 파일이 목적 파일이면, 이를 E에 추가하고 U와 D를 업데이트한다.
2. 아카이브 파일 처리 : 입력 파일이 아카이브 파일이면, U의 미해석 심볼을 해당 아카이브의 심볼과 매칭하여 해석된 심볼이 있는 경우 E에 추가한다. 이 과정은 더 이상 U와 D가 변하지 않을 때까지 반복된다.
3. 최종 처리 : 명령줄의 모든 입력 파일을 스캔한 후 U가 비어 있지 않으면, 링커는 에러를 출력하고 종료한다. U가 비어있으면 E에 있는 객체 파일들을 병합하여 출력 실행 파일을 생성한다.

과정이 이러하기 때문에, 라이브러리가 객체 파일 앞에 나타나면 참조가 해결되지 않아 링크가 실패할 수 있다.

```
gcc -static ./libvector.a main2.c
```

이러면 링크 안됨

```
gcc main2.c -L. -lvector
```

라이브러리를 명령줄 끝에 배치하면 링크 됨

라이브러리들 간의 의존성이 있는 경우, 의존성을 만족시키기 위해 명령줄에 라이브러리를 여러 번 반복해서 나타내야 할 수도 있다.

***

## 7.7 재배치 (Relocation)

재배치 : 링커가 심볼 해석을 완료한 후, 각 심볼 참조를 정확히 하나의 심볼 정의와 연결하는 과정. 입력 모듈들을 합치고 각 심볼에 런타임 주소를 할당한다. 이 때 링커는 입력 목적 모듈들 안에 코드와 데이터 섹션들의 정확한 크기를 알고 있다.

재배치 과정은 두 가지 단계로 구성된다

**1. 섹션 및 심볼 정의 재배치**

이 단계에서 링커는 같은 종류의 모든 섹션을 새로운 집합 섹션으로 합친다.\
예를 들어, 입력 모듈들의 `.data` 섹션들은 출력 실행 목적파일을 위한 한 개의 `.data` 섹션으로 합쳐진다.\
그 후 런타임 메모리 주소를 새로운 통합된 섹션들, 입력 모듈들에 의해 정의된 각 섹션들, 입력 모듈들에서 정의된 각 심볼들에 할당한다.\
이 단계가 완료되면 프로그램의 각 명령어와 전역 변수가 고유한 런타임 메모리 주소를 갖게 된다.

**2. 섹션 내 심볼 참조 재배치**

링커가 코드와 데이터 섹션들 내의 모든 심볼 참조들을 수정해서 이들이 정확한 런타임 주소를 가리키도록 한다.\
이 단계를 수행하기 위해서 링커는 재배치 가능 목적 모듈의 '재배치 엔트리'라는 자료구조를 사용한다.

#### **재배치 항목 (Relocation Entries)**

어셈블러가 객체 모듈을 생성할 때, 코드와 데이터가 궁극적으로 메모리의 어디에 저장될 지 알 수 없다.\
또한, 모듈이 참조하는 외부 정의 함수나 전역 변수의 위치도 모른다.\
따라서 어셈블러는 참조의 궁극적인 위치를 알 수 없을 때마다 해당 참조를 수정하는 방법을 설명하는 재배치 항목을 생성한다.\
코드의 재배치 항목은 `.rel.text` 섹션에, 데이터의 재배치 항목은 `.rel.data` 섹션에 배치된다.

***

## 7.8 실행 가능한 목적파일

앞서 설명한 것들은 링커가 다수의 목적파일들을 하나의 실행 가능 목적파일로 합치는 과정이었다.

이제 C 프로그램이 ASCII 텍스트 파일 모음에서 시작해서,\
메모리에 로드하고 실행하는데 필요한 모든 정보를 포함하는 단일 바이너리 파일로 변환되었다.

#### **실행 파일의 구조 (ELF 형식)**

* ELF 헤더,
* `.text`, `.rodata`, `.data` 섹션 : 재배치 가능한 객체 파일과 유사하지만, 링커에 의해 최종적인 런타임 주소로 재배치된다.
* `.init` 섹션 : 프로그램의 초기화 코드에 의해 호출되는 작은 함수 `_init`을 정의한다.
* `.rel` 섹션은 없다. 실행 파일이 완전히 링크되었기 때문에, 추가적인 재배치 정보 섹션이 필요하지 않기 때문이다.

#### **ELF 실행 파일**

ELF 실행 파일은 메모리에 쉽게 로드될 수 있도록 설계되었다.\
즉, 실행 파일의 연속적인 청크들이 연속적인 메모리 세그먼트에 매핑된다.

프로그램 헤더 테이블 : 실행 파일이 메모리에 어떻게 매핑될지를 설명. 코드 세그먼트와 데이터 세그먼트로 나뉨.

* 코드 세그먼트 : 실행 파일의 기계어 명령어를 포함한다.
  * 명령어 코드 : 컴파일러에 의해 생성된 프로그램의 기계어 명령어
  * 초기화 코드 : 프로그램 시작 시 실행되는 초기화 루틴 (`_init` 함수)
  * 읽기 전용 데이터 : 읽기 전용으로 사용되는 상수 데이터 (`.rodata` 섹션)
* 데이터 세그먼트 : 초기화된 전역 변수와 정적 변수를 포함한다.
  * 초기화된 데이터 : 프로그램 시작 시 특정 값으로 초기화된 전역 변수와 정적 변수 (`.data` 섹션)
  * 초기화되지 않은 데이터 : 프로그램 시작 시 0으로 초기화되는 전역 변수와 정적 변수 (`.bss` 섹션)

***

## 7.9 실행 가능한 목적 파일의 로딩

사용자가 터미널에서 실행 파일을 실행하려고 할 때, 예를 들어 `./prog`를 입력하면, prog가 내장 셸 명령어에 대응되지 않기 때문에 셸은 해당 파일이 실행 가능한 목적 파일이라고 가정하고 이를 실행하려고 시도한다. 셸은 로더를 호출하여 이 작업을 수행한다.

로더는 디스크에서 실행 파일을 메모리에 로드한다. 이 때, 프로그램 헤더 테이블을 참조하여 실행 파일의 각 세그먼트를 메모리의 적절한 위치에 복사한다. 로더는 먼저 코드와 데이터 세그먼트를 메모리에 복사한 후, 프로그램의 진입점으로 점프하여 프로그램 실행을 시작한다.

이 진입점은 보통 `_start` 함수로, 시스템 초기화 루틴을 호출하고, 사용자 프로그램의 `main` 함수를 호출하는 역할을 한다.

이와 같이 프로그램을 메모리로 복사하고 실행하는 과정을 로딩이라고 부른다.

#### **로딩 과정**

1. 메모리 매핑 : 프로그램 헤더 테이블에 따라 파일의 내용을 메모리에 매핑한다.
2. 초기화 코드 실행 : `_init` 함수와 같은 초기화 코드를 실행한다.
3. 진입점으로 점프 : 프로그램의 진입점으로 점프하여 메인 프로그램 실행을 시작한다.

#### **런타임 메모리 이미지**

실행 파일이 메모리에 로드되면, 각 프로그램은 런타임 메모리 이미지를 갖게 된다.

* 코드 세그먼트: 읽기/실행 권한이 부여되며, 프로그램의 기계어 명령어가 포함된다.
* 데이터 세그먼트: 읽기/쓰기 권한이 부여되며, 초기화된 전역 변수와 정적 변수가 포함된다.
* 힙: 동적으로 할당된 메모리가 포함되며, 런타임 중에 크기가 증가할 수 있다.
* 스택: 함수 호출과 관련된 로컬 변수 및 리턴 주소를 포함하며, 일반적으로 위에서 아래로 성장한다.
* 메모리 맵 영역: 공유 라이브러리가 로드되는 영역이다.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lazyartisan.gitbook.io/note/main-page/books/computer-systems-a-programmers-perspective/7.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
