본문 바로가기
Linux/Ubuntu

Linux C/C++ shared library 컴파일하기 fPIC 옵션 GOT, PLT

by ahsung 2020. 2. 6.

c++

 

g++ -shared -fPIC -o <shared library file> <cpp file>

g++ -shared fPIC -o lib.so code.cpp

 

c

 

gcc -shared [-fPIC] -o <shared library file> <c file>

gcc -shared [-fPIC] -o lib.so code.c

-fPIC 생략가능 하지만 공유라이브러리를 만들 때는 -fPIC 옵션 사용을 권장합니다.

(컴파일 종류에 따라서 -fPIC는 필수 옵션입니다.)

 

fPIC를 사용하지 않을경우 재배치 기법을 사용하게 되는데,

간략하게

라이브러리의 주소를 현재 사용하려는 프로세스 주소에서 접근가능하게 재배치 한다는 개녑입니다.

 

단점은

1. 여러 프로세스가 공유라이버리를 공유하게되면 각 프로세스가 로드 할때마다 공유라이브러리의 text section(프로세스의 코드가 있는 영역)가 변해야하는데 이는 불가능하다. -> 로드 타임에 각 프로세스마다 재배치해야한다.

 

불가능한 이유는 각 변수 symbol들의 포인터 등의 주소가 재배치할때 정해져 text 영역에 써지게 되는데, Static 라이브러리의 경우 실행파일에 그대로 넣기 때문에 문제가 없지만, 공유라이브러리의 경우 각 프로세스에서 symbol(변수, 함수 등등)을 다른 메모리에 저장할 수 있으므로, 포인터 주소값이 모두 같을 수 없습니다.

 

2. 프로세스가 여러 공유라이브러리를 공유하게되면 재배치하는 시간이 매우 길어짐

 

 

ex)  int a;

        int *b = &a; 

 

이런 .c 파일을 object(.o) file로 컴파일 하게되면  b안에 a의 주소값은 미정으로 되어 있고, 링킹타임에 a의 주소가 정해져 실행파일이 생성되면, 실행파일의 text영역 안에는 b값에 a의 주소가 제대로 적혀있습니다.

(프로세스의 가상메모리 위치가 정해지는 것입니다. 프로세스마다 가상메모리는 독립적이기 때문에 static하게 정해져도 상관 없음

가상 메모리 -> 물리메모리 매핑은 커널의 역할임)

그러나 공유라이브러리의 경우 여러 프로세스가 이 라이브러리 .so파일을 공유할텐데 프로세스마다 a의 주소를 각각 다르게 할당하기 때문에  .so파일 내부의 text영역 코드를 수정할수 없고 프로세스마다 로드 타임에 각자의 코드 영역에서 주소를 재배치하게 됩니다.

(Symbol이란 변수, 함수와 같이 프로세스가 실행될때 메모리를 지정해줘야 되는 객체를 말합니다.

변수는 메모리안에 value를 넣기 위해서,

함수는 .text영역에 operation 코드를 위해서)

 

그래서 권장해서 쓰는것이 fPIC option이다.

 

fPIC는 상대 주소를 사용하여 재배치의 단점을 보완하였습니다. 즉 상대적인 위치를 저장함으로서 Position Independent Code  (PIC) 프로세스마다 위치 독립적인 코드로 컴파일 하여 사용하는 것 입니다.

 

함수와 같이 .so의 text영역에서 그대로 가져오면 되는 경우 공유라이브러리의 내부적으로 상대적인 위치만 알면 어느 프로세스든 공유라이브러리를 자신의 가상메모리에 올려도 공유라이브러리 내부에서 서로 call하는 경우는 상대적 위치만 알아도 충분히 가능합니다.

 

그러나 공유라이브러리에서 정의된 전역변수는 공유하지 않고  프로세스마다 직접 할당하고, 프로세스의 .text영역의 코드에서 공유라이브러리의 심볼에 해당하는 함수를 콜하기 위해서는 함수의 시작 주소를 알고 있어야합니다.

 

그래서 공유 라이브러리를 사용할 때는 GOT (global offset table)을 사용하게 됩니다.

