반응형


 

출처 : http://www.linuxlab.co.kr/docs/98-11-2.htm

 

공유 라이브러리 만들기

    번역 : 이성주/ 고려대학교 컴퓨터학과
    하이텔(linuxlee)

    이 글에 대한 원문은 다음의 웹 페이지에서 보실 수 있습니다.
    http://www.es.linuxfocus.org/English/November1997/article6.html

 

1. 소개 : 프로그램을 제작하는 과정

    근래의 프로그램 제작 과정은 오랜 시간동안 고통받으면서 쌓아올려진 프로그래머들과 디자이너들의 노력의 산물이다.

    이러한 프로그램 제작 과정은 다음과 같은 순서를 따른다.

    우선 텍스트 편집기로 고급 언어를 사용한 소스 코드를 만든다. 매우 큰 프로그램 같은 경우는 하나의 파일에 담기 매우 힘들기 때문에 소스 코드 자체가 기능적 모듈별로 그 기능을 수행하는데 적합한 프로그래밍 언어가 다를 수 있기 때문에 앞에서 나뉘어진 소스 코드들이 다 같은 프로그래밍 언어를 사용해야 하는 것은 아니다.

    소스 코드들을 생성한 후, 각각의 코드들은 기계(컴퓨터)에 의해 실행가능한 코드로 변환되어야 한다. 이렇게 기계에 의해 실행 가능한 코드를 목적 코드(object code)라고 한다. 이 코드는 기계에 의해 실행 가능하다는 것만 빼고는 앞에서의 소스코드와 똑같은 기능을 수행하게 된다. 소스코드를 목적 코드로 변환하는 과정을 일반적으로 컴파일(compile)이라고 부른다. 컴파일은 대개의 경우 앞에서 나눈 각각의 소스 파일들에 대해 따로따로 적용된다. 컴파일된 목적 코드들은 프로그램이나 서브 루틴, 변수 등 다음 단계에서 필요한 대부분의 요소들을 포함하고 있다.

    프로그램을 생성하는데 필요한 목적 코드들이 생성된 후, 링커(linker)라고 불리우는 프로그램에 의해 앞에서 생성된 목적 코드들을 한데 묶고 연결하는 과정이 진행된다. 이 과정에서는 각각의 모듈(목적 코드)들에 포함되어 있는 다른 모듈을 가리키는 메모리 주소들이 결정되게 된다(예를 들어 한 모듈에서 다른 모듈에 들어있는 변수를 가리키게 되는 경우 링크 과정에서 각 모듈들을 합한 후 최종적으로 결정되는 절대 주소를 변수 부분에 넣어 주게 된다.) 이렇게 해서 생성되는 제품이 일반적으로 메모리에 로딩되고 실행되는 실행 파일이다.

    프로그램 실행은 운영 체제에 포함된 특별한 소프트웨어에 의해 이루어지며 예를 들자면 리눅스의 시스템 콜exec()등이 있다. 이 시스템 콜은 실행 파일을 찾은 후, 프로세스에게 메모리를 할당하고, 파일의 특별한 부분(실행코드와 변수들)을 메모리에 적재하고 실행파일의 '텍스트'라는 부분에서 제어권을 CPU에게 넘긴다.

 

2. 프로그램 제작 과정의 간략한 역사

    프로그램 제작 과정은 좀더 나은 실행 성능과 시스템 리소스 사용을 위해 지속적인 개혁을 해왔다.

    초기에 개발자들은 직접적으로 기계어 코드를 이용하여 프로그램을 개발하였다. 이후에 프로그램을 고급 언어로 개발한 후 기계어로 번역하는 것이 자동화됨으로써 프로그램의 생산성은 비약적으로 증가하게 되었다.

    프로그램 컴파일이 가능해진 후, 프로그램 제작 과정은 소스 파일들을 생성하고, 컴파일하고, 최종적으로 실행 파일을 실행하는 일련의 과정을 포함하게 되었다.

    그러나 컴파일 과정에 CPU 시간을 포함한 상당히 많은 자원이 투자되고 컴파일을 통하여 생성된 많은 프로그램들이 계속 재 사용되는 부분을 포함하고 있다는 것을 차츰 깨닫게 되었다. 더욱이 누군가가 소스의 한 부분을 고치게 되면 그 소스 코드를 포함한 모든 소스코드를 다시 컴파일해야만 하는 불편함이 있다는 사실도 깨닫게 되었다.

    위와 같은 이유 때문에 컴파일을 모듈별로 따로 하는 방식이 나오게 되었다. 이 방식은 메인 부분과 자주 재 사용되는 함수 부분을 따로 분리하여 재 사용되는 함수 부분을 특별한 장소에 미리 컴파일하여 놓아 두는 것이다. (후에 이를 라이브러리라고 부르게 된다.)

    이로써 개발자는 재 사용되는 코드 부분을 다시 작성할 필요없이 위에 미리 컴파일된 목적 코드의 도움을 받으면서 프로그램을 제작할 수 있게 되었다. 그럼에도 불구하고, 이러한 과정 또한 프로그래머가 각각의 모듈들을 링크시킬 때 다 알고 있어야 한다는 점 때문에 복잡하기는 마찬가지였다.(또한 이러한 과정은 프로그래머가 알고 있는 함수만 쓸 수 있다는 제약점을 낳게 되었다.)

 

