7.6-Symbol_Resolution

Linker 는 symbol reference 를 정확히 하나의 definition 과 연결하여, 해결한다.
이 때의 definition 은 입력받은 relocatable obj 파일에 담겨있다.

local Symbol에 대해서는 같은 module 에 정의와 참조가 있기 때문에,
직관적으로 해결 가능하다.
static local variable 에 대해서도 컴파일러가 정확히 하나의 정의가 있음을 보장해주기 때문에,
직관적으로 해결 가능하다.

하지만, Global Symbol 는 조금 복잡하다.
컴파일러가 현재 module 에 정의되지 않은 symbol 을 만날 때,
다른 module 에 정의되어있다고 가정하고, "linker Symbol Table Entry" 를 만들고,
Linker 에게 그 책임을 전가한다.
Linker 가 입력받은 obj 파일 중 어디에서도 찾지 못하면, 에러를 반환한다.

여러 obj 파일은 동일한 이름의 global symbol을 가질 수 있다는 문제가 존재한다.
이 때, Linker 는 에러를 반환하거나, 여러 개 중 1개를 선택하고 나머지는 무시해야한다.

C++ 과 Java 는 함수 오버로딩이 가능한 언어이다.
이 경우에는, Symbol 을 어떤 식으로 관리할까?

이는 컴파일러가 특별한 방법으로
메소드 이름과 파라미터를 연결지어 이름을 짓는 방식으로 해소한다.

이 때, 이름을 짓는 것을 ,
지은 이름을 해석하는 것을 이라고 한다.

C++ 과 Java 는 서로 호환가능한 방법을 이용한다.

7.6.1 How Linkers Resolve Duplicate Symbol Names

Linker 의 입력은 여러개의 Relocatable obj 파일이다.
만약 여러 개의 module 이 같은 이름의 global Symbol 을 정의하면 어떻게 될까?
Linux 의 Compilation System 은 다음과 같이 작동한다.

compile-time 에 컴파일러는 어셈블러에게 global symbol가 , 둘중에 하나의 상태를 전달한다. 그리고 이 정보를 암시적으로 relocatable obj 파일의 symbol table 에 담아둔다.
Function, Initialized global variable 에 대해서는 로,
Uninitialized global variable 에 대해서는 로 저장한다.

  • Rule 1. 여러개의 Strong symbol은 허용하지 않는다.
  • Rule 2. 1개의 Strong symbol 과 여러개의 weak symbol 이 있다면, Strong Symbol 을 고른다.
  • Rule 3. 여러개의 weak symbol 에 대해서는 아무거나 고른다.

2번, 3번 규칙에 의해, 미묘한 버그가 종종 발생한다.

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

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

	/* bar3.c */
	int x;
	void f() {
		x = 15212;
	}

main 의 출력값은 x = 15212 로 미묘하다.
비슷하게, foo3.cx의 초기화를 mainf() 이전에 시행해도 결과는 동일하다.
weak definition 2개가 있을 때는 아무거나 고르기 때문에 에러가 나지 않는다.

만약 weak definition 이면서, type 이 다르면 어떻게 될까?

	// foo.c
	#include <stdio.h>
	void f();
	
	int x = 15123;
	int y = 15212;
	
	int main() {
	    f();
	    printf("x = 0x%x y = 0x%x \n", x, y);
	    printf("x = %d y = %d \n", x, y);
	    return 0;
	}
	
	// bar.c
	double x;
	
	void f() {
	    x = -0.0;
	}
// OUTPUT
// x = 0x0 y = 0x80000000 
// x = 0 !! y = -2147483648 !!

y 에 대해서는 접근이 없는 것처럼 보이지만, 의도치 않는 값이 발생한다.
이는 x = -0.0 이 8byte write 를 하게 되고,
메모리 상 붙어있는y 에 오염을 일으키게 된다.
이를 4byte 씩 끊어서 읽게 되기 때문에 의도치 않는 값으로 출력된다.

이는 코드 작성과 실행이 시점 차이가 큰, 규모가 거대한 프로젝트에서 심각한 버그를 일으킬 수 있다.