GOT는 global symbol들의 절대주소와 기타 정보들이 적혀있고, 프로세스마다 메모리에 각자 할당하게 됩니다.

ex) GOT[3]: 0x1234  // int a의 주소

        GOT[4]: 0x1288 // double b의 주소

공유라이브러리에서 선언된 전역변수를 접근하는 코드가 공유라이브러리에 있다면

이때 공유라이브러리는 프로세스가 전역변수를 어디에 할당할지 알 수 없기 때문에 공유라이브러리의 .text영역에 전역변수 주소를 어떻게 할당할지 문제에 빠질 수 있습니다.  이 공간에는 GOT의 특정 엔트리 주소가 들어가 있고, GOT의 엔트리 내부에 실제 주소를 통해 접근 할 수 있습니다.

GOT의 위치는 프로세스의 가상메모리 공간속에서 항상 공유라이브러리와 상대적으로 일정한 위치에 존재함으로서, fPIC코드로 접근 가능하며, GOT안에는 프로세스에서 할당한 가상메모리의 실제주소가 들어있습니다.

그렇나 함수의 경우 공유라이브러리 내부에 1000개 이상 수 많은 함수가 존재한다면 링킹타임에 공유라이브러리의 모든 함수 instruction 주소를 알아내어 Process의 가상메모리에 바인딩하는데 오랜 시간이 걸릴 수 있습니다. 보통의 경우 라이브러리의 함수를 모두 사용하는 경우도 없기 때문에 효율성을 위한 방법으로 PLT (Procedure Linkage Table)을 사용합니다.

 

PLT는 공유라이브러리의 코드영역에 있어 따로 프로세스마다 RAM의 메모리를 더 할당하지 않고 가상메모리에 매칭하여 사용하고

PLT는 공유라이브러리 함수들의 엔트리로서 항상 프로세스에서 공유라이브러리의 함수가 호출될때는 PLT에 call한 함수에 대응하는 엔트리 주소로 이동합니다.

ex)  callfunc()   ->   PLT[n] 주소로! (callfunc이 PLT 몇번일지는 컴파일될때 알고있음)

 

각 PLT 엔트리의 첫 코드는 해당 함수 symbol에 대응되는 프로세스 GOT의 엔트리 주소이며 GOT로 jump합니다.

ex) PLT[3] -> GOT[3]

이때 처음 함수가 call 된 것이라면,  GOT 엔트리 내부 주소는 다시 PLT 엔트리중 dynamic_linker를 호출할 수 있는 주소가 저장되어고  dynamic linker를 실행 시켜 공유라이브러리의 함수의 물리주소를 process의 가상메모리에 바인딩시키고, 그 뒤에 GOT 엔트리에 바인딩된 함수의 절대주소(가상메모리주소/상대주소아님)를 적음으로서, 2번째 호출부터는   PLT -> GOT -> 함수절대주소로 바로 이동하게 됩니다. 

- 참고: https://www.cs.swarthmore.edu/~kwebb/cs31/s15/bucs/chapter08.html

 

Chapter 9. Dynamic Linking

Including libraries in an executable

www.cs.swarthmore.edu

 

이 포스팅에서 말하는 (그냥)주소는 모두 가상주소를 말하는 것이며 간혹 할당한다는 말은 공유라이브러리의 경우 대부분 프로세스에서 실제 DRAM을 더 이상 할당하는게 아닌 가상메모리로 매칭만 하기 때문에  전역변수와 GOT와 같이 프로세스마다 실제 DRAM에 메모리를 할당하는 경우에는 "할당"한다는 표현을 썼습니다.

 그리고 Dynamic linker가 하는 일은 DRAM에 있는 공유라이브러리의 주소와 프로세스의 가상메모리 주소의 매핑(바인딩)입니다.

 

PLT를 사용하면, 공유라이브러리의 모든 함수를 다 읽을 필요 없이 간략화 시킨 PLT와 GOT를 이용하여 함수를 호출할 때 해당 함수만 프로세스에 바인딩 시키고 GOT에 절대주소를 넣음으로서 2번째 콜부터는 바로 GOT에서 절대주소를 찾아 call하게됩니다.

 

댓글