3. 라이브러리란 무엇인가?

    위에서 이야기한 여러 가지 불편함 때문에 사람들은 라이브러리를 만들게 되었다. 라이브러리란 링커가 이해할 수 있는 파일 포맷을 가지며 개발자가 라이브러리를 지정하면 링커가 알아서 프로그램에 필요한 모듈만 링크 시켜 주는 특수한 파일 형태일 뿐이다. 일 라이브러리를 통해 개발자는 거대한 라이브러리에 있는 함수들의 어떠한 종속성에도 신경쓰지 않고 라이브러리 내의 모든 함수를 마음껏 사용할 수 있게 되었다.

    우리가 지금까지 이야기한 라이브러리는 현재에도 별로 변화하지 않고 있다. 다만 라이브러리 파일의 포맷이 조금 바뀌었는데 새로 바뀐 포맷에서는 파일 맨 앞에 그 라이브러리에 대한 정보와 링커가 모든 라이브러리 파일을 풀지 않고도 라이브러리 내부의 함수들을 알 수 있게 해주는 식별자를 가지고 있다. 리눅스에서는 ranlib(1)이라는 명령어가 이러한 기능(라이브러리 파일에 심볼 테이블을 더하는 작업)을 수행해 준다. 위에서 설명한 라이브러리를 정적 라이브러리(Static Library)라고 한다.

    최초로 멀티 태스킹 시스템이 소개된 후 라이브러리에도 '코드의 공유'라는 변화가 몰려오게 된다. 만약 같은 시스템에서 같은 코드를 가진 두 개의 프로그램이 동시에 실행된다면, 두 프로그램의 코드는 공유될 수 있다.(프로그램실행 시, 프로그램이 코드를 전혀 바꾸지 않기 때문에 가능한 것이다.) 이러한 아이디어는 복수개의 중복된 코드를 메모리에 로드할 필요를 없애 주었고 거대한 다중 사용자 시스템에서 많은 양의 메모리를 절약할 수 있게 되었다.

    위의 개혁에서 한 발짝 앞으로 더 나아가, 많은 프로그램들이 같은 라이브러리를 사용한다는 사실을 생각해보자. 비록 각각의 프로그램들이 같은 라이브러리를 사용한다고 해도, 그들이 라이브러리에서 사용하는 코드의 양은 각기 다를 것이다. 더욱이 메인 코드는 서로 마다 다 다를 것이다. 더욱이 메인 코드는 서로 마다 다 다를 것이고 따라서 그들의 텍스트 또한 공유할 수 없을 것이다. 여기서 만약 하나의 라이브러리를 사용하는 각기 다른 프로그램들이 라이브러리 코드를 공유할 수 있다고 가정한다면 상당한 메모리를 절약할 수 있을 것이다. 자 그럼 서로 다른 프로그램 텍스트를 가지며 동일한 라이브러리를 공유하는 서로 다른 프로그램이 있다고 가정해 보자.

    그러나 프로세스의 입장에서 보면 일은 조금 복잡해 진다. 실행 가능한 프로그램은 완전히 링크된 것이 아니라, 라이브러리의 식별자들의 주소 값을 가지는데 그것도 프로그램의 프로세스로의 적재 때문에 지연된다. 링커는 자신이 공유 라이브러리를 취급하고 있다고 인지하고 프로그램에 공유된 코드를 포함시키지 않는다. 커널의 경우 exec() 함수를 통해 프로그램을 실행시킬 때 자신이 공유 라이브러리를 로딩하기 위한 특별한 프로그램을 실행시킨다.(이 프로그램은 그 자신의 텍스트에 대한 공유 메모리를 할당하고 라이브러리의 변수들을 위한 개인 메모리를 할당하는 등의 작업을 한다.) 실행 파일을 로딩할 때 이 프로세스는 실행되고 모든 프로시져는 더더욱 복잡하다.

    물론 링커가 일반적인 라이브러리를 만났을 때에는 원래대로 행동한다.

    공유 라이브러리는 목적 코드를 포함한 파일들의 집합이라기보다는 목적 코드를 포함한 파일 그 자체라고 볼 수 있다. 공유 라이브러리를 링킹하는 동안에 링커는 어떤 모듈이 프로그램에 첨가되고, 어떤 모듈은 빠져야 되는가에 대한 정보를 라이브러리로부터 얻어내지 않는다. 링커는 단지 할당되지 않은 주소 값이 제대로 할당되도록 보장만 하고 라이브러리를 포함함으로써 어떠한 것이 반드시 리스트에 추가되어야 하는가를 검사한다. 모든 공유 라이브러리들의 묶음을 만들 수도 있지만 대개의 경우 공유 라이브러리는 여러 다양한 모듈이 링크된 결과이고 따라서 라이브러리는 추후 실행시간에 필요하기 때문에 라이브러리들을 한 묶음으로 만드는 일은 드물다. 아마도 공유 라이브러리라는 말 보다도 공유 객체라는 말이 더 어울릴 것이다.(그럼에도 불구하고 공유 객체라는 용어를 사용하지 않는다.)

     

