dayne의 블로그

PIC(Position Independent Code)란 본문

OS

PIC(Position Independent Code)란

dayne_ 2024. 10. 18. 11:26

목차

1. PIC의 정의

2. PIC의 필요성

3. PIC 동작 방법

 

 


1. PIC의 정의

PIC는 메모리에서 특정 위치에 고정되지 않고 어느 위치에서든 실행이 가능한 코드를 의미하며, 절대 주소를 사용하지 않고, 상대 주소를 사용하여 메모리 주소 의존성을 제거합니다.

  • 절대 주소
    • 절대 주소는 프로그램이 실행되는 동안 메모리에서 특정 고정 주소를 참조하는 것을 말합니다.
      만약 프로그램이 컴파일될 때 특정 주소를 절대적으로 사용하도록 하드코딩된 경우, 코드가 실행될 때 그 주소에 로드되지 않으면 잘못된 메모리 접근이 발생할 수 있습니다.
    • 예를 들어, 특정 변수가 메모리의 0x1000번지에 있다고 가정하고 코드가 그 주소에 하드코딩되어 있으면, 해당 주소가 프로그램 실행 시 다른 프로세스에 의해 사용 중이거나 해당 위치에 로드되지 않을 때, Segmentation Fault(메모리 접근 오류)와 같은 문제를 일으킬 수 있습니다.

 

 

PIC를 사용하여 컴파일을 진행하면, 코드가 메모리의 어떤 주소에 로드되더라도 문제없이 동작할 수 있도록 구성됩니다.

 


2. PIC의 필요성

PIC는 특히 공유 라이브러리에서 유용하게 사용됩니다.

 

운영체제는 공유 라이브러리의 코드를 메모리의 '공유 가능 영역'에 로드하고, 이를 프로세스가 참조할 수 있도록 합니다.

 

공유 라이브러리가 여러 프로그램에 의해 사용될 때, 각각의 프로그램이 메모리에서 공유 라이브러리의 코드 영역을 공유할 수 있습니다. 하지만 이는 PIC로 컴파일된 경우에만 안전하게 동작합니다.

 

<PIC 없이 공유 라이브러리를 컴파일할 경우>

 

PIC 없이 컴파일된 라이브러리가 여러 프로그램에 의해 동일한 메모리 주소에 로드될 수는 없습니다. 운영 체제가 이를 허용하지 않으며, 각각의 프로그램은 고유한 가상 메모리 공간을 사용하기 때문입니다.

  • 만약 어떤 프로그램이 특정 주소에 라이브러리를 로드한 상태라면, 다른 프로그램이 그 동일한 주소에 동일한 라이브러리를 로드하려고 하면 충돌이 발생할 수 있습니다.
  • 이 충돌을 방지하기 위해 운영 체제는 다른 가상 메모리 주소에 라이브러리를 로드하여 프로그램마다 독립적인 실행이 가능하도록 합니다.

때문에, PIC 없이 컴파일된 라이브러리는 프로그램마다 서로 다른 메모리 주소에 로드될 수밖에 없습니다. 운영 체제는 프로그램마다 라이브러리를 로드할 적절한 메모리 주소를 다르게 배정합니다.

  • 만약 PIC 없이 컴파일된 라이브러리를 여러 프로그램이 사용하려고 한다면, 각 프로그램이 해당 라이브러리를 다른 주소에 로드하게 되어 메모리 낭비가 발생할 수 있습니다. 이로 인해 메모리 공유가 제대로 이루어지지 않으며, 프로그램 간의 코드 공유도 불가능합니다.

 

 

<그래서 PIC를 사용하게 되면>

 

PIC로 컴파일된 공유 라이브러리는 어떤 메모리 주소로 로드되더라도 문제가 없습니다. (공유 라이브러리가 해당 라이브러리 관련 메모리에 로드되어 있다면, 다른 프로세스들이 해당 메모리를 참조하는 것을 가능하도록 함)

 

