컴파일러와 어셈블러를 거쳤다면 각각의 재배치 목적파일과 라이브러리들을 가지고 링커는 실제 실행이가능한 목적파일을 만들어낸다. 

해당 글에서는 실행가능한 목적파일을 만드는 과정 중 링커가 수행하는 2가지 작업 중, 첫 단계인 Symbol Resolution(심볼 재해석)을 살펴보고자 한다.

Symbol Resolution (심볼 해석)

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

컴파일러는 모듈 당 단 하나의 지역 심볼 정의만을 허용한다. 컴파일러는 또한 지역 링커 심볼들을 갖게 되는 정적 지역변수들이 유일한 이름을 갖도록 보장한다.

그러나 전역 심볼들에 대한 참조를 해석하는 것은 그보다 까다롭다. 컴파일러가 현재 모듈에서 정의되지 않은 변수나 함수 이름 등의 심볼을 만나면, 이것이 다른 모듈에서 정의되어 있다고 가정하고 링커가 심볼 테이블 엔트리를 생성하며, 링커가 이것을 처리하도록 남겨둔다. 

만일 링커가 자신의 입력 모듈 중에 어디에서라도 참조된 심볼을 위한 정의를 찾을 수 없다면 에러 메세지를 출력하고 종료된다. 예를 들어 다음과 같은 소스 파일을 리눅스 머신에서 컴파일하고 링크하려고 한다면 컴파일러 단에서는 문제없이 동작하지만 링커는 foo에 대한 참조를 찾을 수 없기에 에러 메세지를 출력하고 종료한다.

void foo(void);

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

 

또한 동일한 심볼이 다수에 목적 파일에서 정의되는 경우 문제가 된다. 이 경우 링커는 에러를 출력하거나 하나의 정의를 선택하고 나머지를 무시해야 한다. Unix 시스템에서 채택하는 방법은 컴파일러, 어셈블러, 링커가 함께 협력해서 부주의한 프로그래머에게 다소 혼란스러운 버그를 야기하는 문제가 있을 수 있다.

링커가 중복으로 정의된 전역 심볼을 해석하는 방식

컴파일 시에 컴파일러는 각 전역 심볼을 어셈블러로 StrongWeak 두 가지 형태의 심볼로 분류하고, 어셈블러는 이 정보를 재배치가능 목적 파일의 심볼 테이블에 묵시적으로 인코딩한다. 

이때 함수들과 초기화된 전역 변수들은 Strong 심볼로 분류되며, bss (초기화되지 않은 전역 변수) 쪽은 Weak 심볼로 분루된다. 

링커는 이와 관련해서 중복으로 정의된 심볼을 처리하기 위해 다음과 같은 규칙을 사용한다.

규칙 1: 복수로 정의된 Strong 심볼은 허용되지 않는다.
규칙 2: Strong 심볼과 중복 정의된 Weak 심볼들이 있으면 Strong 심볼을 선택한다.
규칙 3: 다중 정의된 약한 심볼이 있다면 어떤 Weak 심볼을 사용해도 상관 없다. 

관련해서 몇가지 예제를 살펴보고자 한다.

예제 1. Strong 심볼 두 개가 중복되는 경우

/* foo2.c */
int x = 15213;
int main()
{
    return 0;
}

/* bar2.c */
int x = 15213;
void f(){};

위의 코드는 두 개의 파일을 의미한다.

이 경우 Strong 심볼인 x가 두 파일에서 중복되어 나타나므로 링킹 단계에서 에러 메세지와 함께 종료된다.

 

예제 2.  초기화된 전역 변수와 초기화되지 않은 전역 변수

/* foo3.c */
#include <stdio.h>
void f(void);

int x = 15213;

int main()
{
    f();
    printf("x = %d\n", x);
    return 0;
}

/* bar3.c */
int x;

void f()
{
    x = 15212;
}

 

런타임에 함수 f는 x 값을 15213에서 15212로 변경하며, 프로그램은 Strong 심볼과 Weak 심볼로 정의된 전역 변수들이 각각의 파일에 존재하므로 문제가 되지 않는다.

 

예제 3. Weak 심볼 두 개가 중복되는 경우

/* foo4.c */
#include <stdio.h>
void f(void);

int x;

int main()
{
    x = 15213;
    f();
    printf("x = %d\n", x);
    return 0;
}

/* bar4.c */
int x;

void f()
{
    x = 15212;
}

 

전역 변수가 초기화되지 않았기에 두 전역 변수 모두 Weak 심볼이다. 앞서 설명하였듯 Weak 심볼이 두 개인 경우 우리는 어떤 심볼을 선택할지 알 수 없다. 때문에 Weak 심볼이 중복되어버린다면 예기치 못한 런타임 버그가 발생할 수 있다.

 

예제 4. Weak 심볼 두 개가 중복되는 경우 (데이터 타입이 다른 경우)

/* foo5.c */
#include <stdio.h>
void f(void);

int x = 15213;
int y = 15212;

int main()
{
    x = 15213;
    f();
    printf("x = 0x%x y = 0x%x \n", x, y);
    return 0;
}

/* bar5.c */
double x;

void f()
{
    x = -0.0;
}

 

Weak 심볼이 중복되면서, 데이터 타입이 다른 경우이다. 이러한 케이스는 특정 환경에서 심각한 문제를 야기할 수 있다.

IA32/Linux 머신에서 double은 8 바이트이고 int는 4바이트이다. 그래서 bar5.c의 x를 -0.0으로 할당하는 부분은 x와 y가 위치한 메모리 영역에 음수 0의 이중정밀도 부동 소수점 표시로 덮어 쓰게 된다. 이러한 종류의 버그는 컴파일 시스템으로부터 아무런 경고도 없이 프로그램의 훨씬 뒤에서 문제가 발생하기 때문에 상당히 번거롭다. 이러한 종류의 의문이 있다면 -fno-common 플래그를 통해 여러번 정의된 전역 심볼이 있다면 에러를 명시적으로 발생시켜줄 수 있다.