다음 이전 차례

6. 링크

호환되지 않는 두 개의 바이너리 형식, 정적 라이브러리와 동적 라이브러리의 구분, 컴파일 과정 후에 일어나는 작업과 이미 컴파일을 마친 실행 프로그램이 실행될 때 일어나는 작업 둘 다에 대하여 "링크"라는 같은 말을 사용하여 생기는 혼란함(사실은 로드(load)한다라는 말에 대한 과부하라고 말할 수도 있다), 이런 모든 것에 대하여 다루므로 이번 섹션은 좀 복잡할 것이다. 말만 어려울 뿐이므로 크게 걱정할 필요는 없다.

이러한 혼란을 완화하기 위해서, 우리는 실행시(runtime)에 일어나는 일에 대하여 동적 로딩(Dynamic Loading)이라는 단어를 사용하겠다. 그리고 다음 섹션에 가서 다루고자 한다. 또는 동적 링킹(Dynamic Linking)이라는 단어로 표현되기도 한다. 이번 섹션에서는 오로지 컴파일 과정 바로 직후에 생기는 링크라는 작업에 대해서만 다루기로 한다.

6.1 정적 라이브러리 vs 공유 라이브러리

프로그램을 만드는 마지막 작업이 바로 링크(Link)라는 과정이다. 필요한 조각들을 모두 모으거나 어떤 부분이 빠져 있는지 알아보기 위한 과정이다. 분명히 프로그램들은 해야할 일이 많다. 이 모든 것을 일일이 다 짜주는 것은 아니다. 예를 들어 화일을 연다든지 하는 일인데 그러한 일들은 이미 여러분에게 라이브러리라는 형태로 주어져 있다. 평범한 리눅스 시스템에서는 /lib/usr/lib/에서 그러한 라이브러리들을 찾을 수 있다.

정적 라이브러리(Static Library)를 사용할 때, 링커는 프로그램이 필요로 하는 부분을 라이브러리에서 찾아서 그냥 실행화일에다 카피해버린다. 공유 라이브러리(또는 동적 라이브러리)의 경우에는 이렇게 하는 것이 아니라 실행화일에다가 단지 "실행될 때 우선 이 라이브러리를 로딩시킬 것"이라는 메세지만을 남겨놓는다. 당연히 공유 라이브러리를 사용하면 실행화일의 크기가 작아진다. 그들은 메모리도 또한 적게 차지하며, 하드 디스크의 용량도 적게 차지한다. 리눅스의 기본 행동은 일단 공유 라이브러리가 있으면 그것과 링크를 시키고, 그렇지 않으면 정적 라이브러리를 가지고 링크 작업을 한다. 공유 라이브러리를 쓴 실행화일을 얻고자 했는데, 우연찮게 정적 실행화일이 만들어졌다면 우선 공유 라이브러리가 제대로 있는지(a.out은 *.sa, ELF는 *.so)살펴보고 읽기 퍼미션이 주어져 있는지 알아본다.

리눅스에서 정적 라이브러리는 libname.a 과 같은 식의 이름을 갖는다. 그에 비해 공유 라이브러리는 libname.so.x.y.z 라는 식의 이름을 갖는데 x.y.z는 버전을 뜻한다. 또한 공유 라이브러리는 종종 링크되어 있다. (아주 중요) libname.so.x 그리고 libname.so라는 식의 링크를 갖는다. 표준 라이브러리들은 이 둘을 모두 가지고 있다.

여러분은 ldd라는 것을 사용함으로써 특정 프로그램이 어떤 공유 라이브러리를 원하는지 알 수 있다. (ldd = List Dynamic Dependencies)

$ ldd /usr/bin/lynx
        libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
        libc.so.5 => /lib/libc.so.5.2.18

위 결과는 본인의 시스템에서 텍스트용 웹 브라우져로 사용하고 있는 lynx 라는 프로그램에 대하여 의존성 체크를 해본 결과이다. libc.so.5 (C 라이브러리)와 libncurses.so.1 (터미널 제어에 사용되는 라이브러리)를 필요로 하고 있다고 출력하고 있다. 아무런 공유 라이브러리도 필요없으면 그냥 `statically linked' 또는 `statically linked (ELF)' 라고만 출력한다.

