C 프로그램이 실행 파일로 변환되는 과정에서 가장 중요한 단계 중 하나가 바로 링킹(Linking)이다.
심볼(Symbol)이란?
심볼의 정의
먼저 간단한 C 코드를 살펴보면
// 인간이 작성한 코드
int x = 3;
int y = x + 1;컴파일러는 이 코드를 다음과 같이 이해한다.
1. 0x1000번지에 3을 저장한다.
2. 0x1000번지 값을 읽어서 1을 더한 뒤
3. 0x10004번지에 저장한다.여기서 x, y는 프로그램 안에서 값을 가리키는 이름(name)인데, 컴파일러 입장에서 이 이름을 부르는 용어가 바로 심볼(symbol)이다.
심볼이란?
프로그램 안의 어떤 '값'이나 '위치'를 나타내는 이름 (e.g. 변수 이름, 함수 이름, 배열 이름 등)
정확히 말하면 전역 변수와 함수가 심볼이며, 지역변수는 링커와 무관하다.
심볼이 필요한 이유
C 프로그램은 변수 이름, 함수 이름을 사용해서 작성하지만, 컴퓨터는 이름이 아닌 메모리 주소로 데이터를 다룬다. 따라서 프로그램이 실행되기 전에
- 각 이름(심볼)이 무엇을 의미하는지 파악하고
- 그 이름을 실제 메모리 주소와 연결해야 한다
C 컴파일 과정과 링킹
C 프로그램이 실행 파일이 되기까지의 전체 과정을 살펴보자.
1. 전처리 단계 (.c → .i)
#include,#define,#ifdef같은 전처리 지시어를 먼저 처리#include를 만나면 헤더 파일 내용이 그 자리에 복사됨
2. 컴파일 단계 (.i → .s)
- C 코드가 어셈블리 코드(.s)로 변환됨
- 사람이 읽을 수 있는 저수준 언어이지만 아직 기계어(binary)는 아님
- 아직 CPU가 실행할 수는 없는 상태로, 기계어로의 변환이 필요함
- 컴파일러는 심볼 이름을 인식하여 어셈블리 코드에 심볼을 기록
3. 어셈블 단계 (.s → .o)
- 어셈블러는 어셈블리(.s) 파일을 받아 기계어(binary)로 번역함
- 컴퓨터가 이해할 수 있는 기계어 코드를 담고 있지만:
- 다른 .o 파일들과 연결이 안 된 상태
- 참조만 있고 주소는 확정되지 않은 심볼들로만 존재함
- 어셈블러가 .o 파일을 만들면서 심볼 테이블을 생성하고, 어떤 심볼을 정의하고 어떤 심볼을 참조하는지 기록
4. 링크 단계 (.o → .exe/.out)
링커는 여러 오브젝트 파일(.o)을 하나로 합친다.
- 심볼 해석(Symbol Resolution) → 모든 오브젝트 파일의 심볼 테이블을 모아 참조된 심볼과 정의된 심볼을 매칭
- 재배치(Relocation) → 연결된 심볼들을 실제 메모리 주소로 변환
외부 라이브러리가 쓰이는 경우
- 표준 라이브러리(libc 등)나 사용자 정의 라이브러리를 가져와서 함께 링크
5. 실행 파일
- 로더에 의해 메모리에 적재되고 실행됨
링커를 사용하는 이유
1. 모듈화(Modularity)
→ 하나의 큰 파일이 아닌 여러 개의 작은 소스 파일로 나눠서 개발할 수 있다.
2. 효율성(Efficiency)
→ 수정된 파일만 재컴파일하고, 다른 파일은 그대로 사용하면 된다.
즉, 여러 개의 .c 파일이 있을 때 수정된 .c 파일만 컴파일하여 .o 파일로 변환하고 링킹하면 된다.
심볼 해석(Symbol Resolution)
심볼 테이블
오브젝트 파일 안에는 심볼 테이블(symbol table)이 존재한다.
- 심볼 테이블은 오브젝트 파일(.o)에 포함된 심볼 정보를 담고 있는 데이터 구조
- 각 심볼 항목에는 이름, 크기, 메모리 주소(또는 오프셋), 타입, 범위 등의 정보가 포함됨
- 심볼 테이블은 링커가 서로 다른 오브젝트 파일의 심볼을 연결하는 데 필요함
- 심볼 테이블은 여러 개의 struct 구조체 배열로 구성되어 있음
링커는 심볼 테이블을 분석하여 참조 심볼과 정의 심볼을 연결하고 에러를 방지한다.
심볼의 구분
1. 글로벌 심볼(Global Symbol)
→ 한 모듈에서 정의되고 다른 모듈에서도 참조할 수 있는 심볼
e.g. static 키워드 없이 선언된 C 함수와 전역 변수
2. 외부 심볼(External Symbol)
→ 한 모듈에서 선언만 되고 정의는 다른 모듈에 있는 심볼
3. 로컬 심볼(Local Symbol)
→ 특정 모듈에서만 정의되고 참조되는 심볼 예: static 키워드로 정의된 C 함수와 전역 변수
local symbols은 local variables이 아님을 주의해야 한다
왜 이렇게 구분할까?
링커는 여러 오브젝트 파일의 심볼들을 해석(symbol resolution)한 뒤, 재배치(relocation)하여 하나의 실행 파일로 만들어야 한다.
그런데 모든 심볼을 다 연결할 필요는 없다.
- 로컬 심볼: 자신 파일 안에서만 유효하니까 다른 파일과 연결할 필요 없음
- 글로벌 심볼과 외부 심볼: 다른 파일과 연결시켜줘야 함
static, extern
static, extern 비교
| 구분 | static | extern | default |
|---|---|---|---|
| 의미 | 이 파일(모듈) 안에서만 사용 | 다른 파일에 정의된 것을 참조 | 함수는 extern 취급, 변수는 꼭 extern 써줘야 함 |
| 심볼 공개 여부 | 외부에 공개하지 않음 (Internal Linkage) | 외부에 공개하거나 가져옴 (External Linkage) | |
| 선언 위치 | 주로 정의하는 파일(.c)에서 사용 | 주로 다른 파일에 정의된 것을 사용할 때 선언 | |
| 메모리 공간 | 있음 (자체적으로 메모리 가짐) | 없음 (정의된 곳의 메모리 사용) | |
| 주 용도 | 1. 파일 내에서만 사용하는 전역 변수 2. (지역 변수로 사용할 때) 함수 내부 상태 유지 |
다른 파일에 있는 전역 변수나 함수를 사용할 때 | |
| 예시 | static int count = 0;static void helper() {} |
extern int num;extern void foo(); |
|
| 적용 대상 | 전역 변수, 함수, 지역 변수 | 전역 변수, 함수 |
재배치(Relocation)
재배치는 링커의 두 번째 주요 작업이다.
재배치 과정
링커는 여러 오브젝트 파일 안에 있는 코드 섹션(.text), 데이터 섹션(.data, .rodata, .bss)을 하나의 통합된 섹션으로 합친다.
링커는 각각 .o 파일 안에서 상대적인 위치(relative location)로 기록되어 있던 심볼들을 최종 실행파일의 절대적인 메모리 주소(absolute memory address)로 재배치한다.
링커는 심볼의 새로운 위치를 반영해서 모든 참조(reference)를 업데이트한다.
예시
다음과 같은 두 파일이 있다고 가정해보면
extern int sum(int *a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}int sum(int *a, int n)
{
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}main.c
| 심볼 이름 | 심볼 종류 | 저장 위치 |
|---|---|---|
| sum | External Symbol | 없음 (단순 선언) |
| array | Global Symbol | .data |
| main | Global Symbol | .text |
| val | 링커와 무관 | stack |
sum.c
| 심볼 이름 | 심볼 종류 | 저장 위치 |
|---|---|---|
| sum | Global Symbol | .text |
| i | 링커와 무관 | stack |
| s | 링커와 무관 | stack |
Local Symbols과 Static 변수
Local Static 변수의 특징
int f()
{
static int x = 0;
return x;
}
int g()
{
static int x = 1;
return x;
}위 코드에서 각 함수의 static 변수 x는 서로 다른 변수다.
- 컴파일러는 각각에 대해 .data 섹션에 별도의 공간을 할당함
- 심볼 테이블(symbol table)에는 각각 다른 이름(예: x.1, x.2)으로 등록함
- 즉, static 지역 변수라도 함수마다 독립된 메모리 공간과 독립된 심볼을 가짐
Local Static vs Local Non-Static
| 구분 | local non-static | local static |
|---|---|---|
| 메모리 위치 | stack | .data or .bss |
| 생성 시점 | 함수 호출할 때 | 프로그램 시작할 때 |
| 소멸 시점 | 함수 종료 시 | 프로그램 종료 시 |
| ELF 파일에 포함 여부 | x | o |