※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


이번 포스팅은 이전에 얘기한데로 symbol과 symbol table에 대해서 조금 더 자세하게 들어가보겠다. 쓰다보면은 이 사람이 지금 symbol에 대해서 설명을 하고 있는지 linker에 대해서 설명을 하는지 혼동될 정도로 symbol과 linker는 긴밀한 연관 관계가 있다. 


symbol은 이전에 설명했듯이 하나의 object file에서 쓰이는 변수나 함수들을 의미한다. 이것 각각에 대한 정보를 한 곳에 모아둔 것이 symtab 즉 symbol table이다. 이렇게 아는 것 마냥 지금 이야기를 하는데... 공부를 하면서 가장 궁금했던 것이 바로 symbol에 대한 실체이다. symbol table이 symbol에 대한 정보를 모아둔 테이블이니까 어딘가에는 symbol 그 각각의 무언가가 있지 않을까나? 나는 그렇게 생각해서 symbol의 실체를 찾으려고 막 별 짓을 다했었다. 그런데 생각해보니 진짜 별거 아니더라. 


생각해보자. 하나의 C파일에서 int a = 1; 과 같이 global 변수를 선언했다고 하자. 이건 당연히 symbol로 분류되겠다. 이것에 대한 실체는 어디있는가? 너무 쉬운 답이다. 바로 data section에 있다. data section의 어느 주소 부분에서 4 byte만큼의 공간을 차지하고 있는 것이다. 그것이 바로 symbol의 실체이다. 


가령 또 다른 C 파일에서 void func(void); 와 같이 함수가 선언되어있다고 해보자. 이 함수 역시 symbol로 분류된다는 것에는 의심할 여지가 없다. 그렇다면 이것의 실체는? 이거 역시 너무 쉬운 질문이 된다. 당연히 text 영역이다. text 영역에 machine code로 존재할 것이며 그것을 사람이 보기 쉽게 바꿔놓으면 push %ebx, %esp 와 같은 assembly code가 되는 것이지.


위와 같이 symbol이 존재할 수 있다. symbol로 정의된 변수나 함수들의 정보를 한 곳에 모아둔 것이 바로 symtab이다. 그렇다면 symbol table은 각 symbol의 어떠한 정보를 보여주는지 알아보자.


/include/linux/elf.h


실제 linux 커널에서 위와 같이 정의되어 있다. 본능적으로? 알겠지만 32비트에서의 또는 64비트에서의 elf structure를 표현한 것이다. 각 symbol들의 저 정보가 sybtab에 표현된다. symtab에서 표현되는 정보를 다시 한번 알아보기 위해서 이전 포스팅에서 봤던 symtab 예시를 다시 한번 봐보자.



한 줄만 골라서 비교해보면 value, size, type, name, index 정보를 볼 수 있고 Bind 정보는 아마도 unsigned char st_info;에서 표현되고 있는 것 같다. 또한 위의 그림에서 Vis는 정확히는 모르겠지만 unsigned char st_other; 부분으로 추정된다. 이 부분은 아직 정의되지 않은 부분으로 그냥 0으로 초기화되어있는 부분이다. 아직 쓰임이 없는 부분이 아닐까 싶다.


이정도 했으면 symbol과 symbol table 그리고 section에 대해서 혼동되지 않을 정도로 자세하게 설명하지 않았나 싶다. 다음에 설명할 부분이 링커에 대한 부분과 많이 연관이 있다. 우선 링커가 하는 일을 다시 한번 짚어보자. 링커는 .o 파일의 relocatable/shared object file들을 한데 모아서 하나의 executable object file을 만드는 것이다. 우리가 보통 컴파일을 할 때는 gcc 명령어를 이용해서 preprocessing → compiling → assembling → linking 한꺼번에 패키지로 해버린다. 그래서 아마 아래와 같은 질문에 대답을 제대로 못 하는 경우가 생길 것 같다. 아래의 코드가 컴파일이 될까?



안 된다고 말하는 사람이 있을 수도 있다 왜냐하면 swap() 함수가 실제로 구현되어 있지 않기 때문에. 그런데 된다. 컴파일은 된다. 하지만 링크 하는 과정에서 에러가 발생한다. swap() 함수에 대한 적절한 definition을 찾지 못 하기 때문에 링크하는 과정에서 에러가 발생한다. 이 말인 즉 컴파일 할 때는 문제가 없다는 것이다.. 컴파일은 .s 파일을 만들어내는 과정인데, 이 때 컴파일러는 "swap() 함수가 있네... 뭐~ 나중에 다른 파일이랑 링크될 때 다른 파일 어딘가에는 선언되어있겠지 뭐~" 하고 넘겨버린다. 따라서 컴파일러는 위의 경우에 대해서 에러를 내면 안 된다. 나중에 링크하는 과정에서 다른 object file에서 선언되어 있을 확률이 있기 때문이다.