4. 라이브러리의 형태

    앞에서 미리 언급했지만, 리눅스에는 정적 라이브러리와 공유 라이브러리, 두  가지의 형태의 라이브러리가 존재한다. 정적 라이브러리들은 ar(1)이라는 유틸리티를 이용해 여러 모듈들을 하나의 파일에 모아놓고 ranlib(1) 이라는 유틸리티를 이용해 각 모듈들을 색인해 놓은 형태를 지니고 있다. 이러한 모듈들은 대개 파일의 이름이 .a로 끝나는 파일에 저장되어 있다. 링커는 파일의 이름이 .a로 끝나는 라이브러리를 만나게 되면 파일의 앞에 수록되어 있는 모듈들의 리스트를 뒤져서 라이브러리를 사용하는 프로그램 코드의 아직 정해지지 않은 주소 값들을 사용하는 모듈들의 주소 값으로 바꾸고 본 프로그램에 사용되는 모듈들을 더하게 된다.

    반대로, 공유 라이브러리의 경우는 하나의 파일이라기 보다는 특수한 코드로 표시된 재 할당가능한 객체라고 볼 수 있다. 앞에서도 언급했듯이 링커 ld(1)는 프로그램 코드에 직접 모듈을 더하지 않고 라이브러리에서 제공하는 식별자를 골라서 프로그램 코드에 삽입하게 된다. 이대 모듈의 코드는 본 프로그램에 삽입되지 않으며 마치 본 프로그램에 이미 삽입되어 있는 척 하게 된다. 링커 ld(1)는 파일의 끝이 .so로 끝나는 것을 보면 공유 라이브러리로 인식한다.

 

5. 리눅스에서의 링크 작업

     모든 프로그램은 실행시키기 위해 연결된 목적 모듈들로 이루어져 있다. 이렇게 모듈들을 연결하는 역할을 수행하는 프로그램을 리눅스 링커 ld(1) 이라고 한다.

    ld(1) 은 실행시 여러 가지 옵션을 제공하는데 본 기사에서는 편의를 위해 라이브러리 사용에 관한 옵션만을 다루도록 하겠다. ld(1) 은 사용자가 직접 실행시킬 수는 없으며 gcc(1) 와 같은 컴파일러가 컴파일의 마지막 단계에서 실행시키게 된다. ld(1)의 실행 방식을 이해함으로써 리눅스에서 라이브러리를 사용하는 방식을 좀 더 쉽게 이해할 수 있을 것이다.

    ld(1)은 그 자체의 적합한 수행 조건으로 프로그램에 링크될 오브젝트파일의 리스트를 필요로 한다. 우리가 앞에서 이야기한  조건(공유 라이브러리는 .so로 끝나고 정적 라이브러리는 .a로 끝난다.)을 따르기만 한다면 ld(1)은 어떠한 순서로라도 리스트에 있는 오브젝트파일들을 읽어들일 수 있다.

    그러나 실제로 '어떠한 순서로라도' 라는 용어가 꼭 맞지는 않는다. ld(1)은 라이브러리를 포함하는 순간에 주소 값을 결정하는데 필요한 모듈만을 포함하기 때문에 뒤에 포함되는 모듈에 의해 결정되어야 할 주소 값은 그대로 결정이 되지 않은 상태로 남게 된다. 따라서 실제로 어떠한 순서로라도 모듈을 읽어 들일 수 있는 것은 아니다.

    반면에 ld(1)은 옵션 -I 과 -L을 이용하여 표준 라이브러리를 포함할 수도 있다.

    그러나 표준 라이브러리라고 해서 다른 것은 별로 없다. 단지 표준 라이브러리의 경우는 미리 정해진 장소에서 모듈을 찾는 다는 것만이 다를 뿐이다.

    라이브러리는 기본적으로 /lib 이나 /usr/lib 디렉토리에 위치한다. -L 옵션은 사용자가 기본 디렉토리 이외에 다른 디렉토리를 기본 디렉토리(기본적으로 라이브러리를 찾는 디렉토리) 목록에 추가시켜 준다. 사용법은 다음과 같다.

    < -L 추가하고자 하는 디렉토리 이름 >

    표준 라이브러리는

    < -I 적재될 라이브러리의 이름 >

    과 같은 방식으로 결정된다. 그러면 ld(1)은 순서대로 해당되는 디렉토리에서 libName.so 파일을 찾는다.(적재될 라이브러리의 이름이 libName 이라고 가정) 만약 찾지 못한다면 그 다음으로 libName.a를 찾게 된다.

    만약 ld(1) 이 libName.so 파일을 찾게 되면, ld(1)은 지정된 라이브러리를 링크 시키게 된다. 그러나 libName.so는 찾지 못하고 libName.a를 찾았을 경우는 여기서 얻은 모듈로 주소 값을 결정하게 된다.

     