6.2 라이브러리 들여다보기 (도대체 sin()은 어디에 들어있는가?)

nm libraryname 이라고 실행시키면 라이브러리 내의 모든 심볼을 출력해준다. 이는 공유 라이브러리와 정적 라이브러리 둘 다 적용된다. 만약 tcgetattr()이라는 함수를 찾고 싶다면 다음과 같이 해주면 된다.

$ nm libncurses.so.1 |grep tcget
         U tcgetattr

U가 뜻하는 바는 "undefined" 즉 ncurses 라이브러리가 사용하고는 있지만 아직 정의는 하지 않고 있다는 뜻이다.

이렇게도 할 수 있다.

$ nm libc.so.5 | grep tcget
00010fe8 T __tcgetattr
00010fe8 W tcgetattr
00068718 T tcgetpgrp

`W'는 "weak" 즉 심볼이 정의는 되어있으나 다른 라이브러리에 의해 재정의될 수 있는 형태라는 의미이다. 일반적으로 정상적인 경우에는 `T'라고 씌여진다.

sin()이 어디에 있는가라는 질문에 대한 가장 짧은 답은 libm.(so|a)이다. <math.h>에 정의되어 있는 모든 함수들은 바로 이 수학 라이브러리에 들어있다. 그것을 사용하기 위해서는 링크시에 -lm 옵션을 주어야 한다. using any of them.

6.3 화일 찾기

ld: Output file requires shared library `libfoo.so.1`

컴파일을 하다보면 위와 같은 메세지가 종종 나오는 것을 볼 수 있을 것이다. ld 그리고 유사한 프로그램들이 화일을 찾는 방식은 버전에 따라 다르지만 기본적으로 /usr/lib를 찾게 된다. 이 곳 말고도 다른 곳에 라이브러리를 가지고 있고 그것을 ld 에게 알려주기 위해서는 gcc 나 ld 에게 라이브러리가 잇는 디렉토리를 -L 옵션을 줘서 알린다.

-L 옵션을 주어도 안된다면, ld 가 원하는 화일이 적절한 장소에 가 있는지 확인해보라. a.out 에 대해서는 -lfoo 라고 하면 ld는 libfoo.sa (공유 라이브러리)를 찾게 된다. 만약 그것을 찾는데 실패하면 libfoo.a (정적 라이브러리)라는 화일을 찾는다. ELF에 한해서는 libfoo.so를 찾고 나서 libfoo.a를 찾는다. libfoo.solibfoo.so.x에 대한 링크이다.

6.4 여러분만의 라이브러리 만들기

버전 관리

다른 모든 프로그램과 마찬가지로 라이브러리 또한 계속적으로 버그를 잡아가야 한다. 또는 새로운 기능을 도입하거나 현재 있는 것을 더 효율적인 것으로 교체한다든지 그리고 필요없는 것은 없애버린다든지 하는 일이 필요하다. 이런 경우 변화하는 라이브러리를 가지고 프로그래밍하는 것은 문제가 아닐 수 없다. 만약 사라져버린 옛 기능에 의존하는 프로그램이라면?

그래서 우리는 라이브러리 버전이라고 하는 것을 도입한다. 그리고 라이브러리의 변화를 마이너 또는 메이저 변화 이렇게 분류하고 마이너 업그레이드는 기존의 프로그램들과 충돌이 없는 변화를 지칭하게 한다. 라이브러리의 버전은 화일명을 보면 알 수 있다. (사실 엄밀히 말하자면, ELF에 대해서는 거짓말이다. 왜 그러한지는 계속 읽어보면 나올 것이다) libfoo.so.1.2는 메이저 버전 1 이고 마이너 버전2 이다. 마이너 버전도 다소 중요한 것이 될 수도 있다. libc의 경우에는 마이너버전에다 패치레벨을 집어넣는다. 따라서 libc.so.5.2.18과 같은 이름이 생긴다. 숫자 말고도 문자, 언더스코어문자(_), 또는 프린트 가능한 문자를 넣어도 좋다.

