KUR Creative


CSAPP독학

[CS:APP 13-1] Linking 1 - 어떻게 링킹 에러를 피할까

13강|embed
이번 화부터는 우리가 만들어대는 프로그램이
시스템 소프트웨어와 어떻게 협력하여 돌아가는지에 대해 배우기 시작한다.

즉 운영체제와 관련되는 부분이다...
책에서는 part 2에 해당한다. (7장~) part 1은 컴퓨터 구조와 관련이 많다.

링커는 알아 뭐해?

그런데 왜 링커를 배워야 할까?
이것은 C나 C++을 하다가
SDL이나 기타 다른 라이브러리를 붙여서 사용하려 할 때 (특히 윈도우에서)
아주 엿같은 경험을 하게되면 자연스럽게 링커에 대해 궁금해지게 된다.

세상에서 가장 무서운 에러 중 하나가... 링킹 에러다

resource/link_error.jpg
링킹 에러를 만나 본 사람들은 하나같이 그 엿같음에 혀를 내두르지만
그러지 않은 뉴비들을 위해 설명하자면...

느낌은 컴파일 에러같이 프로그램 빌드를 거부하는데
소스코드에 표시가 안 됨 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

resource/link2.png
게다가 에러코드도 존나 이상해
시발 이게 도대체 무슨소리일까?
내가 뭘 잘못했는데 ㅡㅡ???

진짜 문제는 내가 잘못한 게 아닐 수도 있다는 점이다
실제로 내가 SDL을 설치할 때, SDL의 코드를 일정부분 바꿔서 재컴파일하고 나서야 설치할 수 있었는데
그것도 다 링킹 에러 때문이었음....

하지만 링킹 에러도 결국은 버그.
버그는 왜 생기는지 알면 잡을 수 있다!
최소한 피해갈 수는 있다...
링킹 에러가 왜 생기는지 알아보기 위해 링킹에 대해서 알아 보고 어떻게 C코드를 짜야 하는지 알아봅시다!

그리고, 왜 어떤 라이브러리는 DLL이 필요한데
어떤 라이브러리는 lib가 필요할까?
lib와 dll의 차이. 즉 static linking과 dynamic linking의 차이와 특성에 대해 알아봅시다!

C/C++로 콘솔 게임, 콘솔 어플리케이션을 벗어나서
진정한 프로그래밍에 입문했을 때, 정말로 실용적인 프로그램을 만들기 시작할 때,
즉 외부 라이브러리를 붙여야하는 순간부터
숙명적으로 프로그래머들은 링커를 사용하게 된다.
수-퍼 후로그래머가 되기 위해서는 링킹에 대해서 알아야 하는 것이다!

Linking이란

그런데 Linking이 뭐지?
링킹은 여러 코드와 데이터들을 모아서 하나의 실행파일로 만드는 것이다.
리눅스에서는... relocatable object파일(.o파일) 여러개를 묶어서 하나의 실행파일로 만드는 행위다.

다음과 같은 코드가 있다고 하자.

main.c

int sum(int a[], int n);  // sum.c에서 가져올 sum 함수
int array[2] = {1, 2};   // 전역 변수(배열)
int main(){
    int val = sum(array, 2);  //sum.c에서 가져온 함수 사용
    return val;
}

sum.c

int sum(int a[], int n)
{
    int i, sum = 0;
    for(i = 0; i < n; i++){
        sum += a[i];
    }
    return sum;
}

이걸로 어떻게 실행파일을 만들어낼까?

뭐? Ctrl+F5면 그냥 만들어진다고?
비주얼 스튜디오 꺼라....

리눅스에서는 다음과 같은 터미널 명령으로 prog라는 executable file(실행파일)을 만들어낸다.
gcc -Og -o prog main.c sum.c

gcc는 gcc라는 컴파일러를 쓰겠다는 거고
-o prog는 실행파일의 이름을 prog로 하겠다는 거고
-Og는 최적화를 하지 않겠다(는 건데 이건 알필요 없음)

그러면 리눅스에서는(비주얼 스튜디오도 사용자에게 숨길 뿐이지만) compiler driver라는 시스템이
실행파일을 만드는데 필요한 각종 프로그램들
preprocessor(전처리기), 컴파일러, 어셈블러, 링커등을 불러서 북치고 장구치고 해준다

그게 어떻게 되는 건지 궁금하다면 리눅스에서는 다음과 같은 명령어로 컴파일 해보면 알 수 있다.
gcc -v -Og -o prog main.c sum.c
-v는 verbose 옵션으로, 어떠한 과정으로 실행파일이 만들어지는지 사용자에게 알려준다.

Visual Studio에서는 어떻게 보냐고? 그런 건 없어...(나는 모르겠네요 혹시 아시면 댓글좀...)

근데 실제로 해봤는데 솔직히 잘 모르겠고.. ㅎ 다음 그림이나 봅시다