6. 공유 라이브러리의 동적 링킹과 로딩

    동적 링킹은 실행 파일을 메모리에 적재하는 순간에 /lib/ld-linux.so 라는 특수한 모듈에 의해 수행된다.(사실 이 자체도 공유 라이브러리이다.)

    실제로 동적 라이브러리와 링크하기 위해서 두 개의 모듈이 있다. /lib/ld.so(예전 a.out 형식의 실행포맷에서 사용한다.) 와 /lib/ld-linux.so(이것은 EFL 실행포맷에서 사용한다.)이다.

    이러한 모듈들은 프로그램이 동적으로 링크될 시기에는 반드시 로딩되어 있어야 한다. 이것들의 이름은 표준적인 것이다. (그래서 이것들을 /lib 디렉토리에서 옮기면 안되고 이것들의 이름도 변경되면 안된다.) 만약 /lib/ld-linux.so 라는 이름을 변경한다면 이 공유 라이브러리는 사용하는 어떠한 프로그램도 자동적으로 실행이 멈추게 된다. 왜냐하면 이 모듈은 실행시에 아직 해석되지 않은 모든 참조 위치를 해석하는 일을 맡고 있기 때문이다.

    /lib/ld.so 모듈은 /etc/ld.so.cache 파일을 사용한다. 이 파일은 해당 라이브러리를 포함하는 대부분의 실행파일을 가리키고 있다. 이것에 대해서는 나중에 다시 말할 것이다.

 