ELF와 a.out 형식의 커다란 차이점 중에 하나가 바로 공유 라이브러리를 만드는 방식에 있다. 우선은 ELF를 알아보기로 하자. 왜냐하면 더 쉽기 때문이다.

ELF? 도대체 그게 무엇인가?

ELF (Executable and Linking Format)이라고 하는 것은 원래 USL(UNIX System Laboratories)라고 하는 곳에서 개발한 바이너리 형식이다. 그리고 현재는 솔라리스와 SVR4에서 사용 중이다. 리눅스가 사용해왔던 오래된 a.out보다 더욱 더 좋은 유연성 때문에 GCC와 C 라이브러리 개발자들은 지난 해 리눅스 표준 바이너리 형식과 마찬가지로 ELF로 이동하기로 결정하였다.

다시 한 번 더?

이번 섹션은 '/news-archives/comp.sys.sun.misc' 문서로부터 나오는 내용이다.

ELF ("Executable Linking Format)라고 하는 것은 "새롭고 향상된" 오브젝트 화일 형식으로서 SVR4 에 도입되었다. ELF는 그냥 COFF 방식보다 더욱 강력하다. 왜냐하면 사용자 확장성이 있기 때문이다. ELF는 오브젝트 화일을 임의의 길이를 갖는 섹션들의 리스트라고만 생각한다. 그것은 고정된 크기의 객체을 갖는 배열과는 다르다. 이러한 섹션은 COFF와는 달리 특정 위치에 있을 필요도 없고, 또한 특수한 순서대로 놓여있을 필요도 없다. 사용자들은 원한다면 새로운 섹션을 첨가할 수 있다. ELF는 또한 DWARF(Debugging With Attribute Record Format)라고 하는 아주 아주 강력한 디버깅 포맷을 가지고 있다. - 리눅스에서는 아직 완벽히 구현되고 있지는 않다. 하지만 작업이 진행 중이다 DWARF DIE들(또는 Debugging Information Entries) ELF 에서 .debug 섹션을 형성한다. 고정된 크기의 작은 정보들 대신에 DWARF DIE들은 각각 임의의 길이를 갖는 복잡한 속성들을 포함하고 있으며 영역별로 프로그램 데이타의 트리구조로씌여져 있다. DIE는 COFF .debug 섹션보다 많은 양의 정보를 잡아낼 수 있다.(COFF의 경우에는 C++ 계승 그래프와 같은 것들을 잡아낼 수 없다.)
ELF 화일들은 SVR4(솔라리스 2.0 ?)의 ELF 접근 라이브러리를 통해서 접근할 수 있다. 그 라이브러리는 ELF에 대하여 쉽고 빠른 인터페이스를 제공하고 있다. ELF 접근 라이브러리를 쓰면서 생기는 중요한 잇점중의 하나는 ELF 화일을 유닉스 화일로서 볼 필요가 전혀 없다는 것이다. 그것은 단지 Elf * 로서 접근가능하다. elf_open() 호출을 하면 그 다음부터 가능하다. 그 후에 elf_foobar()와 같은 작업을 한다. 이는 예전의 COFF 방식에서 실제 디스크 상의 이미지를 가지고 작업했던 것과는 전혀 다른 것이다.

ELF에 대한 찬성/반대, 그리고 현재의 a.out 시스템을 ELF 지원 시스템으로 업그레이드해야 할 필요성들은 ELF하우투 문서에서 다루고 있으며 본인은 그것을 여기에 적고자 하지는 않는다.

ELF 공유 라이브러리

libfoo.so라는 공유 라이비르러를 만들기 위한 기본적인 절차는 다음과 같다.

$ gcc -fPIC -c *.c
$ gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 *.o
$ ln -s libfoo.so.1.0 libfoo.so.1
$ ln -s libfoo.so.1 libfoo.so
$ LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH ; export LD_LIBRARY_PATH

이렇게 하면 libfoo.so.1.0이라는 공유 라이브러리가 만들어질 것이다. 그리고 ld (libfoo.so 필요)와 동적 링커(libfoo.so.1 필요)에 필요한 적절한 링크가 만들어진다. 그것을 테스트해보기 위해서 우리는 LD_LIBRARY_PATH에다 현재 디렉토리를 첨가한다.

만약 라이브러리가 제대로 작동한다는 것을 확인하면, 그 라이브러리를 /usr/local/lib로 이동시킨다. 그리고 다시 링크를 만들어준다. libfoo.so.1로부터 libfoo.so.1.0에 이르는 링크는 ldconfig라고 하는 프로그램에 의해 항상 최신 정보로 관리된다. 보통은 부팅과정에서 알아서 해준다. 하지만 libfoo.so는 수동으로 해주어야 한다. 여러분이 한번에 한 라이브러리의 모든 부분들(예를 들어 헤더화일도 해당) 꼼꼼히 업그레이드해주려고 한다면 libfoo.so -> libfoo.so.1이라는 링크를 만들어 주면 된다. 그렇게 되면 ldconfig가 알아서 링크를 관리해준다. 만약에 이런 것까지 모두 여러분 스스로 모두 행하려고 한다면 나중에 문제가 생길 수도 있다. 분명히 말해두었다.

$ su
# cp libfoo.so.1.0 /usr/local/lib
# /sbin/ldconfig
# ( cd /usr/local/lib ; ln -s libfoo.so.1 libfoo.so )

버전 번호 붙이기, soname 그리고 심볼릭 링크

각 라이브러리는 soname이라는 것을 가지고 있다. 링커가 찾고 있는 라이브러리 안에서 이러한 이름을 발견하게 되면, 실제 화일명(libfoo.so와 같은 이름)이 아니라 soname이라고 하는 것을 실행 바이너리에 표시해둔다. 실행시에는 동적 로더가 soname을 갖는 화일을 찾게 된다. 이 역시 화일명이 아니다. 이는 무엇을 의미하는가? 하면 libfoo.so 화일명을 가진 라이브러리는 libbar.so라는 soname을 가질 수도 있고 그곳에 링크된 모든 프로그램은 결국 libbar.so를 찾는다는 것이다.

이것은 상당히 무의미한 기능처럼 보이는데 사실은 이것이야말로 같은 라이브러리의 서로 다른 버전이 어떻게 한 시스템에서 공존할 수 있는가를 이해하는데 있어 핵심적인 부분이다. 리눅스에서 라이브러리 이름짓는 사실상의 표준은 라이브러리를 libfoo.so.1.2 이런 식으로 부르고 libfoo.so.1이라는 soname을 부여하는 것이다. 만약 표준 라이브러리 디렉토리(예를 들어/usr/lib)에 추가되면 ldconfig는 libfoo.so.1 -> libfoo.so.1.2라는 링크를 만들어 줄 것이다. 그렇게 함으로써 실행시에 적절한 이미지가 선택되도록 해준다. 여러분은 또한 libfoo.so -> libfoo.so.1이라는 심볼릭 링크도 필요하다. 왜냐하면 ld 가 링크할 때 정확한 soname 을 찾게 하기 위해서이다.

따라서 라이브러리의 버그를 고칠 때 또는 새로운 기능을 첨가할 때(기존의 프로그램에 악영향을 주지 않는 변화들), 다시 라이브러리를 만들고 같은 soname을 주고 화일명은 바꾸도록 한다. 만약 여러분의 라이브러리와 링크되어 있는 기존의 프로그램들과 충돌하게 되는 라이브러리로 변화할 때는 soname의 숫자를 하나 늘리면 된다. 이러한 경우 새로운 버전의 라이브러리는 libfoo.so.2.0이 될테고, soname은 libfoo.so.2가 될 것이다. 그리고 이번에는 libfoo.so를 새로운 버전의 라이브러이에 심볼릭 링크시키도록 하자.

여러분이 꼭 이런 식으로 라이브러리 이름을 지어줄 필요는 없다. 하지만 그것은 괜찮은 관습이다. ELF는 여러분에게 라이브러리 이름짓기에 있어 유연성을 주고 있지만 그렇다고 해서 꼭 그렇게만 하라는 것은 아니다.

요약하자면, 여러분이 호환성을 깨는 것이 메이저 업그레이드이고 그렇지 않은 것이 마이너 업그레이드라는 전통을 준수한다면 다음과 같이 하라.

gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor

모든 것이 제대로 될 것이다.

a.out 전통적인 형식

공유 라이브러리 만들기의 용이함은 ELF로의 업그레이드에 대한 중요한 이유이다. a.out으로 가능하기는 하다. ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz를 받아오자. 그리고 그 화일을 풀어서 나오는 20 페이지짜리 문서를 읽어본다. 남들에게 뻔히 보이는 열성지지자가 되고 싶지는 않다. 하지만 나는 나 자신을 귀찮게 하고 싶지는 않다. :-)