resource/link3.png
(괄호 안에 든 건 리눅스에서의 프로그램 이름임)
즉 relocatable object file들을 적절히 조각 내어 붙여서 실행파일로 만드는게 링킹이다.
참고로 relocatable object file바이너리지만 실행이 불가능하다. 그 이유는 나중에 설명하겠읍니다만...
왜 실행을 못할까? 한번 생각은 해보자.

이러시는 이유가 있을 거 아니에요

그나저나 링킹은 왜 하는걸까?

  1. 모듈화
    • 하나의 거대한(만 라인..) 파일보다는 작은 소스파일 여러개가 관리하기 쉽다
    • 맨날 쓰이는 함수들은 라이브러리로 만들어서 두고두고 우려먹을 수 있다(Math 라이브러리 등..)
  2. 효율
    • 시간적 효율: 위 그림에서 알 수 있지만, main은 그대로이고 sum이 바뀌었을 때, main.o는 그대로 두고 sum.c만 다시 빌드해서 sum.o를 만들어 붙이면 새로운 프로그램을 만들 수 있다. 만일 main.c와 sum.c가 하나의 파일이라면 모두 재컴파일해야 할 것이다.
      위 소스는 작아서 체감이 안 가지만 크롬(웹브라우저)은 아마 천만라인이 넘는다고 들었다. 그런데 ui약간 변경한다고 천만라인의 코드를 죄다 재컴파일할 수는 읎지..
    • 공간적 효율: 자주쓰이는 라이브러리 함수들은 함수 하나당 하나의 파일로 쪼개져 있는 경우가 많다. 라이브러리에서 필요한 함수가 들어 있는 xxx.o 파일을 링킹해서 실행파일을 만들면 실행파일 크기를 줄일 수 있다.

그러합니다. 솔직히 다 아는 내용일 듯

하지만 링커가 뭘 하는지는 다들 잘 모르지? (난 몰랐어...)

Linker가 하는 일

링커가 하는 일은 크게 두가지가 있다.

  1. symbol resolution
  2. relocation

일단 symbol resolution에 대해 알아보자.

잠깐... resolution? resolve?
resource/link2.png

그렇다. 엿같은 링킹 에러를 유발하는 발암물질 덩어리다...!
어떠한 방식으로 이것이 일어나는지 안다면 우리가 만드는 C 코드의 링킹에러를 없앨 수 있겠다!

Symbol

symbol resolution을 알아보기 위해
먼저 symbol에 대해서 알아보자.

symbol은 프로그램이 생성하는 것들인데... 다음 3가지 부류로 나눌 수 있다

type explain
global symbol 특정 모듈m에 정의되어 있고 다른 모듈에 의해 참조될 수 있는 심볼. (ex) static이 붙지 않은 C 함수, static이 아닌 전역변수.
external symbol 모듈 m에의해 참조되는 global symbol이지만 다른 모듈에 정의되어 있음
local symbol 모듈 m에서 정의되어 있고 오로지 모듈 m 내부에서만 참조할 수 있는 심볼. 모듈 외부에서는 참조가 불가능하다. ex) static C 함수, static 전역변수
주의- C의 지역변수local symbol다른 개념이다!
(static이 아닌 지역변수는 symbol이 아니다)

resource/link5.png

주의할 점은 지역변수는 static의 여부에 따라 symbol이 되기도 하고 그렇지 않기도 하다는 것이다.
static이 선언된 지역변수local symbol이 되어 링커의 관리 대상이 되고
symbol table에 들어간다(자세한 건 나중에 설명)

int f(){    
    static int x = 0;  // x.1
    return x;
}
 
int g(){
    static int x = 1;  // x.2
    return x;
}

static이 선언되지 않은 지역변수는 애초에 symbol이 아니라서
symbol만 아는 바보 링커는 그런게 있는지도 모른다...

int f(){    
    int x = 0; 
    return x;
}
 
int g(){
    int x = 1; 
    return x;
}

지역변수는 실행시 스택에 할당되고, 전역변수는 실행파일의 .data 영역에 존재하기 때문인데..
이것은 이후에 설명하겠다. 여튼

static이 아닌 지역변수는 symbol이 아니다! static 지역변수는 local symbol이다!

Resolution

symbol resolution은
이 symbol들의 reference참조들(예- 함수 호출, 지역변수 사용)을 각각
단 하나의 symbol definition정의​(예- 함수 정의, 지역변수 정의)와 연관시키는 행위이다.

그런데 symbol의 이름이 겹친다면 링커는 심볼 참조와 정의를 어떻게 연관시켜야 할까?
링커는 symbol의 이름이 겹치는 문제를 해결하기 위해 symbol들을 다시 두 부류로 구분한다.