이 때문에 PIC로 컴파일된 공유 라이브러리는 여러 프로그램이 동시에 사용하더라도 메모리 낭비 없이 코드 영역을 공유할 수 있습니다.

 

PIC를 사용해 컴파일된 공유 라이브러리는 각 프로그램이 별도의 라이브러리 복사본을 메모리에 유지할 필요가 없게 해줍니다.

 

 

 


3. PIC 동작 방법

PIC는 함수 호출과 데이터 참조 시 절대 주소가 아닌 상대 주소를 사용하여 다양한 메모리 위치에서 실행될 수 있습니다. PIC의 주요 동작 원리는 Global Offset Table (GOT)와 Procedure Linkage Table (PLT)이라는 구조를 활용해 상대 주소로 필요한 정보를 동적으로 참조하는 것입니다.

 

3.1 Global Offset Table (GOT)

GOT는 PIC에서 중요한 역할을 하는 테이블로, 프로그램이 전역 변수나 함수와 같은 심볼을 실제 메모리에서 참조하는 데 필요한 정보를 저장합니다. PIC 코드가 메모리 내의 심볼을 참조하려고 할 때, 절대 주소 대신 GOT의 특정 엔트리를 참조합니다. 이 엔트리에는 전역 변수 혹은 함수가 실제로 할당된 메모리 주소가 저장됩니다.

 

GOT는 공유 라이브러리 내에 존재 합니다.

 

동작 과정:

  1. 프로그램이 전역 변수 또는 함수에 접근할 때, 해당 심볼(전역 변수, 함수)의 실제 메모리 주소를 직접 참조하지 않고, GOT의 특정 항목을 먼저 참조합니다.
  2. GOT의 해당 항목에는 심볼의 실제 메모리 주소가 저장되어 있으므로, 이를 기반으로 심볼을 참조할 수 있습니다.
  3. 이렇게 하면 프로그램이 어디에 로드되었는지와 관계없이 심볼을 올바르게 참조할 수 있습니다.

 

3.2 Procedure Linkage Table (PLT)

 

PLT는 PIC에서 함수 호출 시 사용하는 메커니즘으로, 동적 연결된 함수의 실제 주소를 런타임에 결정하는 데 사용됩니다. 공유 라이브러리에서 사용하는 함수들은 절대 주소를 알 수 없기 때문에, PLT를 통해 함수를 동적으로 호출할 수 있습니다.

 

PLT는 공유 라이브러리 내에 존재합니다.

 

동작 과정:

  1. 프로그램이 공유 라이브러리의 함수를 호출할 때, 먼저 PLT에 정의된 항목을 통해 함수의 실제 주소를 찾습니다.
  2. 처음 호출될 때는 동적 링커가 그 함수의 실제 주소를 해결하고, PLT 항목에 저장합니다.
  3. 이후 해당 함수가 호출될 때는 PLT에서 저장된 함수 주소를 사용해 함수를 호출합니다.
  4. 이 방식은 처음 호출 시에만 주소를 동적으로 해결하고, 이후에는 빠르게 해당 함수로 제어를 넘길 수 있습니다.

 

 

3.3 상대 주소 기반 참조

 

PIC는 메모리의 절대 주소에 의존하지 않고 상대 주소를 사용하여 메모리 위치를 참조합니다. 상대 주소를 사용하면 프로그램이 어느 메모리 주소에 로드되더라도, 그 기준 위치를 기준으로 변수를 참조할 수 있습니다.

 

동작 과정:

  • 프로그램이 실행될 때, 기준 위치가 결정됩니다. 이 기준 위치는 프로그램이 실제로 메모리에 로드된 주소를 의미합니다.
  • PIC 코드는 함수나 변수에 접근할 때, 기준 위치에 상대적인 오프셋을 사용하여 해당 변수나 함수에 접근합니다.
  • 따라서 코드가 어디에 로드되든, 기준 위치에 따라 올바른 메모리 위치를 참조할 수 있게 됩니다.

 

3.4 동적 링커(ld.so)의 역할

 