ZMAGIC vs QMAGIC

QMAGIC 이라고 하는 것은 예전의 a.out(ZMAGIC 이라고 알려져 있다)과 마찬가지로 실행 화일의 형식이다. 하지만 첫번째 페이지는 매핑하지 않는 바이너리이다. 0-4096 까지 어떠한 매핑도 존재하지 않기 때문에 이렇게 함으로써 NULL 디레퍼런시 트래핑(deference trapping)을 아주 쉽게 할 수 있다. 부차적인 효과로서 여러분의 실행화일은 약 1K 정도 작아지게 된다.

구식 링커들은 오로지 ZMAGIC 만을 지원한다. 약간 덜 구식의 링커들은 둘 다 지원하면, 최신 버전들은 오로지 QMAGIC 만을 지원하고 있다. 이것은 별로 중요하지 않다. 왜냐하면 커널 자체가 두 가지를 모두 실행시킬 수 있기 때문이다.

file 명령을 주면 그것이 QMAGIC인지 판별할 수 있을 것이다.

화일 위치(File Placement)

a.out(DLL) 공유 라이브러리는 2 개의 실제적인 화일 그리고 하나의 링크로 구성 되어 있다. 이 문서 전체를 통해서 계속 사용해온 이름인 foo 라는 라이브러리에 대하여 예를 들어 알아보자. foo 에 대하여 libfoo.sa, libfoo.so.1.2 그리고 libfoo.so.1 이라는 링크로 구성되어 있다. 링크는 libfoo.so.1.2를 가리킨다. 이것들 모두 무엇인가?