articles/chapter7/7.5-Symbols_and-Symbol_Tables 에서 COMMON, .bss 에 symbol 을 할당하는 방법에 대해 확인해봤다.
사실 이 방법은 몇몇 경우에서는, linker 가 여러 개의 module 을 허용하며,
이 때 여러 개의 같은 이름의 global symbol 이 가능할 수 있다.

컴파일러가 module 을 번역할 시, weak global symbol(이후 x 라고 가칭) 을 만났을 때,
x 가 다른 module 에 있을 지 알 수 없다. 즉, 어떤 instance 를 고를지 예측할 수 없다.
이 때문에 linker 에게 책임을 넘기기 위해, COMMON 부분에 x 를 할당하게 된다.

반면에, x 가 zero 로 초기화되어있다면, 이는 strong symbol 이기 때문에,
.bss 에 저장한다.
비슷하게, static symbol 은 유일하기 때문에, .bss 에 할당할 수 있다.

7.6.2 Linking With Static Libraries

linker 가 relocatable obj files 을 읽어서, output executable file 로 반환하는 것으로 가정했다.
사실은, 컴파일 시스템은 여러 관련된 obj module 을 하나로 묶는 mechanism 을 제공한다.
이 묶인 파일은 로 불리며, linker 의 input 으로 사용될 수 있다.

executable 파일을 만들 때, linker는 library 에 있는 module 중, 프로그램이 참조한 module 만 복사한다.

이러한 Library 의 개념이 왜 사용되는지, ISO C99 로 알아보자.
C99는 Standard I/O, String manipulation, Integer Math Function 등등 다양한 함수가
정의되어 있고, 이는 libc.a 라이브러리에 저장되어있다.

만약, Library 라는 개념을 쓰지 않는다면 다음과 같은 방법이 가능하다.

  • 컴파일러는 표준 함수에 대한 Call 을 인식하고, 올바른 코드를 바로 생성한다.
    이는 Pascal 같이 표준 함수가 적은 경우에는 적절할 수 있지만,
    C언어 같이 표준 함수가 많은 경우에는 적절하지 않다.

    이는 컴파일러의 부담이 커진다.
    즉, 복잡도가 증가하며, 새로운 표준함수가 늘어날 때마다 새로운 버전이 필요해진다.

    한편으로는 사용자 입장에서는 표준함수가 언제나 가능하다는 점에서 편리하긴하다.

다른 방법으로는

  • 모든 표준 함수를 하나의 relocatable obj 파일에 담는다. 예컨데, libc.o 라고 하자.
    이는 컴파일러의 구현과 표준함수의 구현을 분리할 수 있으며,
    여전히 개발자에게는 유용해보인다.

    하지만, 여전히 큰 문제점이 있다.
    executable file 을 만들 때마다, 모든 표준함수가 구현되어있는 목적파일을 사용한다.
    실행하는 프로그램에서도, 메모리에 함수가 복사되어 낭비하게 된다.

    또한, 표준 함수에 변화가 생길 때마다, library 개발자는 전체 소스코드를 다시 컴파일해야한다. 이는 유지보수에서 시간을 소모하게 만든다.

    이 문제는 relocatable obj 파일을 나눠서 well-known 디렉토리에 저장할 수 있지만,
    executable file 을 만들 때, 명령어가 복잡해질 수 있다.

Static Library 의 개념은 위의 단점을 해소할 수 있다.
관련된 함수는 분리된 module 에 컴파일되고, 하나의 static library 로 포장된다. (packaged)

프로그램은 command line 에 file name 을 적음으로서, library 안의 함수를 사용할 수 있다.

link time에, linker 는 프로그램이 referenced 한 obj module 만 복사해온다.
이를 통해, disk 와 memory 공간을 아낄 수 있다.
동시에 개발자는 몇개의 library 이름만 command line 에 추가하면 된다.

리눅스에서는 static library 는 disk 에 archive 형태로 저장된다.
archive 은 relocatable obj 파일을 모아둔 것이다.
추가로 header 가 존재하는데, 각 obj 파일의 사이즈와 위치가 저장되어 있다.
확장자로는 .a 을 쓴다.

