이번엔 Linking에 대해 다뤄볼 것이다. 우리가 C Program을 작성하고, Program을 메모리에 올릴 때, Process에는 Code 및 Data가 자리잡는다(단순히 Segment를 말하는 것이 아니다).
이때, Process의 '다양한 Data와 Code 조각'을 Memory에 Load될 수 있는 'Single Executable File'로 만들고, 이를 Execute하는 일련의 과정을 Linking이라 한다.
Linking은 Compile Time, Load Time, Run Time에 수행될 수 있다.
현대 시스템에선 Linker가 Linking을 자동으로 수행한다.
우리가 Linking의 원리, 그리고 Linker를 알아야하는 이유는 무엇일까?
큰 프로그램을 짤 때 도움이 된다.
심각한 에러의 발생을 방지할 수 있다.
Language Scoping Rule에 대해서 더 잘 이해할 수 있게 된다.
Linker가 만들어내는 Executable Object File은 Program Loading, Program Running, Virtual Memory, Paging, Memory Mapping 등에 대한 폭넓은 정보를 담고 있다. 이들에 대한 System적 이해를 도모할 수 있다.
Shared Library를 '잘' 만들고, '잘' 이용하는 법을 알 수 있다.
즉, Linking 과정에 대한 이해는 SP 학습에 있어서 상당히 큰 도움이 된다.
Compiler Driver가 Program을 Translate하고 Link한다.
Compiler Driver (Compiler)의 구성
C Preprocessor (cpp) : 전처리를 수행한다.
C Compiler (cc1) : 컴파일을 수행한다. 결과로 Assembly Code File이 만들어진다.
Assembler (as) : Assembler를 통해 Assembly File을 Relocatable Binary Object File로 만든다.
Linker (ld) : 위의 과정을 거친 '복수'의 .o File들을 하나로 합친다.
각각 만들어진 Relocatable Object File을 Linker가 하나의 Fully Linked Executable Object File로 합치는 것이다. ★★★
위와 같은 흐름으로 Linking이 수행되는 것을 Static Linking이라 한다. ★
Fully Linked가 Static Linking을 의미하는데, 자세한 이야기는 이후 포스팅에서 다룬다.
Linker는 Linking을 수행하는 장치이다. Linker의 장점은 무엇일까?
Modularity : Program을 'Smaller Source File'들의 Collection으로 만들 수 있다. 즉, 모듈화할 수 있다.
Time Efficiency : Compiler Driver는 Separate Compilation을 수행하기 때문에 시간적인 효율이 좋다.
Space Efficiency : 실행 파일을 만들 때 '실제 사용하는 함수'에 대해서만 코드를 가져오기 때문에 공간적인 효율도 높아진다.
Linker가 하는 일은 크게 'Symbol Resolution'과 'Relocation'으로 나눌 수 있다. 이들은 순차적으로 일어난다. 우선, Symbol Resolution에 대해 알아보자.
Linker는 Symbol Resolution을 통해 각 Symbol Reference가 정확히 하나의 Symbol Definition으로 대응되는지를 확인한다.
int sum(int *a, int n) {
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}
프로그램 안에는 여러 Symbol이 정의되어 있다. 위의 프로그램에서,...
프로그램 안에는 여러 Symbol이 정의되어 있고, 이들이 서로 참조하고, 참조된다. ★
Assembler가 만들어내는 Relocatable Object File 안에는 Symbol Table이라는 자료구조가 있고, 그 안에 각 Symbol에 대한 정보인 'Symbol Definition'들이 저장된다. ★★★
Linker는 Relocation을 통해, Linker의 대상이 되는 여러 Separate한 Code & Data Section들을 하나의 Section으로 만들고, 각각의 Relocatable Symbol들을 실제 Memory Location에 맵핑시킨다.
추후 이 두 Step을 더 자세히 설명할 것이다.
Object File은 아래와 같이 세 가지 종류로 분류할 수 있다. 각 유형을 Module이라고도 부른다.
Relocatable Object File (.o)
Executable Object File (filename)
Shared Object File (.so)
ELF(Executable and Linkable Format)는 Object File을 위한 표준 Binary Format이다. 'ELF Binaries'라고도 부른다.
각 Section들과, 각 Section에 대한 정보를 담고 있는 Section Header Table 부분으로 나뉜다. ★
Sections
ELF Header : Word Size, Byte Ordering, File Type(.o, exec, .so 등), Machine Type, Section Header Table의 Offset 위치 등의 정보를 담고 있다. ★
.text : Compiled Program의 Machine Code가 담겨 있다.
.rodata : Read Only Data를 담고 있다.
.data : 초기화가 이루어진 Global Variable들의 정보가 담겨있다. Static Local Variable 정보도 여기에 들어간다.
.bss : 초기화되지 않은 Global Variable(Static Local도 포함)들이 여기에 들어간다.
.symtab : Symbol Table이다.
.rel.text : .text Section에 대한 Relocation 정보를 담는다.
.rel.data : .data Section에 대한 Relocation 정보를 담는다.
.debug : 'gcc -g'를 통한 Symbolic Debugging 시에 사용되는 정보들이 담겨있다.
Section Header Table : 위의 각 Section에 대한 Offset과 Size 정보가 담겨있다. ★
.symtab의 Symbol Table에는 각 Relocatable Object Module에서 Define되고 Reference되는 Symbol들에 대한 정보가 담겨있다. ★
한편, Linker 입장에선 Symbol을 아래와 같이 분류한다.
Global Symbols : 현재 Module에서 정의되어 있으면서, 동시에 다른 Module에서 참조할 수 있는 Symbol들을 가리킨다.
External Symbols : Global Symbol이긴 한데, 현재 Module에서 정의되지 않고 다른 Module에서 정의된 Symbol을 의미한다.
Local Symbols : 현재 Module에서 정의되어 있으면서, 동시에 오로지 현재 Module에서만 참조할 수 있는 Symbol들을 가리킨다.
※ Local Linker Symbol vs Local Program Variable
~> 'static Keyword를 붙이지 않고 선언한 모든 Local Variable(Non-Static Program Variable)'들은 .symtab의 Symbol Table에 담기지 않는다.
~> 앞서 말한 것처럼, Local Non-Static Program Variable들은 Run Time 시에 Stack에 의해 Dynamic하게 관리된다. Linker와는 하등 관계가 없다.
앞서 말했듯이, Local Non-Static C Variable은 우리가 아는 일반적인 '지역변수'를 의미하며, Stack Segment에서 Dynamic하게 관리된다.
반면, Local Static C Variable은 .bss(초기화x)나 .data(초기화o)에 저장된다. Global Variable과 동일하게 취급되는 것이다. Lifetime은 다르지만.
int foo(void) {
static int x = 0; // foo 안에서만 접근 가능한 Variable
return x;
}
int bar(void) {
static int x = 1; // bar 안에서만 접근 가능한 Variable
return x;
}
~> 위 코드에서, Compiler는 각각의 x라는 변수를 .data 영역에 할당한다(초기화되어 있으므로).
=> 이때, 이름이 같은 서로 다른 두 Local Static Variable을 Symbol Table 상에는 x.1, x.2와 같이 Linker(Compiler) 자체적으로 구분을 해서 넣어준다. ★★★
Symbol이 중복될 때, 바로 위의 문장에서는 Linker가 각각 구분을 해서 저장한다고 언급했다. 그러나, 포스팅 초반에는 분명 각 모듈에서 전역변수 명이 겹치면 오류가 난다고 했다. 무엇이 맞는 것일까?
"Linker는 Duplicate Symbol Definition 상황을 어떻게 Valid, Invalid 판정할까?"
int foo = 5; // Strong Symbol
int bar; // Weak Symbol
void ex_procedure { // Strong Symbol
/* ... */
}
Linker는 상기한 Duplicate Symbol Definitions 상황을 아래와 같은 3가지 Rule을 통해 처리한다.
Rule1
Rule2
Rule3
위의 그림에서 3, 4번째 예시와 관련하여, 한 가지 코드를 더 보면서 다시 한 번 확인해보자. 아래의 코드는 심각한 Run-Time Bug가 발생할 수 있는 코드이다. Link Time Error는 발생하지 않고 말이다. ★
/* test1.c 파일 내부 */
#include <stdio.h>
void foo(void);
int x; // Weak Symbol
int main(void) {
x = 12345;
foo();
printf("x = %d\n", x);
return 0;
}
/* test2.c 파일 내부 */
int x; // Weak Symbol
void foo(void) {
x = 12344;
}
~> Comipler가 두 모듈 내 Symbol들의 메모리 영역을 어떻게 구성하느냐, Duplicate Symbol 중 어느 모듈이 Symbol이 택해지느냐에 따라, 의도치 않은 메모리 영역 침범 및 런타임 강제 종료를 야기할 수 있는 코드이다. 그냥 얼핏 봤을 때는 문제가 없어보이지 않는가? 그래서 더욱이 심각한 에러인 것이다.
=> 궁금하면 직접 Linux에서 돌려보자!
또 다른 코드를 보자. 이 역시 마찬가지의 문제 상황을 야기할 수 있는 코드이다.
/* test1.c */
#include <stdio.h>
void foo(void);
int y = 12344;
int x = 12345; // Strong Symbol
int main(void) {
foo();
printf("x = 0x%x y = 0x%x\n",
x, y);
return 0;
}
/* test2.c */
double x; // Weak Symbol
void foo(void) {
x = -0.0;
}
~> 이 코드는 반드시 에러가 발생하는 코드이다. x라는 Symbol이 겹치는데, test1.c에선 Strong, test2.c에선 Weak이다. 따라서, test1.c의 x가 선택된다. 허나, 두 소스 파일에서 x의 Type 및 Size가 다르다. 만일, foo 함수 호출로 인해 x에 0.0이란 값이 입력되면, test1.c의 int x에 0.0이란 Floating Point Value가 들어가게 되고, 이는 x의 4바이트 공간을 초과해 미지의 영역에 Write하는 일을 만든다.
=> 이는 Subtle(미묘)하고 Nasty(심각)한 Run-Time Bug를 만들어내는 것이다. ★
이러한 예시들에서 우리가 얻을 수 있는 교훈은 무엇이 있을까? 이번 Linking 포스팅에서 배울 수 있는 Skill 말이다.
사실, 일단 기본적으로 Duplicate Symbol 상황 자체를 만들지 않아야 한다. ★
그리고, 왠만하면, 프로그래밍 시, Global Variable은 피한다. ★
만약 사용해야한다면, Static Local Variable이나, Initialized Global Variable을 사용하고, 외부 전역 변수 사용 시엔 extern으로 명시해준다. ★
커다란 프로그램을 작성해본 컴퓨터공학도라면 분명 한 번 쯤은 '도무지 에러가 날 상황이 아닌데, 또는 아무리 봐도 모든 코드 부분을 정확히 작성했고 잘 검증했는대도, 이상한 에러가 발생했던 경험'이 있을 것이다. 정말 다 확인했는대도 에러가 발생하는 것이다. 기억나는가? 그리고 매우 높은 확률로, 전역 변수를 지역 변수로 바꿨더니 뜬금없이 해결된 적이 있을 것이다. 기억이 나지 않거나, 눈치채지 못했더라도, 아마 그랬던 순간이 분명 있을 것이다. 그러한 상황이 바로 오늘 배운 Linking 원리에서 비롯된 것이다. 이제는 Linking에 대해 어느 정도 이해가 잡혔으니, 더 이상 그런 실수에 애먹을 일이 없을 것이다. 이것이 바로 우리가 Linking을 배우는 이유인 것이다.
금일 포스팅은 여기까지이다.
와.....linking에 대해서 자세한 내용을 찾고 있었는데, 이것만큼 자세하고 잘 설명되어 있는 블로그가 없네요..! 감사합니다.