PIC 코드를 사용하는 프로그램이 실행될 때, 동적 링커(dynamic linker, ld.so)가 메모리에서 프로그램을 로드하고, GOT와 PLT를 초기화하여 각 심볼이 가리키는 실제 메모리 주소를 설정합니다.

 

동작 과정:

  • 프로그램이 처음 실행되면, 동적 링커는 공유 라이브러리의 함수나 전역 변수를 참조할 수 있도록 GOT와 PLT를 설정합니다.
  • 이렇게 설정된 GOT와 PLT는 프로그램이 실행되는 동안 계속해서 참조됩니다.

 

3.5 PIC 동작 방식의 예

 

<공유 라이브러리 내의 함수에 대한 첫 번째 호출 시의 처리 과정>

출처 : https://www.slideshare.net/slideshow/elfexecutable-and-linkable-format/226703637

 

<공유 라이브러리 내의 함수에 대한 첫 번째 호출 이후의 호출 처리 과정>

출처 : https://www.slideshare.net/slideshow/elfexecutable-and-linkable-format/226703637

 

  • 첫 번째 사진
    1. 코드에서 call func@PLT 호출
      • 프로그램 코드에서 공유 라이브러리의 func 함수가 호출됩니다. 이 호출은 PLT의 해당 함수 엔트리인 PLT[n]이로 이동하게 됩니다.
      • PLT[0]에서 resolver 호출
        • PLT의 첫 번째 항목은 PLT의 초기 설정에 사용됩니다. PLT[0]은 resolver, 즉 함수의 실제 위치를 찾기 위한 과정으로 연결됩니다.
    2. PLT[n]: jmp *GOT[n]
      • 이 과정에서 PLT는 GOT의 해당 항목을 참조하여 함수를 호출합니다. 첫 번째 호출 시, GOT의 항목은 아직 해당 함수의 실제 주소를 포함하지 않으므로 resolver를 호출하기 위한 준비 단계가 진행됩니다.
    3. PLT[0]으로 복귀
      • 아직 해당 함수의 실제 주소가 GOT에 설정되어 있지 않으므로 PLT[0]으로 돌아와서 resolver가 호출됩니다. 이 resolver는 실제로 동적 링커가 나서서 해당 함수의 주소를 찾고 GOT의 해당 항목에 그 주소를 저장합니다. 이후 GOT는 더 이상 prepare resolver를 가리키지 않고, 해당 항목의 실제 주소를 가리키게 됩니다.
  • 두 번째 사진
    1. 코드에서 call func@PLT 호출
      • 프로그램에서 동일한 func 함수가 다시 호출됩니다. 이전 호출로 인해 PLT 및 GOT의 설정이 완료되어 있으므로, 첫 번째 호출 때와 과정이 달라집니다.
    2. PLT[n]: jmp *GOT[n]
      • PLT는 GOT의 해당 항목을 참조하여 jmp *GOT[n]을 수행합니다. 이번에는 이미 GOT에 함수의 실제 주소가 저장되어 있으므로 바로 함수로 점프하게 됩니다.
    3. GOT에서 함수의 실제 주소 참조 후 실행
      • PLT를 통해 GOT에 저장된 주소를 참조하고, 바로 해당 함수의 실제 주소로 이동하여 실행됩니다.

 

 

PIC는 프로그램이 메모리 주소에 의존하지 않고 어디에서든 로드되어 실행될 수 있도록, 상대 주소와 GOT, PLT 같은 테이블을 활용해 동적으로 메모리 참조를 처리합니다. 이를 통해 하나의 공유 라이브러리가 여러 프로그램에서 동시에 사용되거나, 프로그램이 실행될 때마다 다른 메모리 주소에 로드되더라도 정상적으로 동작할 수 있게 합니다.

 

 


참고

https://nuc13us.wordpress.com/2015/12/25/hack-using-global-offset-table/

 

HACK Using Global Offset Table

This method is useful when ASLR(Address Space Layout Randomization) is enable or one is unable to overwrite a Instruction Pointer. Global Offset Table is something like a cache. It actually stores …

nuc13us.wordpress.com