By salen | May 3, 2016
linux 환경의 메모리 보호기법을 알아보자(3)
이번 편에서는 다음 편에서 다룰 PIE라는 개념을 위해 먼저 PIC에 대해 알아보도록 하겠습니다!
배경 지식들과 함께 알아봅시다.
- 실습 환경 Ubuntu 15.10 32bits
정적 라이브러리와 공유 라이브러리
1) 정적 라이브러리
정적 라이브러리(Static Library)는 여러 프로그램에서 사용되는 함수를 포함하는 오브젝트 파일들을
ar명령을 이용하여 하나의 아카이브 파일(.a)로 모아놓은 것입니다.
정적 라이브러리를 만드는 과정을 통해 라이브러리 내의 함수를 어떻게 다른 실행 파일에서 호출할 수 있는 지 알아보겠습니다.
$ vi my.c // myFunc이라는 함수를 my.c 파일에 정의
#include <stdio.h>
void myFunc( int a, int b ){
int sum = 0;
sum = a + b;
printf( "[myFunc] sum : %d\n", sum );
}
$ gcc -c my.c // gcc를 이용하여 my.c 파일의 오브젝트 코드 생성
$ ar rv libmy.a my.o // ar 프로그램을 이용하여 라이브러리 파일(.a) 생성
$ vi static_test.c // 만든 라이브러리 함수를 호출하는 프로그램 생성
#include <stdio.h>
int main(){
printf( "[static_test] main() start!\n" );
myFunc( 10, 10 );
printf( "[static_test] main() finish!\n" );
return 0;
}
$ gcc static_test.c -o static_test libmy.a // 정적라이브러리를 포함하여 컴파일
$ ls // 실행파일이 잘 생성되었는지 확인
$ ./static_test // 프로그램 실행
위와 같이 정적 라이브러리를 링크할 경우,
링커는 다른 오브젝트 파일에서 정의되지 않은 심볼을 찾아 지정된 정적 라이브러리에서
해당 심볼을 정의하고 있는 오브젝트 파일의 사본을 추출해서 실행 파일 내에 포함시킵니다.
# 다른 시스템에서도 잘 동작하는 모습
이렇게 정적 라이브러리를 이용하여 실행 파일을 생성하면 해당 실행 파일 내에 라이브러리 함수의 코드가 포함됩니다.
따라서 같은 라이브러리 함수를 여러 프로그램에서 사용하게 되면 각각의 실행 파일마다 똑같은 라이브러리 함수의 코드가 포함됩니다
이렇게 되면 동일한 함수의 코드가 메모리상의 여러 곳에 존재하게 되므로 메모리나 하드 디스크 공간이 낭비되게 됩니다.
이러한 정적 라이브러리의 단점을 해결한 것이 다음에 설명할 공유 라이브러리입니다.
2) 공유 라이브러리
공유 라이브러리는 여러 오브젝트 파일을 하나의 오브젝트 파일로 만들어 이를 공유할 수 있도록 한 것입니다.
즉 실행 파일에 라이브러리 함수 코드가 포함되는 것이 아니라 실행 시 공유 라이브러리를 참조하는 방식(2편 참고)으로 링크됩니다.
일반적으로 공유 라이브러리는 다음과 같이 생성합니다.
$ vi my.c // myFunc이라는 함수를 my.c 파일에 정의
#include <stdio.h>
void myFunc( int a, int b ){
int sum = 0;
sum = a + b;
printf( "myFunc] sum : %d\n", sum );
}
$ gcc -c -fPIC my.c // 독립적인 코드로 만들기 위해 gcc의 -fPIC 옵션을 이용하여 컴파일
$ gcc -shared -o libmy.so my.o // 공유 라이브러리 즉, libmy.so 파일 생성
$ vi /etc/ld.so.conf
/etc/ld.so.conf.d 디렉터리 안에 *.conf 란 파일명으로 파일을 만들고
그 파일에 자신이 만든 공유 라이브러리 파일(.so)의 전체 경로를 적어둡니다.
$ vi /etc/ld.so.conf.d/mylib.conf
추가 후 ldconfig 명령을 이용하여 캐쉬를 갱신합니다.
그러면 이제 공유 라이브러리를 다른 모든 실행파일 내에서 사용할 수 있게 됩니다.
$ vi dynamic_test.c // 만든 라이브러리 함수를 호출하는 프로그램 생성
#include <studio.h>
int main(){
printf( "[dynamic_test] main() start!\n" );
myFunc( 10, 10 );
printf( "[dynamic_test] main() finish!" );
return 0;
}
$ gcc dynamic_test.c -o dynamic_test -lmy -L. // 공유 라이브러리 링킹
ldd(List Dynamic Dependencies) 는 프로그램에서 요구하는 공유 라이브러리를 출력해 주는 프로그램입니다.
같이 ldd를 이용하여 dynamic_test 프로그램에서 libmy.so 라는 공유 라이브러리를 참조하는 것을 확인할 수 있습니다.
공유 라이브러리를 링크한 실행 파일을 실행할 경우에는 동적 링커 로더(ld.so)가
해당 실행 파일에서 필요한 공유 라이브러리를 찾아내어 실행 시 해당 프로세스의 메모리 맵을 조작해서
공유 라이브러리와 실행 바이너리가 같은 프로세스 공간을 사용하도록 합니다.
즉 실제 라이브러리 코드는 실행 파일에 포함되어 있지 않고 공유 라이브러리에만 존재합니다.
따라서 실행 파일을 배포할 때 실행 파일과 공유 라이브러리를 함께 배포해야 합니다.
그렇지 않으면 실행 시 라이브러리를 찾을 수 없다는 에러메시지가 나타납니다.
# 다른 시스템에서 라이브러리를 찾을 수 없다는 에러 발생 화면
PIC(Position Independent Code)
앞서 공유 라이브러리를 만들 때 -fPIC 옵션을 이용하여 소스를 컴파일 했습니다.
그리고 통상 GNU/리눅스의 공유 라이브러리를 만들 때는 각각의 .c 파일을 PIC가 되도록 컴파일한다고 합니다.
그렇다면 PIC가 무엇이고, PIC로 컴파일을 하면 뭐가 좋을까요?
Position-independent code(PIC)란 메모리의 어느 공간에든 위치할 수 있고 수정 없이 실행될 수 있는 “위치 독립 코드”입니다.
즉 이 코드를 사용하는 각 프로세스들은 이 코드를 서로 다른 주소에서 실행할 수 있으며, 실행 시 재배치가 필요 없습니다.
공유 라이브러리를 PIC로 만들면 뭐가 좋은 지를 예제를 통해 알아보겠습니다.
#include <studio.h>
void main(){
puts( "hi" );
puts( "hi" );
puts( "hi" );
}
다음과 같이 puts함수를 3번 호출하는 test-pic.c 파일을 생성합니다.
이 소스코드를 PIC로 컴파일 했을 때와 하지 않았을 때의 특징을 비교하기 위해
두 버전으로 컴파일 하여 공유 라이브러리를 만들어보겠습니다.
# gcc -shared -o no-pic.so test-pic.c // PIC가 아닌 공유 라이브러리 생성
# gcc -shared -fPIC -o pic.so test-pic.c // PIC 공유 라이브러리 생성
readelf의 -d 옵션으로 생성한 공유 라이브러리들의 dynamic 섹션을 확인해 보면 PIC가 아닌
공유 라이브러리에는 TEXTREL이라는 엔트리(텍스트 내의 재배치 필요)가 있고
RELCOUNT(재배치 횟수)는 6으로 PIC 공유 라이브러리보다 큽니다
(이 때 puts함수를 3회 호출하므 PIC 공유 라이브러리에서보다 3만큼 큰 것입니다).
PIC는 재배치가 필요 없음에도 불구하고 공유 라이브러리에서의 RELCOUNT가 0이 아닌 이유는
gcc가 기본적으로 사용하는 시작 파일에 포함된 코드 때문입니다.
-nostartfiles 옵션을 주고 컴파일을 하면 이 값은 0이 되어 아래와 같이 RELCOUNT 엔트리가 없어집니다
(이걸로 PIC는 재배치가 필요 없다는 말이 증명되었네요!).
위 예제에서 확인한 것과 같이 PIC가 아닌 공유 라이브러리는 실행 시 6개의 주소가 재배치되어야 합니다.
지금은 puts함수를 3회만 호출하는 간단한 프로그램이므로 재배치되어야 하는 주소의 개수가 얼마 없지만
프로그램이 커져서 재배치 수가 늘어난다면 프로그램 실행 시 재배치에 걸리는 시간이 매우 커질 것입니다.
또한 PIC가 아닌 공유 라이브러리는 실행 시 재배치가 필요한 부분의 코드를 재작성하기 위해
텍스트 섹션 내의 재배치가 필요한 페이지를 로드하고 이를 재 작성하는 과정을 거치다가 copy on write가 발생하여
다른 프로세스와 텍스트 섹션을 공유할 수 없는 상황이 발생할 수도 있습니다
공유 라이브러리의 장점이 텍스트 섹션을 다른 프로세스와 공유할 수 있어서 사용하는 것이었는데
이를 다른 프로세스와 공유할 수 없게 된다면 공유 라이브러리를 쓰는 이유가 없어지겠죠!
따라서 공유 라이브러리를 PIC로 생성하지 않으면 실행할 때 재배치에 시간이 소요된다는 단점과
다른 프로세스와 코드를 공유할 수 없게 될 수 있는 단점이 있기 때문에
통상적으로 공유 라이브러리를 작성할 때 .c 파일을 PIC로 컴파일 하는 것입니다.
non-PIC vs PIC
PIC가 아닌 공유 라이브러리와 PIC 공유 라이브러리가각각 함수를 어떻게 호출하는 지 그 차이에 대해 알아보도록 하겠습니다.
먼저 PIC가 아닌 공유 라이브러리를 생성합니다.
여기서 my.c은 앞서 정적 라이브러리와 공유 라이브러리 설명 때 사용했던 myFunc이라는 함수가 정의되어 있는 예제입니다.
printf 함수 호출 시 0x554를 call합니다.
0x554가 어느 섹션에 위치하는 지 알아보기 위해 gdb 상에서 info file 명령을 쳐보면
아래와 같이 0x554는 .text 섹션에 위치하는 코드입니다.
다음으로 PIC 공유 라이브러리에서 함수를 어떻게 호출하는 지 알아보겠습니다.
아래와 같이 PIC 공유 라이브러리를 생성한 후 gdb를 이용하여 디스어셈블합니다.
printf 함수 호출 시 printf@plt를 call합니다. 즉 plt를 경유하여 printf 함수를 호출합니다.
Relocatable code vs PIC
마지막으로 헷갈릴 수도 있는 Relocatable code와 PIC의 차이에 대해 정리해 드리겠습니다.
Relocatable code는 말 그대로 재배치(relocation)를 해야 하는 코드를 의미합니다.
컴파일러에 의해 생성된 코드는 실제로 메모리상의 어느 위치에 로드 될 지 알 수 없으므로
로드 시에 결정된 위치에 따라 참조하는 함수/변수의 주소를 변경해 주어야 합니다.
executable file로 링크되는 코드는 로드되는 주소가 정해져 있으므로
미리 알고 있는 주소로 link time에 재배치가 이루어지기 때문에 신경 쓸 필요가 없지만
라이브러리에 있는 함수는 로드되는 위치가 매번 달라지므로 link time에 이를 고정하여 코드에 반영시켜 둘 수가 없습니다.
따라서 별도의 섹션에 .text 영역 내에서 재배치가 필요한 위치를 저장해두고
로더가 이후에 이 영역의 값을 변경하여 재배치를 수행하게 되는 것입니다.
하지만 이는 .text 영역의 수정이 필요하다는 것을 의미하기 때문에
재배치가 이루어진 코드는 다른 위치로 로드된 동일한 코드와 더 이상 공유할 수 없게 됩니다.
이는 공유 라이브러리의 기본 개념과 어긋나는 것이므로 공유 라이브러리에는 PIC를 이용합니다.
PIC는 PC-relative 주소 지정 방식을 이용하기 때문에 로드된 주소에 상관없이 link time에 계산된 오프셋만을 이용하여 원하는 함수/변수를 참조할 수 있게 됩니다.
이렇게 해서 다음 편에서 알아볼 PIE를 위해 이번 편에서는 PIC에 대해 다뤄봤습니다.
다음 편은 리눅스 메모리 보호기법 마지막 편으로, PIE에 대해 알아보도록 하겠습니당.