컴파일할 때 ldlibfoo.sa를 찾는다. 이것이야말로 라이브러리에 대한 그루터기 화일이 된다. 그리고 링크과정에 대한 모든 외부 데이타와 함수에 대한 포인터를 지니고 있다.

하지만 실행시에는 동적 로더가 libfoo.so.1을 찾는다. 이는 실제 화일이 아니라 심볼릭 링크이다. 그 이유는 앞서와 마찬가지로 라이브러리가 기존의 어플리케이션과의 충돌없이, 더 새로운, 버그가 잡힌 새로운 버전으로 교체될 수 있도록 하기 위해서이다. 새로운 버전이 나오면(예를 들어 libfoo.so.1.3)이라고 하자. ldconfig를 실행시키면 자동으로 libfoo.so.1 --> libfoo.so.1.3 링크 작업을 해 줄 것이다. 구버전을 쓰는 프로그램도 아무 이상이 없을 것이다.

DLL 라이브러리(동어반복이라는 사실은 알고 있다. 역자 주 :DLL 에 이미 라이브러리라는 말이 들어있다)는 종종 정적 라이브러리보다 크다. DLL은 미래의 확장성을 위해서 뻥 뚤린 구멍의 형태로 자리를 유보해둔다. 하지만 그 자리는 디스크 영역을 차지하지는 않도록 할 수 있다. 간단한 cpmakehole이라는 프로그램으로 이렇게 하는 것이 가능하다. 이미 고정된 위치에 주소들이 있으므로 라이브러리 생성 후에 strip 할 수 있다. 하지만 ELF 라이브러리에 대해서는 strip하지 말라.

libc-lite 란 무엇인가?

