2011. 7. 5. 03:55

♧ 통상 GNU/리눅스의 공유 라이브러리를 만들 때는

각각의 .C 파일을 PIC(Position Independent Code)가 되도록 컴파일 한다.

그러나 실은 PIC로 컴파일 하지 안아도 공유 라이브러리는 만들 수 있다.

그러면 굳이 PIC로 컴파일 하는 이유가 있는 것일까?

   

▶ fpic.c 작성

#include <stdio.h>

void func() {

printf("");

printf("");

printf("");

}

   

PIC로 컴파일하기 위해 gcc -fpic 또는 fPIC 옵션을 지정한다.

-fpic :: 좀더 고속으로 코드를 생성할 가능성이 있지만,

CPU에 따라 -fpic로 생성할 수 있는 GOT(Glocal Offset Table)의 크기에 제한이 있다.

-fPIC :: CPU에 관계없이 사용할 수 있다. 여기서는 -fPIC를 사용한다.

(※ x86에서는 -fpic와 -fPIC가 동일)

   

% gcc -o fpic-no-pic.s -S fpic.c

% gcc -fPIC -o fpic-pic.s -S fpic.c

위와 같이 생성된 어셈블리어의 소스코드를 보면 PIC 버전은 printf를

PLT(Procedure Linkage Table)를 경유해서 호출하는 것을 알 수 있다.

(※ 우분투 7.04에서 테스트)

   

다음에는 공유 라이브러리를 만든다.

% gcc -shared -o fpic-no-pic.so fpic.c

% gcc -shared -fPIC -o fpic-pic.so fpic.c

   

▷ 위 공유 라이브러리의 동적 섹션(dynamic section)을 readelf 명령으로 보면,

비 PIC공유 라이브러리에서는 TEXTREL 이라는 엔트리가 있고 (텍스트 내의 재배치 필요),

RELCOUNT(재배치 수)가 5로, PIC 공유 라이브러리보다 3만큼 크다.

그 이유는 printf()3회 호출하기 때문이다.

   

PIC 공유 라이브러리에서의 RELCOUNT0이 아닌 이유는 gcc가 기본적으로 사용하는

시작 파일에 포함된 코드 때문이다. gcc에 -nostartfiles 옵션을 지정하면 이 값은 0이 된다.

   

[ PIC와 비 PIC 공유 라이브러리의 성능 비교 ]

♧ 위 예에서는 비 PIC 버전은 실행 시에(동적 링크 시에) 5개의 주소가

재배치 되어야 한다고 했다. 그러면 재배치 수가 매우 커지면 어떻게 될 것인가?

   

공유 라이브버리를 PIC버전과 비 PIC버전printf()를 천만 번 호출 ( 셸 스크립트로 실행 비교)

▶ 다음 셸 스크립트를 실행하면 printf()를 천만 번 호출하는 공유 라이브러리를

비 PIC버전과 PIC 버전으로 만들고, 각각을 링크한 실행 파일 fpic-no-pic와 fpic-pic를 생성.

#! /bin/sh

rm -f *.o *.so

num=1000

for i in `seq $num`; do

echo "void func$i() { " > fpic$i.c

ruby -e "10000.times { puts 'printf(\"\");' }" >> fpic$i.c

echo "} " >> fpic$i.c

gcc -o fpic-no-pic$i.o -c fpic$i.c

gcc -fPIC -o fpic-pic$i.0 -c fpic$i.c

done

gcc -o fpic-no-pic.so -shared fpic-no-pic*.o

gcc -o fpic-pic.so -shared fpic-pic*.o

echo "int main() { return 0; }" > fpic-main.c

gcc -o fpic-no-pic fpic-main.c ./fpic-no-pic.so

gcc -o fpic-pic fpic-main.c ./fpic-pic.so

   

비 PIC 버전이 첫 회 2.15초, 두 번째 이후에는 약 0.55초가 걸리고,

% repeat 3 time ./fpic-no-pic

2.15s total : 0.29s user 0.48s system 35% cpu

0.56s total : 0.25s user 0.31s system 99% cpu

0.55s total : 0.30s user 0.25s system 99% cpu

   

PIC 버전은 최초 0.02초, 두 번째 이후에는 0.00초가 되었다.

% repeat 3 time ./fpic-no-pic

2.15s total : 0.29s user 0.48s system 35% cpu

0.56s total : 0.25s user 0.31s system 99% cpu

0.55s total : 0.30s user 0.25s system 99% cpu

   

☞ main()의 내용은 없으므로 비 PIC 버전은 동적 링크할 때

재배치에 2.15 ~ 0.55초를 필요로 함을 알 수 있다.

실행 환경은 Xeon 2.8GHz + Debian GNU/리눅스 sarge + GCC 3.3.5이다.

   

▷ 비 PIC 버전의 단점은 실행할 때 재배치에 시간이 걸린다는 점만이 아니다.

재배치가 필요한 부분의 코드를 재작성 하기 위해

『텍스트 섹션 내의 재배치가 필요한 페이지를 로드 → 재작성

→ copy on write 발생 → 다른 프로세스와 텍스트를 공유할 수 없음』

이라는 사태가 발생. 즉, 여기서는 텍스트(프로그램 코드)를 다른 프로세스와

공유할 수 있는 『공유』라이브러리의 주요한 장점이 사라지고 만다.

   

한편, 비 PIC 버전의 fpic-no-pic.so 와 PIC 버전의 fpic-pic.so 파일 크기를 비교하면,

전자는 268MB, 후자는 134MB로 크게 차이가 난다. readelf -S로 섹션 데이터를 보면

다음과 같은 차이가 있다.

   

컴파일러

.rel.dyn

.text

비 PIC

152MB

114MB

PIC

0MB

133MB

비 PIC 버전은 코드(.text) 크기는 PIC 버전보다 작지만, 재배치에 필요한 정보(.rel.dyn)가

상당한 용량을 차지.

   

[ 정리 ]

여기서는 공유 라이브러리를 작성할 때 PIC로 컴파일 해야 하는 필요성에 대해 알아보았다.

비 PIC 공유 라이브러리를 작성할 수는 있지만 실행할 때 재배치에 시간이 소요되고 다른

프로세스와 코드(.text)를 공유할 수 없는 커다란 단점이 있다. 따라서 공유 라이브러리를

작성할 때는 .c 파일을 PIC로 컴파일 하도록 한다.

 

Posted by devanix