library 에 대한 논의를 시작하기 전에 아래의 2개의 파일을 정의하고 간다.

	int addcnt = 0;

	void addvec(int *x, int *y, int *z, int n) {
		addcnt++;

		for(int i = 0; i < n; i++) 
			z[i] = x[i] + y[i];
	}
	int mulcnt = 0;

	void mulvec(int *x, int *y, int *z, int n) {
		mulcnt++;

		for(int i = 0; i < n; i++) 
			z[i] = x[i] * y[i];
	}

해당 파일로 library 를 만들기 위해, 아래의 명령어를 수행한다.

	gcc -c addvec.c multvec.c
	ar rcs libvector.a addvec.o multvec.o

해당 library 를 사용하기 위한 main2.c 을 다음과 같이 작성한다.

	#include <stdio.h>
	#include "vector.h"

	int x[2] = {1, 2};
	int y[2] = {3, 4};
	int z[2];

	int main() {
		addvec(x, y, z, 2);
		printf("z = [ %d %d]\n", z[0], z[1]);
		return 0;
	}

vector.hlibvector.a 의 함수를 정의해둔 헤더파일이다.

executable 로 만들기 위해 다음의 명령어를 수행한다.

	gcc -c main2.c
	gcc -static -o prog2c main2.o ./libvector.a
	# gcc -static -o prog2c main2.o -L. -lvector

-static : 정적 링킹을 강제한다. 즉, 외부의 추가적인 linking 이 필요하지 않게 만든다.
-L: 라이브러리 검색 경로를 추가하는데, . 이므로 현재 디렉토리 위치가 된다.
-lvectorvector 라는 이름의 library 를 -L 의 경로에서 찾는다.

articles/chapter7/imgs/Linking_with_static_libraries.png
위 그림은 linker 의 행동을 요약해서 보여준다.

linker 는 addvec 이라는 symbol 이 main2.c 에서 필요하고, 이것이 addvec.o 에 존재함을 파악하여 해당 addvec.o 를 executable 에 복사해온다.
반면에, multvec.o 는 필요하지 않으므로 복사해오지 않는다.

7.6.3 How Linkers Use Static Libraries to Resolve References

static library 는 유용하지만, 개발자의 복잡도를 증가시키는 요인 중 하나이다.
리눅스가 외부 참조를 해결하는 방법 때문이다.

Symbol Resolution Phase에, linker 는 왼쪽에서 오른쪽으로, 입력으로 받은 obj 파일과 archive 파일을 확인한다. 그동안에 3개의 집합을 관리하게 된다.

  • : executable 을 만들기 위해 포함할 relocatable obj 파일
  • : 아직 해결되지 않은 symbols (참조되어있지만, 아직 정의되어있지 않은)
  • : 전의 input file 에서 정의된 symbols

input file 에 대해 다음과 같은 행동이 발생한다.

  • 가 obj 인지, archive 인지 확인한다.
  • 가 obj 인 경우, 에 추가되며, 를 업데이트한다.
  • 가 archive 인 경우, linker 는 의 해결되지 않은 symbol 을 해결하고자 시도한다.
    만약 member m이 의 symbol 을 해결한다면, 에 추가된다.
    그리고, 를 업데이트한다.
    member obj 에 대해, 이 작업은 가 업데이트 되지 않을 때까지 반복한다.
  • 가 input file 에 대한 탐색이 끝나도 비어있지 않다면, error 를 반환하고 종료된다.
    아니라면, 의 파일과 함께 executable output file 을 만들어낸다.

이 알고리즘은 순서가 중요하기 때문에, 이상한 에러를 만날 수 있다.

	gcc -static ./libvector.a main2.c

위 명령어는 error, 특히 addvec 을 참조할 수 없다고 나온다.
처음 libvector.a 를 검사할 때, 는 비어있기 때문에, 어떤 member obj 도 포함하지 않기 때문에 발생하게 된다.

따라서, 배치의 순서에는 관례적인 규칙이 존재한다.
library 는 보통 가장 마지막에 배치한다.
만약 여러 library 간의 의존관계가 존재할 경우, 적절하게 참조되도록 순서에 주의해야한다.
만약, main.clibx.a 에 의존하며, libx.a 가 다시 liby.a 를 의존한다면,

	gcc -c main.c libx.a liby.a

식으로 작성해야한다.

혹은, 안전을 위해 archive 파일을 여러번 적는 것도 가능하며, 2개의 library 를 합치는 것도 방법이다.