7. soname, 공유 라이브러리의 버전, 호환성

    이제부터 공유 라이브러리에 대한 이해하기 어려운 주제에 대해서 이야기하려고 한다. 버전이 바로 그것이다. 이 기사를 읽은 분들은 종종 'library libX11.so.3 not found' 라는 에러메세지를 발견하곤 했을 것이다. 그러나 현재 자신의 라이브러리 버전을 libX.so.6 인 것을 보고 매우 당혹했을 것이고 이러한 에러 메시지에 대해서 어떻게 처리해야 할지 난감해 하는 경우가 있을 것이다. 어째서 ld.so(8)이 libpepe.so.45.0.1과 libpepe.so.45.22.3 은 서로 교환할 수 있는 버전으로 이해하지만 libpepe.so.46.22.3 은 그렇지 못하다고 인식하는 것일까?

    리눅스에서(물론 다른 모든 ELF 형식을 가지는 운영체제에서도 마찬가지이지만) 라이브러리들은 이것들을 구별하는데 연속된 문자를 사용한다. 이것이 soname 이다.

    soname은 라이브러리 내부에 포함되어 있고 연속된 문자들은 라이브러리들을 구성하는 대상이 링크될 때 결정된다. 공유 라이브러리가 만들어질 때 우리는 ld(1) 에 -soname 옵션을 넘겨주어야 한다. 물론 -soname 다음에는 이 문자열을 값으로 주어야 한다.

    동적 로더는 어떠한 실행파일이 실행될 때 로드 되어지고 인식되어야 하는 공유 라이브러리를 인식하기 위해서 이 문자열을 사용한다. 이 과정은 다음과 같다. 먼저 ld-linux.so 는 프로그램이 어떠한 라이브러리가 필요하고 해당 soname 이 뭔지 결정한다. 그런 다음 해당 이름을 가지고 /etc/ld.so.cache에서 그 라이브러리를 포함하고 있는 파일의 이름을 얻어온다. 그리고 라이브러리 안에 존재하는 이름과 요청된 soname을 비교한다. 만약 서로 동일하다면 그것으로 끝이다. 그러나 만약 그렇지 않다면 발견될 때까지 검색을 하고 그래도 발견되지 않는다면 에러를 낸다.

    만약 라이브러리가 로드되기에 적절한 것이라면 soname을 발견할 수 있을 것이다. 왜냐하면 ld-linux.so는 요청되어진 soname이 요구되어질 파일과 일치한다고 확신하기 때문이다. 만약 일치하지 않는 경우라면 보통 눈에 익은 'library libXXX.so.Y not found'가 나타날 것이다. 찾아야 하는 것이 soname이고 해당 soname을 참조할 때 주어지는 에러메세지인 것이다.

    여기에서 우리는 라이브러리 이름이 변경되거나 지속성에 문제가 발생할 대에는 혼돈을 일으킬 소지를 가지고 있다. 그러나 Linux 사회에는 soname을 할당하는데 관례가 있기 때문에 soname을 접근하거나 soname을 변경하는 것을 좋은 생각이 아니다.

    관례상 라이브러리의 soname은 반드시 적절한 라이브러리와 동일해야 하고 그러한 라이브러리에 대한 인터페이스여야 한다. 만약 라이브러리가 변경되어진다면 그것은 단지 내부적으로 일어나는 변화에 그쳐야 하지 전체적인 인터페이스에 영향을 미쳐서는 안되는 것이다. 즉 그 라이브러리에 대한 함수나 변수 그리고 함수에 대한 인자의 수 등에 영향을 안된다는 것이다. 이럴 경우에 구버전과 신버전 사이에 상호교환적이 되어야 한다. 즉 전체적으로는 변함이 없고 변경된 것은 적은 부분에서 이루어짐으로써 서로간에 교환 즉 호환성이 보장된다는 것이다. 적은 부분의 변화로 minor 숫자의 변경이 이루어지는 경우가 많다. 즉 이것은 soname 자체적으로는 변화가 없다는 것이다. 그리고 해당 라이브러리는 어떠한 중대한 문제없이 교체할 수 있게 되는 것이다.

    그러나 함수를 더하거나 제거하거나 하는 경우 등과 같은 인터페이스가 변경되는 경우에는 상호 교환이 불가능하다는 것을 쉽게 짐작할 수 있을 것이다. 예를 들면 libX11.so.3 과 libX11.so.6 사이를 보면 알 수 있다. X11R5 에서 X11R6 로 업그레이드 되면서 새로운 많은 함수들이 정의되어졌고 인터페이스에도 변경이 가해졌는데 이것들 사이에 상호교환이라는 것은 큰 문제를 유발할 수 있게 될 것이다. X11R6-v3.1.2에서 X11R6-v3.1.3과 같은 변화는 인터페이스에는 변화가 없고 그 라이브러리도 같은 soname을 가지고 있다. 이 경우도 구 버전을 유지하기 위해서는 다른 이름을 주어야 하는 것은 당연하다.(이러한 이유로 라이브러리의 이름에 soname 에 보여지는 주어진 번호가 있고 그 외에 전체적인 버전 숫자가 나타나는 것이다.)

 

8. ldconfig(8)

    /etc/ld.so.cache 라는 파일을 언급한 것이 있는데 이것은 라이브러리가 어떠한 파일에 포함되어져 있는지에 대한 정보를 가지고 있는 것이다. 이것은 표율성을 위해서 바이너리 파일로 만들어지고 ldconfig(8) 유틸리티에 의해서 만들어진다. ldconfig(8)은 /etc/ld.so.conf에 지정된 디렉토리를 찾아다니면서 발견된 각각의 동적 라이브러리들에 대해서 그 라이브러리의 soname 에 의해서 불리어지는 심볼릭 링크들을 만들어낸다. ld.so가 파일의 이름을 얻으려고 할 때 이것은 soname을 발견한 파일의 디렉토리를 선택하는 그러한 일을 한다. 그리고 이렇게 함으로서 우리가 라이브러리를 추가할 때마다 ldconfig(8)를 수행할 필요는 없게 되는 것이다. ldconfig는 리스트에 새로운 디렉토리를 만들어 널때만 실행시키면 된다.

 

