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
2011. 7. 2. 16:47

♧ 공유 라이브러리 의존관계란,

실행 파일 또는 공유 라이브러리에서 필요로 하는 공유 라이브러리의 SONAME이

ELF 정보 내의 동적 섹션에 있는 NEEDED에 기록된 정보로 관리되고 있음을 의미한다.

▷ 개별 파일 내의 NEEDED는 objdump 또는 readelf를 사용해서 확인할 수 있지만,

의존관계를 이루고 있는 모든 공유 라이브러리를 출력하려면 ldd명령을 이용한다.

▷ ldd 명령은 내부적으로 환경변수 LD_TRACE_LOADED_OBJECTS를 이용해 구현되어 있다.

   

   

[ objdump 와 readelf로 공유 라이브러리 의존 관계 확인 ]

♧ 공유 라이브러리를 이용할 때, 실행 파일 또는 공유 라이브러리 자체는

이를 실행 하려 할 때 필요한 공유 라이브러리에 대한 정보를 갖고 있다.

▷ 그 정보는 ELF 동적 섹션의 NEEDED에 기록.

   

▶ [/bin/ls]를 objdump/readelf 명령을 사용하여 확인

objdump -p ::

   

readelf -d ::

▷ 이와 같이 /bin/ls는 [librt.so.1, libselinux.so.1, libacl.so.1, libc.so.6]

4개의 공유 라이브러리를 필요로 함.

▷ 그러나 /bin/ls 실행할 때 이 4개의 공유 라이브러리만 필요한 것은 아니다.

(4개의 공유 라이브러리는 각각의 또 다른 공유 라이브러리를 필요로 하기 때문.)

   

NEEDED로 표시되 어 있는 것은 SONAME이므로, SONAME으로부터 실제 파일을 찾아야 한다.

▷ 특히 설정되어 있지 않은 경우에는

/usr/lib(또는 /lib)에 SONAME에 해당하는 파일이 공유 라이브러리다.

(실제로는 /lib/tls 또는 /lib/tls/i686/cmov 등에 있는 공유 라이브러리가 사용 되기도 함)

▷ 환경변수 LD_LIBRARY_PATH에 라이브러리 경로를 설정한 경우 해당 디렉토리를 참조.

/etc/ld.so.cache에 기록 정보를 참조.

(/etc/ld.so.conf의 설정을 이용해 ldconfig를 실행할 때 갱신)

▷ 예를 들어 [ librt.so.1 ]의 경우, /lib/librt.so.1이라는 파일(심볼릭 링크)이 존재하므로,

이것이 SONAME librt.so.1에 해당하는 공유 라이브러리 파일이다.

   

▶ [ librt.so.1 ]이 필요로 하는 라이브러리 확인.

...(중략)...

   

▶ [ /bin/ls ]의 다른 공유 라이브러리도 의존 관계를 확인.

(※ 공유 라이브러리 의존관계는 CPU종류, OS 배포판에 따라 조금씩 다르다.)

   

   

[ ldd로 공유 라이브러리 의존 관계 확인 ]

♧ 앞에서 살펴본 바와 같이 objdump / readelf를 사용해 공유 라이브러리 의존 관계를 확인할 수는

있지만, 각각의 공유 라이브러리에 대한 의존관계를 개별적으로 확인해야 하므로 다소 번거롭다.

또한 실제 실행될 때 사용되는 공유 라이브러리의 디렉토리 경로를 정확히 얻기도 어렵다.

 

▶ ldd로 공유 라이브러리 의존 관계 확인.

(※ ldd는 실행 파일 외에도 공유 라이브러리에도 사용 가능)

- 이와 같이 실행파일이 필요로 하는 공유 라이브러리의 SONAME, 경로명과 할당된 메모리 주소를 확인.

 

▷ 사실 GNU/리눅스에서 ldd 명령은 단순히

환경변수(LD_TRACE_LOADED_OBJECTS)를 이용한 셸 스크립트이다.