그리고 다음의 규칙대로 symbol의 reference와 definition을 연관시킨다.

  1. 중복되는 strong symbol은 허용되지 않는다: LINK ERROR!
  2. strong symbol은 ​하나만 존재하고, 동일한 이름의 weak symbol들이 있다면,
    링커는 strong symbol을 선택한다: 이 이름의 symbol에 대한 reference들은 strong symbol로 연관된다.
  3. 만약 이름이 중복되는 ​weak symbol​만 여러개 있다면... 링커는 아무거나 골라서 연관시킨다.

특히 2, 3번 weak symbol과 엮이는 부분이 사람을 환장하게 만드는 링킹 관련 버그의 근원이다

다양한 Linking 에러 발생 과정

다음 one.c와 two.c를 컴파일하고 링킹한다고 생각해보자.

(one.c)

int x;
p1() {} //strong!

(two.c)

p1() {} //strong!

이 경우 2개의 strong symbol의 이름이 겹치므로 Link time error가 뜬다. (1.)
이런 경우 간단히 코드를 고치면 된다.

(one.c)

int x;  //weak
p1() { x = 0; } 

(two.c)

int x;  //weak
p2() { x = 9; } 

이 경우 p1이나 p2에서 전역변수 x를 쓰게 된다면,
one이나 two의 변수 중 하나를 선택해서 쓰게 된다. (.3)
링크에러는 발생하지 않는다.

p1에서 x를 썼다고 one.c의 x를 쓰고
p2에서 x를 썼다고 two.c의 x를 쓰는게 ​아니다!
​판단은 링커가 한다....
resource/link6.png
그래도 여기까지는 괜찮다(?). 문제는 다음이다.

(one.c)

int x;     //weak
int y;     //weak
p1() {} 

(two.c)

double x;  //weak
p2() {x = 800.0} 

컴파일러는 오직 하나의 소스파일만을 알기 때문에
one.c에서 x의 크기는 4이고
two.c에서 x의 크기는 8이다.

만약에 p2에서 x에 데이터를 썼는데,
링커가 p2reference x를 one.c의 symbol x와 연관시켰다면 (.3)
8바이트를 수정하기 때문에, p1의 x뿐만이 아닌 y까지 값이 변하게 된다.

이런 일이 일어나는 이유는
컴파일러가 two.c로 만든 오브젝트 파일two.o에서는
x가 있는 곳의 메모리를 8바이트 수정하기 때문이다.
이 또한 3번째 resolution 규칙인 임의의 연결 문제 때문이다.
그러나 링크에러는 발생하지 않는다.

그래도 이것또한 운이 억세게 좋다면 발생하지 않을 수도 있다...
그러나 다음은 반드시 버그가 발생한다.

(one.c)

int x = 7;  //strong!
int y = 8;  //strong!
p1() {} 

(two.c)

double x;  //weak
p2() {x = 800.0} 

one.c의 xy는 초기화된 전역변수이므로 strong symbol이다.
그런데 two.c의 x는 weak symbol이다.
그러므로 링커는 p2에서 사용된 x의 참조를 반드시 one.c의 x symbol과 연관시킨다. (2.)

그러나 컴파일러는 그 사실을 모르므로 two.o에서 x를 변경하면 8바이트의 메모리를 수정하게 되고
p2에서 x 변수를 변경하면 반드시 one.c에 정의된 x(4byte)와 ​y까지(4byte) 변경​된다.
그러나 역시 링크에러 그런 건 없어...


그렇다면, 가장 끔찍한 시나리오는 무엇일까?
바로 2개의 동일한 이름의 struct가 서로 다른 소스에 ​weak symbol​로 정의되어 있고,
링크해야할 오브젝트 파일들이 ​서로 다른 메모리 정렬 규칙​을 따르는 다른 컴파일러​들에 의해서 각각 생성되었을 경우이다.

그렇게 되면 존나 어떻게 될지 상상도 불가능하다...
물론 링크에러 그런 거 안 띄워줌 ^_^

Linking 관련 버그 피하기

그럼 지금까지의 링커에 의한 씨발창 상황들을 해결하려면 어떻게 해야할까?

resource/link7.png
결론: 전역변수를 쓰지마!
좀 쓰지 말라면 쓰지 말자. 괜히 C 첨 배우는 애들한테 강조하는 거 아니다...

아니 근데 꼭 써야 된다고?

요약

이제 우리는 링커가 하는 일 중 첫번쨰: Symbol Resolution이 뭘 하는 것인지 알게 되었다.
그것은 어떤 심볼에 대한 참조들을 단 하나의 심볼 정의와 연관시키는 일이다.

그리고 그 방식을 알아보고 링크와 관련된 심각한 버그들이 어떻게 발생하는지 확인했고,
이런 버그를 방지하는 방법에 대해 알아보았다.



다음은 Relocation과 정적 링킹, 동적 링킹에 대한 이야기다.
한 숨 돌리고 시작하지요


프로그래밍 갤러리 댓글

#from/old-blog
이전글CSAPP독학다음글
kur1612241313Archivekur1701091636