9. 동적 라이브러리 만들기

    동적 라이브러리를 만들기 전에 고려할 사항이 있다. 그것은 바로 이렇게 동적 라이브러리로 만들었을 때 유용한가 하는 것이다. 동적 라이브러리는 여러 가지 이유로 시스템에 부하를 주게 된다. 프로그램의 로딩중에는 여러 가지 단계가 있다. 첫째는 메인 프로그램을 로딩하는 것이고 그 외에 프로그램에서 사용하는 각 동적 라이브러리를 로딩한다.

    동적 라이브러리는 재할당이 가능한 코드로 만들어져야 한다. 이것은 당연한 것으로 프로세스를 위해서 가상 주소 공간 안에 할당된 주소가 해당 프로그램이 로드될 때까지 알 수 없기 때문이다. 컴파일러는 라이브러리의 로딩위치를 유지하기 위해서 레지스터를 예약해 놓아야 한다. 그리고 결과적으로 코드 최적화에서 이것 때문에 레지스터 하나를 사용할 수 없게 된다. 이러한 경우는 사소하다고 볼 수 있다. 왜냐하면 이러한 것 때문에 나타나는 시스템의 부하는 대부분의 경우 다른 부하들의 5%이하이기 때문이다.

    동적 라이브러리가 적절한 것이 되려면 다른 여러 프로그램에서 자주 사용되어야 한다. 즉 이렇게 하면 시작한 프로세스가 없어진 이후에도 그 라이브러리에 대한 TEXT가 다시 로딩되는 것을 방지할 수 있다. 다시 로딩이 안되는 이유는 해당 라이브러리는 다른 프로그램에서 사용하고 있기 때문에 메모리에서 없애 버리면 안되기 때문에 항상 메모리가 남아있어서 다시 불러들일 필요가 없어진다.

    동적 라이브러리의 좋은 예는 C 표준 라이브러리이다. (이 라이브러리들은 대부분의 C 프로그램을 작성하는데 사용 되어진다.)평균적으로 모든 함수들이 자주 사용되어진다.

    정적 라이브러리의 경우에 좀처럼 드물게 사용하는 함수를 포함할 때 유리하다. 즉 해당 함수는 이것을 포함하고 있는 모듈에 있기 때문에 내가 필요하지 않으면 링크를 시키지 않으면 된다.

 

    9.1  소스코드의 컴파일

    소스의 컴파일은 한 가지만 제외하고는 대부분의 소스를 컴파일하는 경우와 같은 형식으로 수행된다. 차이점이라면 프로세스의 가상 주소 공간 안의 다른 지역에 로드될 수 있도록 하는 코드를 만들기 위해서 '-f PIC' -PIC(Position Independent Code)- 라는 옵션을 사용해야 한다는 것이다.

    동적 라이브러리에서 이 단계는 반드시 필요한 것이다. 왜냐하면  정적으로 링크된 프로그램에서는 라이브러리 오브젝트의 위치가 링크시에 결정되고 그래서 고정된 시간이 되어 버리기 때문이다. 예전의 a.out 실행형식에서는 이러한 단계가 불가능했다. 왜냐하면 각 공유 라이브러리가 가상주소의 공간 안에 고정된 위치를 가지게 만들어졌기 때문이다. 결과적으로 가상메모리의 오버래핑된 지역에 로드된 두 라이브러리는 사용하길 원하는 프로그램에서는 어떠한 경우에는 충돌이 발생하게 되었다. 이것은 라이브러리들의 리스트를 유지해야만 하는 결과를 만들었다. 즉 동적 라이브러리로 만들기를 원한다면 해당 라이브러리가 어디에서 어디까지 사용한다는 것을 선언하는 부분이 반드시 필요하게 되어진 것이고 이 영역을 다른 사람들이 사용하면 안되게 된 것이다.

    그렇다면 우리가 이전에 언급한 것에 따르면 공식적인 리스트에 동적 라이브러리를 등록하는 것이 필요없게 된 것이다. 왜냐하면 라이브러리는 로딩이 되어질 때 로드될 위치를 결정하게 되기 때문이다. 이것은 코드를 위치조정이 가능하게 만들었기 때문에 가능한 일이다.

     

    9.2  라이브러리의 오브젝트를 링크하기

    모든 객체를 컴파일 한 후에 동적인 로딩이 가능한 오브젝트파일로 만들기 위한 특별한 옵션이 필요하다.

    gcc -shared -o libName.so.xxx.yyy.zzz -Wl, -soname, libName.so.xxx

    짐작할 수 있듯이 공유 라이브러리를 만들어내는 옵션이 보이는 것을 제외하고는 보통의 링크과정처럼 보인다.
    하나하나 확인해보자.

    -shared

    이것은 공유 라이브러리를 만들어라 하는 옵션이다. 그래서 라이브러리에 관련된 출력파일에 실행 가능한 형태가 되는 것이다.

    -o libName.so.xxx.yyy.zzz

    최종적인 출력파일이름이다. 이름을 짓는데는 보통의 관례를 따를 필요는 없지만 이것은 표준적으로 개발에 사용되기를 원한다면 관례를 따르는 것이 좋다.

    Wl, -soname, libName.so.xxx

    이 -W1옵션은 gcc(1)가 콤마로 부분된 다음 옵션들을 링커에게 알려주기 위한 것이다. 이것은 gcc(1)가 ld(1)에게 옵션을 넘겨주기 위한 방법이다. 이 예에서는 링커에게 다음과 같은 옵션을 넘겨준 것이다.

    -soname libName.so.xxx

    이 옵션은 라이브러리에 대한 soname을 고정시키는 것이다. 그리고 필요한 다른 프로그램에서는 이 soname을 사용해야만 한다.

     

    9.3 라이브러리를 설치하기

    이미 관련된 실행파일을 가지고 있게 되었다. 이제는 이것을 적절한 장소에 설치하고 사용하기만 하면 된다. 새로운 라이브러리를 원하는 프로그램을 컴파일하기 위해서 다음과 같이 하면 된다.

    gcc -o program libName.so.xxx.yyy.zzz

    또는 만약 라이브러리가 /usr/lib에 설치되어 있다면 단순히 다음과 같이 하는 것만으로 충분하다.

    gcc -o program -lName

    (만약 /usr/local/lib에 있다면 '-L/usr/local/lib'라는 것을 추가 시켜주면 된다.)

    라이브러리를 설치하기 위해서는 다음과 같이 한다.

    라이브러리를 /lib 난 또는 /usr/lib 에 복사한다. 만약 다른 장소에 복사하기로 했다면(예를 들면 /usr/local/lib) 링커인 ld(1)가 자동적으로 링크할 수는 없을 것이다. libName.so.xxx.yyy.zzz로부터 libName.so.xxx 라는 심볼릭링크를 만들기 위해서 ldconfig(1)을 수행한다. 이 단계에서 만약 이전단계가 확실히 수행되어졌다면 이 라이브러리는 동적 라이브러리로 인식이 될 것이다. 링크된 프로그램들은 이 단계에서는 영향을 받지 않는다. 이것은 실행시에 라이브러리가 로딩될 때 영향을 받는 것이다.

    libName.so.xxx.yyy.zzz(또는 soname 인 libName.so.xxx)로부터 libName.so 라는 심볼릭 링크를 만들어야 한다. 이렇게 해야만 하는 이유는 링커가 -1옵션으로 라이브러리를 발견할 수 있게 하기 위해서이다. libName.so 라는 형식에 맞추어진 라이브러리의 이름이 필요한 경우를 위해서 고안된 작동방법이다.

     