▷ LD_TRACE_LOADED_OBJECTS에 1을 설정해서 프로그램을 실행하면 프로그램 실행 시점에

ELF 인터프리터(런타임 로더 /lib/ld_linux.so.2)가 필요한 공유 라이브러리를 찾아

메모리에 로딩해서 그 정보를 표시한 후, 실제 프로그램이 실행되기 전에 종료하게 된다.

 

▶ LD_TRACE_LOADED_OBJECTS를 이용한 공유 라이브러리 의존성 확인.

- 이와 같이 ldd와 동일한 결과를 얻을 수 있다.

   

▷ LD_TRACE_LOADED_OBJECTS는 실행 파일이 아닌 공유 라이브러리에의 경우 실행할 수 없다.

   

▷ 이런 경우에는 런타임 로드 /lib/ld-linux.so.2를 실행한다.

  

Posted by devanix
2011. 7. 2. 13:24

[ 정적 라이브러리 (static library) ]

♧ 정적 라이브러리는 여러 프로그램에서 사용되는 함수를 포함하는

오브젝트 파일을 하나의 파일로 다룰 수 있도록 정리해 놓은 것.

   

▷ 프로그램을 작성할 때는 소스 파일을 분류해서 일정한 분량으로 나누어

각각의 오브젝트 파일로 컴파일 한 후, 이를 최종적으로 링크해서 하나의 실행 가능한 파일을 생성한다.

이때 다른 프로그램에서도 사용될 만한 모듈이 여러 개의 오브젝트 파일로 나뉘어 있으면

이것들을 한 덩어리로 다루기가 번거로워진다. 그래서 생각해낸 것이 아카이브 파일이다.

   

▷ archive(아카이브) 파일(.a) - 여러 개의 오브젝트 파일을 하나의 파일로 모아놓은 것.

▷ ar(1) 명령을 사용해 여러 개의 오브젝트 파일을 하나의 아카이브 파일로 합칠 수 있다.

▷ OS에 따라 ranlib(1)를 사용하면 이 아카이브 파일 내의 오브젝트가 제공하는 심볼 정보를 해시화해서,

심볼을 제공하는 오브젝트 파일을 효율적으로 검색할 수 있게 된다. (ar -s와 같음)

이와 같은 아카이브 파일을 정적 라이브러리라 한다.

   

▶ 정적라이브러리 생성

ⓐ 오브젝트 파일 생성.

ⓑ 아카이브 파일로 묶어 줌 / 확인.

ⓒ 실행 가능한 파일 생성

▷ 정적 라이브러리를 링크할 경우,

링커는 다른 오브젝트 파일에서 정의되지 않은 심볼을 찾아 지정된 정적 라이브러리에서

해당 심볼을 정의하고 있는 오브젝트 파일의 사본을 추출해서 실행 파일 내에 포함.

▷ 이 경우에는 baz.o 내에 정의되지 않은 심볼이

libfoo.a에 포함되어 있는 오브젝트 파일들 중에 정의되어 있다면,

그 오브젝트 파일을 찾아 실행 파일 baz에 복사해서 링크 한다.

   

▷ 여기서 핵심은 라이브러리 내의 오브젝트 파일 단위로 처리된다는 점,

링크 시에는 실행 파일 내에 오브젝트 파일의 사본이 포함된다는 점.

▷ 정적 라이브러리를 링크해서 생성된 실행 바이너리를 실행할 경우에는

정적 라이브러리가 없어도 관계 없다. 필요한 코드는 실행 바이너리에 복사되어 포함되기 때문.

   

[ 공유 라이브러리 (shared library) ]

♣ 정적 라이브러리의 경우 여러 오브젝트 파일의 아카이브였다면, 공유 라이브러리는

여러 오브젝트 파일을 하나의 거대한 오브젝트 파일로 만들어 이를 공유할 수 있도록 한 것.

- OS의 가상 메모리 관리 시스템이 진보함에 따라, 하나의 파일을 mmap(2)등을 이용해