자 그러면 링커가 하는 역할을 다시 한번 조금 더 구체적으로 정의해보자. 링커는 각각의 object file에 있는 symbol들이 제대로 정의되어있는지를 확인해서 하나의 executable object file로 만들어 내는 것이다. 여기서 제대로 정의되어있는지 여부는 정의되어있는 것이 오로지 한 개 인지를 확인하는 것이다. 이것에 대한 링커만의 규칙이 아래와 같이 정의되어 있다.


Rule 1 : Multiple strong symbols are not allowed.

Rule 2 : Given a strong symbol and multiple weak symbols, choose the strong symbol.

Rule 3 : Given multiple weak symbols, choose any of weak symbols.


내가 공부하는 책에 쓰여있는 것을 그대로 적은 것이다. 위의 규칙들을 이해하기 위해서는 strong symbol과 weak symbol에 대한 이해가 선행되어야 한다. 그렇게 어려운 개념은 아니다. 


strong symbol은 함수나 initialized global variable이다.

weak symbol은 uninitialized global variable이다.


간단한 개념이다. 그렇다면 이제 위의 3개의 링커만의 규칙을 공부해보자.


Rule 1 : Multiple strong symbols are not allowed.

여러 개의 strong symbol은 허용되지 않는다. 가령 아래와 같은 예제를 의미한다.

편의상 한 파일에 적었다-_- 위의 경우는 링커에 의해 에러가 발생한다. main이라는 함수(strong symbol)이 두 번 선언되었기 때문이다. 당연히 foo1.c와 bar1.c를 컴파일 할 때는 문제가 발생하지 않는다. 그런데 컴파일 해서 생긴 foo1.o bar1.o를 링커로 연결할 때 문제가 생긴다는 거다. initialized variable로 인한 문제는 딱히 예시로 보여주지는 않겠다. 아무튼 initialized global variable이 두 개 이상 있으면 안 된다는 것이다.


Rule 2 : Given a strong symbol and multiple weak symbols, choose the strong symbol.

이건 더 쉽다. strong symbol 한 개와 weak symbol 여러 개가 있을 때는 strong symbol를 선택한다는 것이다. 아래와 같은 경우, foo1.c에 있는 int a = 1;를 선택한다.

여기서 둘 중에 하나를 선택하니 어쩌니... 헛깔려하는 사람이 있을 거 같아서 한번 집고 넘어가보자. foo1.c를 컴파일하면 foo1.o가 나오겠지? 그건 relocatable object file이겠지? 거기에 있는 symbol 중에서 int a가 있겠지? (int a = 1로 선언되어 있으니까 strong symbol) 마찬가지로 bar1.c를 컴파일한 bar1.o에도 int a가 있는데 uninitialized global variable이므로 weak symbol이다. 이 두 파일을 링커로 연결할 때 int a에 대한 선언은 foo1.o에 있는 걸로 할꺼냐? 아니면 bar1.o에 있는 걸로 할꺼냐? 이럴 경우에 대한 규칙이다. 이럴 경우 strong symbol인 foo1.o에 있는 걸 선택한다는 것이다.


Rule 3 : Given multiple weak symbols, choose any of weak symbols.

이것도 쉽다. weak symbol이 여러 개 있으면 아무거나 선택하라는 것이다.

아무거나 둘 중에 하나 선택하라는 것이다.


링커가 여러 개의 multiple-defined symbol를 만나면 처리하는 규칙에 대해서 알아봤다.

여기서 하나 더 알아봐야 하는 링커의 규칙이 있다. 보통 우리가 리눅스 환경에서 링커 명령어를 사용할 때 아래와 같이 사용할 수 있다.


ld -o hello main.o swap.o 


사실 위와 같이 실행을 하면 "cannot find entry symbol _start ....." 와 같은 에러가 날 것이다. _start 라는 함수는 hello라는 프로그램의 entry point를 정해주는 그런 역할을 하는 함수인데 그 함수가 정의되어있지 않기 때문에 위와 같은 에러가 난다. 해결법은 _start가 정의되어있는 object file을 찾아서 같이 링크해주면 된다. 가령 그 파일이 /usr/lib/abc.o라고 한다면 아래와 같이 사용하면 될 것이다.


ld -o hello main.o swap.o /usr/lib/abc.o


위의 명령어는 틀릴 수 있다는 것을 잊지 말아줬으면 한다. 단순히 설명을 하기 위해 쓴 예시일 뿐이다. 그래서 말하고 싶은 것은 순서이다. 링커는 object file을 나열할 때 object file의 순서에 따라서 에러가 날 수 있다. 이 부분은 개인적으로 공부해보길 바란다. 우선 나도 그렇게 제대로 알지 못 하고 중요도에 비해 양이 많은 것 같기도 하다.


다음 포스팅은 relocation에 대한 내용이 될 것이다. 참 이 부분 공부하기가 그렇게 쉽지 않을 줄은 알았는데 생각해보면 별로 어렵지도 않는 것 같기도 하면서 그렇게 또 쉬운 것 같지도 않고... 이건 무슨 쉬운 건지 어려운 건지 결정하는게 더 어렵다.


이게 어렵든 쉽든 나는 최선을 다해서 공부할 거다.

Posted by 빛나유
,