7.12 Position-Independent Code (PIC)(위치 독립성 코드)
- 공유 라이브러리의 주요 목적은 실행 중인 여러 프로세스가 메모리에서 동일한 라이브러리 코드를 공유하여 귀중한 메모리 공간을 절약할 수 있도록 하는 것이다.
- 그렇다면 어떻게 여러 프로세스가 프로그램의 단일 복사본을 공유할 수 있을까?
- 한 가지 접근 방법은 사전에 각 공유 라이브러리에 대한 주소 공간의 특정 chunk를 할당한 다음에 loader가 항상 해당 주소에서 라이브러리를 load하도록 요구하는 것이다.
- 간단하지만 이 접근 방식은 몇 가지 심각한 문제를 야기한다.
- 프로세스가 라이브러리를 사용하지 않더라도 공간의 일부가 할당되기 때문에 주소 공간을 비효율적으로 활용한다.
- 주소 공간을 관리하기도 어렵게 된다. 라이브러리가 수정될 때마다 할당된 청크에 여전히 맞는지, 청크가 겹치지 않는 지 확인해야 한다. 맞지 않다면 새로운 청크를 찾아야 한다.
- 만약 새로운 라이브러리를 만들었다면 그것을 위한 공간을 찾아야 한다.
- 시간이 지남에 따라 시스템에 있는 수백 개의 라이브러리와 라이브러리 버전이 주어지면 주소 공간을 더욱 관리하기 어려울 것이다.
- 간단하지만 이 접근 방식은 몇 가지 심각한 문제를 야기한다.
- 위 같은 문제를 피하기 위해, 최신 시스템은 shared modules의 code segement를 컴파일하여 linker에서 수정하지 않고도 메모리의 어느 위치에서나 load 할수 있다. 이 방식을 이용하면 shared modules segement의 단일 복사본을 프로세스 수에 제한 없이 공유할 수 있다. (물론 여전히 표준I/O는(읽기 쓰기 데이터) run-time 동안 실행 중인 각 프로세스의 text segement에 복사된다)
위치 독립 코드(position-independent code-pic)
- 위 방식처럼 relocation 없이 load할 수 있는 코드를 위치 독립 코드(position-independent code-pic)라 한다.
- 사용자는 gcc에 -fpic 옵션을 사용하여 PIC코드를 생성하도록 GNU 컴파일 시스템에 지시한다.
- 공유 라이브러리는 항상 이 옵션으로 컴파일해야 한다.
- x86-64 시스템에서 동일한 실행 가능한 object module의 symbol에 대한 reference는 PIC가 되기 위한 특별한 처리가 필요하지 않다.
- 이러한 참조는 PC-relative addressing을 사용하여 컴파일할 수 있으며, object file을 빌드할 때 static linker에 의해 relocation 될 수 있다.
- 그러나 shared module에 의해 define된 외부 프로시저 및 전역 변수에 대한 참조는 아래에서 설명할 몇 가지 특별한 기술이 필요하다.
PIC Data References
- 컴파일러는 다음과 같은 구조를 이용하여 전역 변수에 대한 PIC 참조를 생성한다.
- 메모리에서 object module(shared object module 포함)을 로드하는 위치에 관계없이 데이터 segement 항상 코드 segement와 동일한 거리에 존재한다.
- 따라서 코드 segement의 instruction과 데이터 segment의 변수 사이의 거리는 코드 및 데이터 segement의 절대적인 메모리 위치와 관계 없이 run-time constant(상수) 이다.
- 전역 변수에 대한 PIC 참조를 생성하려는 컴파일러는 데이터 segement의 시작 부분에 global offset table(GOT)이라는 테이블을 생성하여 위 구조를 이용한다.
- GOT에는 object module이 참조하는 각 전역 데이터 object(프로시저 또는 전역 변수)에 대한 8byte 항목이 포함되어 있다.
- 컴파일러는 또한 GOT의 각 항목에 대한 relocation record를 생성한다.
- load 시 dynamic linker는 object의 절대 주소를 포함하도록 GOT의 각 항목을 relocation 한다.
- global object를 참조하는 각 object module에는 자체 GOT가 있다.
- 위 그림은 예제 libvector.so shared module의 GOT를 보여준다.
- addvec routine은 전역 변수 addcnt의 주소를 GOT[3]를 통해 간접적으로 로드한 다음에 메모리에서 addcnt를 증가시킨다.
- 여기서 핵심은 GOT[3]에 대한 PC-relative reference의 offset이 run-time constant라는 것이다.
- addcnt는 libvector.so 모듈에 의해 정의되므로 컴파일러는 addcnt에 대한 직접적인 PC-relative reference를 생성하고 linker가 shared module를 빌드할 때 resolve하기 위한 relocation를 추가로 수행하여 코드와 데이터 segement 사이의 일정한 거리를 이용할 수 있다.
- 그러나 addcnt가 다른 shated module에 의해 define된 경우 GOT를 통한 간접 액세스가 필요하다. 이 경우 컴파일러는 모든 참조에 대해 가장 일반적인 솔루션인 GOT를 사용하도록 선택했다.
PIC Function Call
- 프로그램이 공유 라이브러리에 의해 정의된 함수를 호출한다고 가정하자.
- 컴파일러는 함수의 런타임 주소를 예측할 방법이 없다. 이를 정의하는 공유 모듈이 run-time에 어디에서나 로드될 수 있기 때문이다.
- 일반적인 접근 방식은 참조에 대한 relocation record를 생성하는 것이다.
- 그런 다음 프로그램이 load될 때 이 record를 resolve할 수 있다.
- 그러나 이 접근 방식은 linker가 호출 모듈의 코드 segment를 수정해야하므로 PIC가 아니다.
- GNU 컴파일 시스템은 프로시저가 처음 호출될 때까지 각 프로시저 주소의 binding을 지연시키는 lazy binding이라는 흥미로운 기술을 사용하여 미 문제를 해결한다.
- lazy binding의 motivation은 일반적인 응용 프로그램이 libc.so와 같은 공유 라이브러리에서 내보낸 수백 또는 수천 개의 불필요한 relocation을 피할 수 있다.
- 함수가 처음 호출될 때 적지 않은 run-time 오버헤드가 있지만 그 이후의 각 호출에는 단일 instruction과 간접 참조를 위한 메모리 참조만 필요하다.
- lazy binding은 GOT와 PLT(procedure linkage table)라는 두 데이터 구조 간의 간결하지만 다소 복잡한 상호 작용으로 구현된다.
- object module이 공유 라이브러리에 정의된 함수를 호출하면 자체 GOT 및 PLT가 존재한다.
- GOT은 데이터 segemnet의 일부이고 PLT는 코드 segement의 일부이다.
- 위 그림은 PLT와 GOT가 함께 작동하여 런타임에 함수의 주소를 확인하는 방법을 보여준다.
- 먼저 각 테이블의 내용을 보면 ,
- PLT(procedure linkage table): PLT는 16바이트 코드 entry의 배열이다.
- 실행 파일에 의해 호출되는 각 공유 라이브러리 함수에는 자체 PLT entry가 있다.
- 이러한 각 entry는 특정 함수를 호출하는 역할을 한다.
- PLT[0]은 dynamic linker로 jump하는 특수 entry이다.
- PLT[1](위에서 표시x)은 실행 환경을 초기화하고 기본 함수를 호출하여 반환 값을 처리하는 시스템 시작 기능(_libc_start_main)을 호출한다.
- PLT[2]부터의 entry는 사용자 코드에서 호출하는 함수를 호출한다. 위 예제에서 PLT[2]는 addvec를 호출하고 PLT[3](표시x)은 pirntf를 호출한다.
- GOT(global offset table): GOT는 8바이트 주소 entry의 배열이다.
- PLT와 함께 사용되는 경우 GOT[0] 및 GOT[1]에는 dynamic linker가 함수 주소를 확인할 때 사용하는 정보가 포함된다.
- GOT[2]는 ld-linux.so 모듈의 dynamic linker에 대한 entry point 이다.
- 나머지 각 entry는 런타임 시 주소를 확인해야 하는 호출된 함수에 해당된다.
- 각각 일치하는 PLT entry가 있다.
- 예를 들어, GOT[4]와 PLT[2]는 addvect에 해당한다.
- 처음에 각 GOT entry는 해당 PLT entry의 두번째 instruction를 point한다.
- PLT(procedure linkage table): PLT는 16바이트 코드 entry의 배열이다.
- 그림 7.19 (a)는 addvec 함수가 처음 호출될 때 함수 addvec의 run-time 주소를 lazily 하게 resolve하기 위해 GOT와 PLT가 함께 작동하는 방법을 보여준다.
- 1단계: 프로그램은 addvec을 직접 호출하는 대신 addvec의 PLT entry인 PLT[2]를 호출한다.
- 2단계: 첫 번째 PLT intruction은 GOT[4]를 통해 간접 점프를 수행합니다. 각 GOT entry는 초기에 해당 PLT entry의 두 번째 instruciton을 가리키므로 간접 점프는 단순히 PLT[2]의 다음 instrucion으로 제어를 다시 전송합니다.
- 3단계: addvec(0x1)의 ID를 스택에 푸시한 후 PLT[2]는 PLT[0]으로 점프합니다.
- 4단계: PLT[0]은 GOT[1]을 통해 간접적으로 dynamic linker에 대한 인수를 푸시한 다음 GOT[2]를 통해 간접적으로 dynamic linker로 점프합니다. dynamic linker는 두 개의 스택 항목을 사용하여 addvec의 런타임 위치를 결정하고 GOT[4]를 이 주소로 덮어쓰고 제어권을 addvec에 넘깁니다.
- 그림 7.19 (b)는 addvec의 후속 호출에 대한 제어 흐름을 보여줍니다.
- 1단계. 제어가 이전과 같이 PLT[2]로 전달됩니다.
2단계. 그러나 이번에는 GOT[4]를 통한 간접 점프가 제어를 직접 addvec에 전달합니다.
- 1단계. 제어가 이전과 같이 PLT[2]로 전달됩니다.
'Computer Science > 컴퓨터 구조' 카테고리의 다른 글
[CSAPP] 7.7 Relocation(재배치) (0) | 2023.02.21 |
---|---|
[CSAPP] 7.6 Symbol Resolutions(심볼 해석) (0) | 2023.02.21 |
[CSAPP] 7.11 Loading and Linking Shared Libraries from Applications (응용 프로그램으로부터 공유 라이브러리를 로드하고 링크하기) (0) | 2023.02.17 |
[CSAPP] 7.10 Dynamic Linking with Shared Libraries(공유 라이브러리 동적 링크) (0) | 2023.02.17 |
[CSAPP] 7.5 Symbols and Symbol Tables(심볼과 심볼테이블) (0) | 2023.02.15 |