여러 프로세스에서 메모리를 공유해서 참조할 수 있게 되었다.

이를 활용할 수 있도록 한 것이 공유 라이브러리(or 공유 오브젝트)이다.

(최근의 OS에서는 일단 메모리맵을 설정해 두는 것만으로 실제로 그 메모리 내용이 참조되기까지

디스크 액세스를 지연시킬 수 있으므로, 거대한 오브젝트 파일이라도 그다지 문제가 되지 않는다.)

   

▶ 공유 라이브러리 생성

-shared옵션 : 공유 오브젝트를 만들게 된다.

-Wl,-soname옵션 : 공유 오브젝트에 특정 SONAME을 지정.

(SONAME에 따라 실행 시에 링크할 공유 라이브러리가 결정)

   

▷ 공유 라이브러리는 정적 라이브러리와 같은 방법으로 링크 할 수 있다.

- 그러나 실제 내부적인 처리는 많이 다르다.

- 이 경우 baz.o 내에 정의되지 않은 심볼이 공유 오브젝트에 정의되어 있다면,

해당 공유 오브젝트의 SONAME을 실행 파일의 NEEDED에 설정할 뿐,

공유 오브젝트에 포함되어 있는 코드 자체를 복사하지는 않는다.

(정적 라이브러리와 달리 *.so 내에 어떤 오브젝트 파일이 있는지는 기본적으로 남지 않는다)

- 여기서 핵심은 공유 라이브러리 단위로 처리된다는 점,

링크 시에는 필요한 공유 라이브러리의 SONAME만을 실행 파일에 NEEDED로 등록해 둔다는 점이다.

- 공유 라이브러리를 링크한 실행 파일을 실행할 경우에는 동적 링커 로더가

NEEDED의 정보를 이용해 필요한 공유 라이브러리를 찾아내어,

실행 시에 해당 프로세스의 메모리맵을 조작해서 공유 라이브러리를

링크한 실행 파일을 실행할 경우에는 공유 라이이브러리가 시스템에 반드시 존재해야 한다.

실제 라이브러리 코드는 실행 파일에 포함되어 있지 않고 공유 라이브러리에만 존재.

(※ /etc/ld.so.conf 수정, ldconfig)

   

   

[ 라이브러리 차이점]

♣ 메모리 크기

▷ 실행 시에 필요한 메모리 크기도, 최근의 OS에서는 공유 라이브러리 쪽이 유리.

▷ 특히 PIC(Position Independent Code)로 생성해 두면 코드 부분이 어느 주소에 위치하더라도

변경할 필요가 없기 때문에, 공유 라이브러리를 하나의 물리적인 메모리 페이지에 읽어 들이는 것만으로

각각의 메모리 공간에 있는 프로세스에서 공유 라이브러리의 메모리 페이지를 공유 할 수 있게 된다.

 

♣ 파일 크기

정적 라이브러리:

라이브러리에 포함된 코드를 여러 실행 파일에서 이용하게 되면

각 오브젝트가 매번 복사되므로 용량이 늘어나게 됨.

공유 라이브러리:

라이브러리 코드 자체는 실행 파일에 복사되지 않고 공유 라이브러리 파일에서만

포함하고 있으므로, 라이브러리 코드를 이용하는 실행 파일이 많더라도

라이브러리 코드 크기만큼의 용량이 증가하지 않음.

    

♣ 라이브러리 패치 (문제가 되는 코드 발견시)

정적 라이브러리:

컴파일된 실행 바이너리에 복사된 정적 라이브러리의 오브젝트가 남아 있으므로,

해당 라이브러리를 사용하는 모든 실행 바이너리를 재컴파일 해야 함.

공유 라이브러리:

공유 라이브러리만 수정하면 됨.

데몬과 같이 장시간 실행하고 있는 프로그램의 경우, 메모리에 로드 되어 있으므로

새로운 공유 라이브러리를 참조하도록 재실행 해야 함.

 

  

Posted by devanix