libc-lite 라고 하는 것은 libc 에 대한 소규모 버전이라고 할 수 있다. 하나의 플로피 안에 들어가고 유닉스의 자잘한 많은 업무들에 충분한 정도만으로 구성된 라이브러리이다. 그것은 curses 나 dbm, termcap 등의 코드를 포함하고 있지 않다. 만약 여러분의 /lib/libc.so.4가 lite 버전의 라이브러리에 링크되어 있다면 즉시 완전한 libc 버전으로 교체하기 바란다.

보통 슬랙웨어의 루트 디스켓을 마운트해보면 이 lite 버전의 C 라이브러리가 들어있음을 알 수 있을 것이다. 설치 준비와 설치에 필요한 만큼의 작은 C 라이브러리이다.

링크하기 : 일반적인 문제들

여러분의 링크 문제를 내게 보내달라! 그러면 그것에 대해서 나는 아무 일도 하지 않을 것이다. 하지만 많이 쌓이는 문제에 대해서는 글을 쓰겠다.

공유 라이브러리와 링크되길 바라는데 정적 라이브러리와 링크되고 있다.

우선은 ld가 공유라이브러리를 제대로 찾을 수 있도록 링크가 알맞게 되어 있는지 점검한다. ELF에 대해서라면 이것은 libfoo.so 심볼릭 링크를 말하며 a.out의 경우에는 libfoo.sa화일을 말하는 것이다. ELF binutil 2.5 버전에서 2.6 버전으로 업그레이드한 많은 사람들이 겪고 있는 문제이다. 전 버전이 공유 라이브러리에 대하여 오히려 더 똑똑하게 찾아냈는데, 그 사람들은 모든 링크를 제대로 만들지 않았던 것이다. 지적인 행동양식을 다른 모든 설계방식과의 호환성을 위해서 신버전에서 제거되었다. 지적 행동양식은 잘못된 가정을 갖게 되고 오히려 더 많은 문제를 낳기 때문에 그렇게 한 것이다.

DLL 툴인 mkimage 가 libgcc를 찾는데 실패한다.

libc.so.4.5.x와 그 이상의 버전에 관하여 libgcc는 더 이상 공유 라이브러리가 아니다. 따라서 여러분은 `-lgcc'와 같은 라인을 모두 `gcc -print-libgcc-file-name`로 바꿔주어야 한다. (주의할 것은 바로 백쿼우트문자(`)의 사용이다. 꼭 이 문자만을 사용하라.)

또한 모든 /usr/lib/libgcc* 화일들을 삭제하라. 이것이 중요하다.

__NEEDS_SHRLIB_libc_4도 마찬가지 문제이다.

DLL 생성시에 ``Assertion failure'' 메시지

이 메시지는 여러분이 가지고 있는 jump table 슬롯이 원래의 jump.vars화일에 너무 적은 공간 밖에 예약되지 않았기 때문에 오버플로우로 인해 생기는 문제이다. 여러분은 tools-2.17.tar.gz 패키지에 들어 있는 `getsize' 명령을 사용하여 그 범인을 찾아낼 수 있다. 아마도 유일한 해결책은 메이저 번호의 증가 밖에 없는 것 같다. 단지 이전 버전과 호환되도록 고려하면서 말이다.

ld: output file needs shared library libc.so.4

이러한 문구는 보통 libc가 아닌 라이브러리들 (즉, X 윈도우 라이브러리들...)하고 링크하려고 할 때 발생한다. -static을 함께 사용하지 않고 링크 시에 -g 옵션을 주었을 때이다.

공유 라이브러리에 대한 .sa 화일은 보통 정의되지 _NEEDS_SHRLIB_libc_4 라는 심볼을 가지고 있는데 나중에 libc.sa에서 해결된다. 하지만 -g 옵션을 주게 되면 libg.a 또는 libc.a와 링크되게 되므로 그 심볼은 해결이 되지 않게 되고 위와 같은 에러 메세지가 뜨게 되는 것이다.

결론적으로 -g 플래그로 컴파일할 때는 -static 이라는 옵션을 함께 주기 바란다. 또는 -g로 컴파일하지 않으면 된다. 링크할 것 없이 원하는 부분만 -g 옵션을 주고 컴파일해도 충분한 디버깅 정보를 얻을 수 있다.


다음 이전 차례