10. 정적 라이브러리 만들기

    만약에 위와는 반대로 정적 라이브러리를 만드려고 한다면(또는 정적으로 링크된 것과 동적으로 링크된 것 둘 다를 원한다면) 다음과 같은 과정을 따른다.
    주의 : 라이브러리를 발견하는데 있어서 링커는 libName.so 라는 파일을 먼저 찾고 그 다음에 libName.a를 찾는다. 만약 두 개의 라이브러리(정적인 버전과 동적인 버전)를 같은 이름으로 부른다면 일반적으로 각각의 경우를 링크(동적인 것이 항상 먼저 링크되기 때문에)하는 것을 결정하는 것은 불가능하게 된다.

    이러한 이유로 항상 만약에 두 개의 라이브러리가 필요한 버전이 있다면 정적인 것은 libName_s.a 라고 이름을 붙여주고 동적인 것은 그냥 libName.so 라고 이름 붙여줄 것을 권하고 싶다. 링크할 때에는 그래서 다음과 같이 하면 될 것이다.

    gcc -o program -lName_s

    이 경우는 정적인 경우이고 동적인 것을 만들 때면 다음과 같이 한다.

    gcc -0 program -lName

     

    10.1  소스를 컴파일하기

    소스를 컴파일하기 위해서는 특별한 방법은 없다. 위에서 말한 대로 링크단계에서 결정해야 한다. 필요하다면 -f PIC 옵션을 사용하는 것이다.

     

    10.2  라이브러리를 객체에 링크하기

    정적 라이브러리의 경우에는 링크 단계가 필요없다. 모든 객체는 ar(1) 명령어의 해서 라이브러리 파일들을 만들 수 있다. 그런 다음 심볼들을 빠르게 해석해내기 위해서 라이브러리에 대한 ranlib(1) 명령을 수행하는 것이 좋다. 비록 반드시 필요한 것은 아니지만 이 명령을 실행하지 않으면 실행파일에 대한 모듈에 대한 링크가 지워질지도 모른다.

     

    10.3  라이브러리의 설치

    정적 라이브러리는 만약 오로지 정적 라이브러리 형태로만 사용하려고 한다면 libName.a 형태로 되어있는 이름을 가지고 있을 것이다. 그러나 두 가지 형태의 라이브러리를 모두 가지고 있기를 바란다면 libName_s.a 라고 이름을 지어줄 것을 권하고 싶다. 이것은 정적이나 또는 동적으로 로딩하는 것을 쉽게 제어하기 위해서 이다.

    링크단계에서 -static 옵션을 줄 수 있다. 이 옵션은 /lib/ld-linux.so 모듈의 로딩을 제어하는 것이다. 그러나 이것은 라이브러리의 검색 순서에 영향을 미치지는 않는다. 그래서 만약에 -static 으로 지정하고 ld(1)이 동적 라이브러리를 발견한다면 ld(1)는 발견된 동적 라이브러리를 가지고 작업을 할 것이다. 즉 -static 이라고 지정했다고 해서 계속적으로 정적으로 되어 있는 것을 찾지는 않는다는 것이다. 이것은 실행시에 라이브러리 루틴을 호출할 때 에러를 발생시킬 수 있다. 왜냐하면 동적 라이브러리는 실제 자신의 프로그램에는 해당 라이브러리가 있지 않기 때문이다. -자동적으로 동적인 로딩을 위한 모듈이 링크되어지지 않았고 그래서 이 단계가 수행될 수 없으므로 에러를 내는 것이다. 그래서 사용할 때 -static이 주어지면 링커가 정확한 라이브러리를 찾도록 해주어야 한다.

     

