Log4KJS
C 의 링크 과정 본문
대략적인 컴파일 과정
C 에서 어떻게 링크가 이루어지는지 살펴보기 이전에 대략적인 컴파일 과정에 대해 살펴보도록 하겠습니다.

preprocessor (전처리기) 는 소스 파일 내의 # 로 시작하는 전처리 지시문들을 수행합니다.
즉, 헤더 파일을 읽어 소스 파일에 삽입하고, 매크로를 치환합니다.
compiler 는 이렇게 전처리된 .i 파일을 .s 어셈블리 파일로 컴파일합니다.
assembler 는 .s 어셈블리 파일을 기계어로 컴파일하여 relocatable object file 을 생성하는데,
리눅스에서 사용하는 relocatable object file 의 포맷을 ELF(Executable and Linkable Format) 이라고 부릅니다.
linker 는 여러 relocatable object file 들을 symbol resolution 과 relocation 의 과정을 거쳐 executable object file,
즉 실행 파일을 만듭니다.
ELF 파일의 구조

이 중에서 링크 과정에서 중요한 .symtab 영역과 .rel.text, .rel.data 영역만 살펴도록 하겠습니다.
.symtab
프로그램에서 정의되거나 참조된 함수, 전역 변수에 대한 정보를 가지고 있는 심볼 테이블입니다.
static 전역 변수, static 함수, static 로컬 변수 또한 심볼 테이블에 기록되지만,
링크 작업을 할 때는 무시됩니다. (디버그용)
.rel.text
.text 섹션에서 relocate 되어야 하는 외부 참조들의 위치를 기록합니다.
즉, 컴파일러가 함수, 전역 변수에 대한 정의를 발견하지 못했을 경우,
외부 참조로 가정하고 이 영역에 링크를 위한 정보를 기록합니다.
.rel.text 영역에 있는 각각의 엔트리는 심볼에 대한 참조를 가집니다
.rel.data
.rel.text 와 마찬가지이지만, .data 영역에서 relocate 되어야 하는 외부 참조들의 위치를 기록합니다.
Symbol Table
typedef struct {
int name; /* String table offset */
char type:4, /* Function or data (4 bits) */
binding:4; /* Local or global (4 bits) */
char reserved; /* Unused */
short section; /* Section header index */
long value; /* Section offset or absolute address */
long size; /* Object size in bytes */
} Elf64_Symbol
심볼 테이블은 여러 엔트리로 구성되어 있고, 엔트리의 구조는 위와 같습니다.
name 필드는 string table 에서의 offset 으로 심볼의 이름을 가리킵니다.
type 은 해당 심볼이 함수인지 데이터인지 나타내는 필드이고,
binding 은 함수 또는 변수의 접근 범위가 로컬 (static) 인지 global 인지 나타냅니다.
section 과 value 는 심볼이 정의된 위치를 가리키는데,
section 은 section header 에서의 인덱스, 즉 해당 심볼이 어떤 섹션(.text 인지 .data 인지)에 정의되어 있는지,
value 에는 해당 섹션에서 심볼이 정의되어 있는 offset 을 기록합니다.
해당 relocatable obejct file 에 정의되어 있는 심볼이 아닌 외부 참조라면, section 영역에 UNDEF (undefined) 가 기록됩니다.
직접 심볼 테이블을 조회해보겠습니다.
extern int sum(int a, int b);
extern int global_a;
int global_local_1= 0;
int global_local_2= 1;
int global_local_3= 2;
static int static_function() {
return 1;
}
int main() {
int a = global_a;
int b = 4;
int c = sum(a, b);
int d = sum(b, c);
int e = static_function();
}
int function() {
return 1;
}
작성한 main.c 파일을
gcc -c main.c
readelf -a main.o
명령어를 통해 relocatable object file 로 만들고, symbol table 을 조회할 수 있습니다.
# Section index
# [1] .text
# [2] .rela.text
# [3] .data
# [4] .bss
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 15 FUNC LOCAL DEFAULT 1 static_function
10: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_local_1
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_local_2
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT 3 global_local_3
13: 000000000000000f 84 FUNC GLOBAL DEFAULT 1 main
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND global_a
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
17: 0000000000000063 15 FUNC GLOBAL DEFAULT 1 function
Ndx -> Section index 입니다.
스태틱 함수인 static_function 은 Bind 가 LOCAL, Type 은 FUNC(function)
정의된 위치는 section: .text, offset: 0 즉 .text 영역의 시작 부분에 정의되어 있습니다.
반면, 글로벌 함수인 main, function 은 Bind 가 GLOBAL 이고, .text 영역에 각각 offset 이 f, 63인 것을 확인할 수 있습니다.
main.c 내에 정의되지 않은 sum 함수와 global_a 전역 변수는 외부 참조로 해석되어 Section index 에 UND 가 기록되었습니다.
objdump -d main.o
를 이용해 .text 영역을 확인해 보면,
0000000000000000 <static_function>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: b8 01 00 00 00 mov $0x1,%eax
d: 5d pop %rbp
e: c3 retq
000000000000000f <main>:
f: f3 0f 1e fa endbr64
13: 55 push %rbp
14: 48 89 e5 mov %rsp,%rbp
17: 48 83 ec 20 sub $0x20,%rsp
1b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 21 <main+0x12>
21: 89 45 ec mov %eax,-0x14(%rbp)
24: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
2b: 8b 55 f0 mov -0x10(%rbp),%edx
2e: 8b 45 ec mov -0x14(%rbp),%eax
31: 89 d6 mov %edx,%esi
33: 89 c7 mov %eax,%edi
35: e8 00 00 00 00 callq 3a <main+0x2b>
3a: 89 45 f4 mov %eax,-0xc(%rbp)
3d: 8b 55 f4 mov -0xc(%rbp),%edx
40: 8b 45 f0 mov -0x10(%rbp),%eax
43: 89 d6 mov %edx,%esi
45: 89 c7 mov %eax,%edi
47: e8 00 00 00 00 callq 4c <main+0x3d>
4c: 89 45 f8 mov %eax,-0x8(%rbp)
4f: b8 00 00 00 00 mov $0x0,%eax
54: e8 a7 ff ff ff callq 0 <static_function>
59: 89 45 fc mov %eax,-0x4(%rbp)
5c: b8 00 00 00 00 mov $0x0,%eax
61: c9 leaveq
62: c3 retq
0000000000000063 <function>:
63: f3 0f 1e fa endbr64
67: 55 push %rbp
68: 48 89 e5 mov %rsp,%rbp
6b: b8 01 00 00 00 mov $0x1,%eax
70: 5d pop %rbp
71: c3 retq
Symbol table 에 기록된 오프셋에 각 함수가 정의되어 있는 것을 볼 수 있습니다.
또, 외부 참조인 global_a 와 sum 의 주소가 00 00 00 00 으로 비워져 있습니다.
# Section index
# [1] .text
# [2] .rela.text
# [3] .data
# [4] .bss
10: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_local_1
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_local_2
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT 3 global_local_3
13: 000000000000000f 84 FUNC GLOBAL DEFAULT 1 main
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND global_a
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
이어서 symbol table 을 살펴보면,
global_local_1 은 0 으로 초기화되었으므로 .bss 영역에 정의되었고,
global_local_2, global_local_3 은 0이 아닌 값으로 초기화 되었으므로 .data 영역에 정의되었습니다.
global_local_2 의 offset 이 0, global_local_3 의 offset 이 4 이므로 두 전역 변수가 .data 영역에 차례로 정의되었음을 알 수 있습니다.
.rel.data, .rel.text
relocatable object file 을 만들 때, 어셈블러는 코드와 데이터가 런타임 메모리의 어디에 위치할지 알지 못합니다.
또, 함수와 전역 변수에 대한 외부 참조가 어디에 정의될지도 알지 못합니다. 그래서 어셈블러는 정의된 위치를 알 수 없는 참조를 만날 때마다 이를 relocation entry 의 형식으로 .rel.text 또는 .rel.data 영역에 기록합니다.
이 relocation entry 들은 linker 가 relocatable object files 를 이용해 executable 을 만들 때 이러한 참조들을 어떻게 치환해야 하는지 알려주는 역할을 합니다.
relocation entry 의 구조는 다음과 같습니다.
typedef struct {
long offset; /* Offset of the reference to relocate */
long type:32, /* Relocation type */
symbol:32; /* Symbol table index */
long addend; /* Constant part of relocation expression */
} Elf64_Rela;
offset 은 relocate 해야하는 레퍼런스의 위치를 기록합니다. .rel.text 일 경우 .text 섹션, .rel.data 일 경우 .data 섹션에서의 오프셋입니다. type 은 레퍼런스를 relocate 할 때 절대 주소를 쓸지 PC 상대 주소를 쓸지 등을 결정합니다.
symbol 은 Symbol table 에의 참조로, 어떤 심볼에 대한 레퍼런스인지 나타냅니다.
addend 는 relocate 시 더해져 주소를 보정하는 데에 쓰입니다.
위의 main.o 파일의 .rel.text 영역을 직접 살펴보겠습니다.
Relocation section '.rela.text' at offset 0x3a0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001d 000e00000002 R_X86_64_PC32 0000000000000000 global_a - 4
000000000036 001000000004 R_X86_64_PLT32 0000000000000000 sum - 4
000000000048 001000000004 R_X86_64_PLT32 0000000000000000 sum - 4
.text 영역의 offset 1d 에서 global_a, offset 36, 48 에서 sum 에 대한 미해결된 외부 참조를 가지고 있는 것을 볼 수 있습니다.
1b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
...
35: e8 00 00 00 00 callq 3a <main+0x2b>
...
47: e8 00 00 00 00 callq 4c <main+0x3d>
objdump 를 이용해 확인해 보면, 해당 위치가 global_a, sum 에 대한 참조가 들어와야 할 위치이고, 00 00 00 00 으로 비워져 있는 것을 확인할 수 있습니다.
링크 과정
여러 relocatable object file 들을 링크하는 과정은,
Symbol Resolution 과 Relocation 과정으로 나눌 수 있습니다.
Symbol Resolution
Symbol Resolution 이란 여러 obj 파일에 있는 같은 심볼에 대한 참조를 하나로 통일하는 것을 말합니다.
각 relocatable object file 들에는 정의된 심볼들과 정의되지 않은 심볼(UNDEF 심볼) 들이 존재합니다.
이 과정에서 정의되지 않은 심볼에 대한 참조는 다른 obj 파일의 정의된 심볼에 대한 참조로 변경됩니다.
링커가 정의되지 않은 심볼에 대한 정의를 다른 obj 에서 찾지 못하면 링크 에러를 던지고 종료됩니다.
이 과정에서 충돌하는 심볼에 대한 처리도 이루어집니다.
Relocation
Symbol Resolution 이 끝났다면 심볼에 대한 모든 참조는 정의된 심볼을 가리키고 있을 것입니다.
이제 Relocation 을 수행하는데, 다시 두가지 단계로 나뉩니다.
1. Relocating sections and symbol definitions.
링커는 여러 obj 파일에 있는 모든 같은 타입의 섹션들을 하나로 합칩니다.
예를 들어, .text 는 .text 끼리, .data 는 .data 끼리 합쳐 하나의 .text, .data 영역을 만듭니다.
이 과정에서 링커는 각 섹션의 런타임 메모리 주소를 정하게 됩니다.
즉, 모든 함수와 데이터의 런타임 메모리 주소가 정해지고,
심볼 테이블의 offset 역시 심볼이 가리키는 정의(함수, 변수 등)의 런타임 메모리 주소로 바뀝니다.
2. Relocating symbol references within sections.
이제 컴파일시에 비워두고 .rel.text, .rel.data 영역에 기록한 미해결된 외부 참조를 해결해야 합니다.
.rel.text, .rel.data 에서 참조하는 심볼은 원래 UNDEF 인 심볼이었지만, 이제 위 과정으로 인해 심볼이 정의된 런타임 메모리 주소 를 알 수 있습니다. 따라서 .rel.text, .rel.data 의 relocation entry 들을 순회하며 offset 에 0 으로 비워둔 참조를 심볼이 정의된 런타임 메모리 주소로 치환합니다.
예시를 보면,
Source Code --------------------------------------------------------------
extern int sum(int a, int b);
extern int global_a;
int global_local_1= 0;
int global_local_2= 1;
int global_local_3= 2;
static int static_function() {
return 1;
}
int main() {
int a = global_a;
int b = 4;
int c = sum(a, b);
int d = sum(b, c);
int e = static_function();
}
int function() {
return 1;
}
Symbol Table --------------------------------------------------------------
# Section index
# [1] .text
# [2] .rela.text
# [3] .data
# [4] .bss
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 15 FUNC LOCAL DEFAULT 1 static_function
10: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_local_1
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_local_2
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT 3 global_local_3
13: 000000000000000f 84 FUNC GLOBAL DEFAULT 1 main
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND global_a
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
17: 0000000000000063 15 FUNC GLOBAL DEFAULT 1 function
.rel.text section ----------------------------------------------------------
Relocation section '.rela.text' at offset 0x3a0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001d 000e00000002 R_X86_64_PC32 0000000000000000 global_a - 4
000000000036 001000000004 R_X86_64_PLT32 0000000000000000 sum - 4
000000000048 001000000004 R_X86_64_PLT32 0000000000000000 sum - 4
.text section --------------------------------------------------------------
0000000000000000 <static_function>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: b8 01 00 00 00 mov $0x1,%eax
d: 5d pop %rbp
e: c3 retq
000000000000000f <main>:
f: f3 0f 1e fa endbr64
13: 55 push %rbp
14: 48 89 e5 mov %rsp,%rbp
17: 48 83 ec 20 sub $0x20,%rsp
1b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 21 <main+0x12>
21: 89 45 ec mov %eax,-0x14(%rbp)
24: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
2b: 8b 55 f0 mov -0x10(%rbp),%edx
2e: 8b 45 ec mov -0x14(%rbp),%eax
31: 89 d6 mov %edx,%esi
33: 89 c7 mov %eax,%edi
35: e8 00 00 00 00 callq 3a <main+0x2b>
3a: 89 45 f4 mov %eax,-0xc(%rbp)
3d: 8b 55 f4 mov -0xc(%rbp),%edx
40: 8b 45 f0 mov -0x10(%rbp),%eax
43: 89 d6 mov %edx,%esi
45: 89 c7 mov %eax,%edi
47: e8 00 00 00 00 callq 4c <main+0x3d>
4c: 89 45 f8 mov %eax,-0x8(%rbp)
4f: b8 00 00 00 00 mov $0x0,%eax
54: e8 a7 ff ff ff callq 0 <static_function>
59: 89 45 fc mov %eax,-0x4(%rbp)
5c: b8 00 00 00 00 mov $0x0,%eax
61: c9 leaveq
62: c3 retq
0000000000000063 <function>:
63: f3 0f 1e fa endbr64
67: 55 push %rbp
68: 48 89 e5 mov %rsp,%rbp
6b: b8 01 00 00 00 mov $0x1,%eax
70: 5d pop %rbp
71: c3 retq
main 의 소스 코드와 링크 전 main.o 의 symbol table, .rel.text, .text 영역입니다.
Source code ---------------------------------------------------------------
int global_a = 4;
int sum(int a, int b) {
return a + b;
}
Symbol Table ---------------------------------------------------------------
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 global_a
10: 0000000000000000 24 FUNC GLOBAL DEFAULT 1 sum
.text area ---------------------------------------------------------------
0000000000000000 <sum>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 89 75 f8 mov %esi,-0x8(%rbp)
e: 8b 55 fc mov -0x4(%rbp),%edx
11: 8b 45 f8 mov -0x8(%rbp),%eax
14: 01 d0 add %edx,%eax
16: 5d pop %rbp
17: c3 retq
main 의 외부 참조(global_a, sum) 이 정의된 sum 의 소스 코드, 심볼 테이블, .text 영역입니다.
gcc main.o sum.o
readelf -a a.out
objdump -d a.out
을 통해 두 오브젝트 파일을 링크하여 실행 파일을 만들고, 그 심볼 테이블과 .text 영역의 코드를 확인해 보도록 하겠습니다.
# Section Index
# [23] = .data
# [14] = .text
# [24] = .bss
Symbol table '.symtab' contains 70 entries:
Num: Value Size Type Bind Vis Ndx Name
49: 0000000000004018 4 OBJECT GLOBAL DEFAULT 23 global_a
50: 0000000000004014 4 OBJECT GLOBAL DEFAULT 23 global_local_3
57: 000000000000119b 24 FUNC GLOBAL DEFAULT 14 sum
62: 0000000000004010 4 OBJECT GLOBAL DEFAULT 23 global_local_2
64: 0000000000001138 84 FUNC GLOBAL DEFAULT 14 main
65: 000000000000118c 15 FUNC GLOBAL DEFAULT 14 function
66: 0000000000004020 4 OBJECT GLOBAL DEFAULT 24 global_local_1
-------------------------------------------------------------------------------
0000000000001129 <static_function>:
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: b8 01 00 00 00 mov $0x1,%eax
1136: 5d pop %rbp
1137: c3 retq
0000000000001138 <main>:
1138: f3 0f 1e fa endbr64
113c: 55 push %rbp
113d: 48 89 e5 mov %rsp,%rbp
1140: 48 83 ec 20 sub $0x20,%rsp
1144: 8b 05 ce 2e 00 00 mov 0x2ece(%rip),%eax # 4018 <global_a>
114a: 89 45 ec mov %eax,-0x14(%rbp)
114d: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
1154: 8b 55 f0 mov -0x10(%rbp),%edx
1157: 8b 45 ec mov -0x14(%rbp),%eax
115a: 89 d6 mov %edx,%esi
115c: 89 c7 mov %eax,%edi
115e: e8 38 00 00 00 callq 119b <sum>
1163: 89 45 f4 mov %eax,-0xc(%rbp)
1166: 8b 55 f4 mov -0xc(%rbp),%edx
1169: 8b 45 f0 mov -0x10(%rbp),%eax
116c: 89 d6 mov %edx,%esi
116e: 89 c7 mov %eax,%edi
1170: e8 26 00 00 00 callq 119b <sum>
1175: 89 45 f8 mov %eax,-0x8(%rbp)
1178: b8 00 00 00 00 mov $0x0,%eax
117d: e8 a7 ff ff ff callq 1129 <static_function>
1182: 89 45 fc mov %eax,-0x4(%rbp)
1185: b8 00 00 00 00 mov $0x0,%eax
118a: c9 leaveq
118b: c3 retq
000000000000118c <function>:
118c: f3 0f 1e fa endbr64
1190: 55 push %rbp
1191: 48 89 e5 mov %rsp,%rbp
1194: b8 01 00 00 00 mov $0x1,%eax
1199: 5d pop %rbp
119a: c3 retq
000000000000119b <sum>:
119b: f3 0f 1e fa endbr64
119f: 55 push %rbp
11a0: 48 89 e5 mov %rsp,%rbp
11a3: 89 7d fc mov %edi,-0x4(%rbp)
11a6: 89 75 f8 mov %esi,-0x8(%rbp)
11a9: 8b 55 fc mov -0x4(%rbp),%edx
11ac: 8b 45 f8 mov -0x8(%rbp),%eax
11af: 01 d0 add %edx,%eax
11b1: 5d pop %rbp
11b2: c3 retq
11b3: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
11ba: 00 00 00
11bd: 0f 1f 00 nopl (%rax)
심볼 테이블에 있는 모든 심볼들은 심볼이 정의된 메모리 주소를 가집니다. 즉, UNDEF 심볼이 사라진 것을 확인할 수 있습니다.
.text 영역에 있는 코드에서도, 링크를 수행하고 나서는 00 00 00 00 으로 비워둔 외부 참조들이 모두 링크되어 적절한 메모리 주소를 참조하고 있는 것을 확인할 수 있습니다.