11.정적 링크하기와 동적 링크하기

    잘 알려진 라이브러리를 사용한 프로그램을 배포하는 경우를 생각해보자.(아마 모티프라이브러리를 사용한 것이 적절한 예가 될 것이다.)

    이런 종류의 소프트웨어를 만들기 위해서는 세가지 선택 사항이 있다.
    첫 째는 실행파일을 정적으로 링크시켜 만드는 것이다.(오직 .a 라이브러리만을 사용한다.) 이러한 종류의 프로그램은 오직 한번만 로드되어지고 시스템 안에 있는 어떠한 라이브러리도 가지고 있을 필요가 없다.(심지어 /lib/ld-linux.so 까지도) 그러나 이것은 바이너리 파일 안에 해당 소프트웨어에 필요한 모든 것들을 가지고 다녀야 하는 단점이 있다. 그리고 대부분 프로그램의 크기가 매우 크다.
    두 번째는 동적으로 만드는 것이다. 이것은 즉 우리의 소프트웨어가 실행되기 위해서는 연관된 모든 동적 라이브러리들이 모두 제공되어져야 한다는 것을 의미한다. 실행파일은 매우 작지만 모든 라이브러리를 모두 가지고 다니는 것은 쉽지 않은 일일 것이다.(예를 들면 Motif를 가지고 있지 않은 사람도 있다.)
    세 번째 선택 사항이 있다. 이것은 두 가지 형태를 서로 합치는 것이다. 즉 어떠한 라이브러리는 동적으로 또 어떤 라이브러리는 정적으로 해서 두 개의 장점을 모두 취하는 것이다. 이러한 형태의 소프트웨어가 배포에는 가장 편한 방법이 된다.

    예를 들면, 세가지 다른 라이브러리 포함 형태는 다음과 같다.

    gcc -static -o program.static program.o -lm_s -lXm_s -lXt_s -lX11_s -lXmu_s -lXpm_s

    gcc -o program.dynamic program.o -lm -lXm -lXt -lX11 -lXmu -lXpm

    gcc -o program.mixed program.o -lm -lXm_s -lXt -lX11 -lXmu -lXpm

    세 번째 경우에 Motif 라이브러리인 Motif(-lXm_s)는 정적인 결과를 얻고 그리고 다른 것들은 동적인 결과를 얻게 된다. 프로그램이 실행되기 위해서는 적당한 버전을 위한 라이브러리들이 필요하게 된다. 여기에서는 libm.so.xx, libXt.so.xx, libX11.so.xx, libXmu.so.xx, libXpm.so.xx 등이 필요에 따라서 사용될 것이다.



 

컴파일 참고

 --------------------------------------------------------------------------------

gcc -fPIC -m64 -c 파일명.c

gcc -fPIC -m64 -I{INCLUDE_PATH} -c 파일명.c

ex) gcc -fPIC -m64 -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -c 파일명.c

 

gcc -shared -o 출력라이브러리명.so -WI,soname,라이브러리버전명.so.xx.xx.xx 목적파일1.o 목적파일2.o ....

 

Makefile 생성시

--------------------------------------------------------------------------------

INCLUDEPATH = -I$(JAVA_HOME)/include \

-I$(JAVA_HOME)/include/linux

 

BITS = -m64

CC = gcc

 

TARGET = 출력라이브러리명.so

OBJECTS = 목적파일1.o \

목적파일2.o ...

 

$(TARGET) : $(OBJECTS)

$(CC) $(BITS) -g -shared -o $@ -WI,soname,$@ @^

 

.c.o :

$(CC) $(BITS) -g -fPIC $(INCLUDEPATH) -c $<

 

clean :

rm *.so *.o

--------------------------------------------------------------------------------

 

 


반응형

+ Recent posts