Make 기반 빌드 시스템 (9): 공유 라이브러리 지원

공유 라이브러리 지원

재활용하기 쉽고 필요한 라이브러리들과 응용 프로그램들을 한방에 빌드할 수 있는 빌드 시스템을 구축하는 사이, MyClient 포로토타입을 받아 간 우리의 영업맨, 어느 날 ‘짐짓 들뜬’ 표정을 지으며 달려 옵니다. 아~~, 갑자기 드는 불길한 예감! 아니나 다를까, 계약 딴거나 다름 없다며, 이제 정말 ‘사소한 거’ 하나만 해 주면 된다네요.

사소한 거라, 진짜로요? 아 그렇다니까. 고객사가 필요로 하는 기능은, 이미 이건 다 구현되어 있고 소스도 다 제공해 줄 건데 우리 쪽에서 함수 몇 개 호출만 해주면 된다는데? 흠. 진정 그렇게 친절한 갑이 있단 말인가요?

사소하단 고객의 요구사항인 즉슨, 오픈 소스 라이브러리의 일부를 가져와 약간의 수정만 해서 빌드하고, 우리 프로그램에서 초기화 함수랑 종료 함수, 그리고 중간 중간 callback 함수를 호출해 주는 것이네요. 흠, 그 정도라면야… 싶었는데, 그 오픈 소스 라이브러리의 라이센스가 LGPL 여서 공유 라이브러리로 빌드해 달라는 조건이 붙었습니다. 윽.

LGPL 라이센스이면, 해당 라이브러리를 링킹해서 사용하는 나머지 소스는 공개할 필요가 없습니다. 대신 (1) LGPL 라이브러리의 소스를 수정하는 경우, 수정된 소스를 공개해야 하고, (2) 최종 사용자가 LGPL 라이브러리의 새 버전 또는 자신이 수정한 것을 빌드해서 사용할 수 있도록 해 줄 의무를 지켜야 합니다.

(2)를 만족시키는 방법은 해당 라이브러리를 정적으로 빌드하느냐 아니면 공유 라이브러리로 빌드하느냐에 따라 달라집니다. 정적 라이브러리로 빌드하는 경우에는, LGPL 라이브러리를 가지고 최종 실행 파일을 빌드하는데 필요한 것들, 다시말해 최종 실행 파일의 컴파일 된 목적 파일과, 최종 실행 파일에서 사용하는 나머지 라이브러리의 바이너리들을 제공해야 합니다.

즉 MyClient 프로그램을 릴리즈 하면서, 또는 추후 사용자로부터의 요청이 있을 때, LGPL 라이브러리의 소스 파일과 함께  libMyThread.a libMyNetwork.a, libMyProtocol.a와 MyClient.o 를 같이 배포해야 한다는 것이죠. 어려운 건 아니지만, 뭐 이런 것들을 챙겨서 배포한다는 게 신경쓰이고 성가십니다.

이에 비해 LGPL 라이브러리를 공유 라이브러리로 빌드해서 사용하는 경우에는, 최종 실행 바이너리를 릴리즈 할 때 LGPL 라이브러리의 소스 파일들만 공개하면 됩니다. 공유 라이브러리는, 응용 프로그램을 빌드할 때가 아니라 실행되는 시점에서 필요하기 때문에 내가 작성한 목적 파일과 라이브러리를 배포할 필요가 없어지죠.

어쨌거나 우리 make 기반 빌드 시스템은 정적 라이브러리만 빌드합니다. 자 얼른 영업팀에 가서 안 된다고 말하세요. 응? 그럴 수는 없죠… 사실 소프트웨어 개발 쪽 일을 좀 하다 보면, 꼭 이런 경우 아니라도, 공유 라이브러리로 빌드해야 하는 경우가 생기곤 합니다. 그리고 공유 라이브러리를 빌드하는 것 자체는 어려운 것도 아니고요.

그럼 공유 라이브러리 빌드 기능을 집어 넣기 앞서 정적 라이브러리와 공유 라이브러리에 대한 개념부터 살펴 볼까요?

정적 라이브러리와 공유 라이브러리

아시는 분들이야 잘 아실테지만, “도대체 라이브러리면 다 라이브러리지, 정적 라이브러리는 뭐고 공유 라이브러리는 뭐람?” 하시는 분이라면 다음의 글들을 읽어보실 것을 추천합니다.

기본적으로 라이브러리는 비슷한 기능을 하나의 파일로 묶어서, 해당 기능을 필요로 하는 다른 프로그램에서 이를 재사용할 수 있게 해 주는 설계 기법입니다. 네, 이제까지 우리가 계속 해 온 겁니다. 그런데 사실 리눅스에서는 두 가지 라이브러리 유형을 지원합니다.

첫 번째는 이제까지 우리가 계속 만들어 온, 정적(Static) 라이브러리입니다. 정적 라이브러리는 응용 프로그램을 컴파일하고 링킹하는 시점에서 정적으로 결합됩니다. 응용 프로그램에서 사용하는 라이브러리의 구현부분들을 링킹할 때 최종 응용 프로그램에 복사해 넣는 것이죠.

여러 개의 응용 프로그램들이 같은 정적 라이브러리를 링킹하면, 개별 프로그램마다 라이브러리의 사본을 가지게 되는 셈입니다. 따라서 라이브러리가 커질 수록 최종 응용 프로그램들의 크기가 커질 뿐더러, 링킹하는 응용 프로그램들이 많아질 수록, 저장공간과 실행 메모리를 더 많이 차지하게 됩니다.

이에 비해 두 번째 라이브러리 유형인 공유(Shared) 라이브러리는, 응용 프로그램을 빌드해서 링킹할 때는 공유 라이브러리의 어떤 기능을 사용할 것이다 표시만 해놓고, 응용 프로그램과 라이브러리와의 결합은 응용 프로그램이 실행될 때 이뤄집니다.

따라서 이름에 걸맞게, 복수 개의 응용 프로그램들이 하나의 라이브러리 파일을 공유하게 됩니다. 당연히 저장공간과 메모리도 적게 차지하고, 라이브러리가 변경되었을 때 (주요 인터페이스의 변경과 같은 호환성 문제만 아니라면) 실행 파일들을 다시 빌드할 필요가 없습니다.

이렇게만 놓고 보면 당최 정적 라이브러리를 써서 좋은 건 하나도 없어 보이네요. 하지만 빛이 생기면 그림자도 생기는 게 세상 이치죠. 일단은 성능 문제가 있습니다. 실행 시에 결합되는 공유 라이브러리의 특성상 컴파일 시 위치 독립적인 코드를 생성해야 하는 등의 몇 가지 이유로 인해 공유 라이브러리의 실행 속도가 정적 라이브러리에 비해 느리다고 합니다. 궁금하신 분은 다음 사이트를 참고하세요.

제 개인적으로 공유 라이브러리냐 정적 라이브러리냐에 따른 실행 속도의 차이를 체감한 적은 아직까지는 없습니다만, 다음 사이트에서는 MySQL 의 경우 심지어 13% 까지 느려질 수 있다는 내용도 있긴 합니다. (좀 극단적인 경우가 아닐까 싶습니다.)

다음으로, 하나의 공유 라이브러리가 여러 개의 응용 프로그램들에 의해 공유되기 때문에 발생할 수 있는 호환성의 문제가 있습니다. 즉 어떤 공유 라이브러리가 새 버전으로 업데이트되어야 하는데, 새 버전의 인터페이스가 예전 버전과 호환되지 않는 경우, 예전 버전을 사용하는 기존 응용 프로그램들에 문제가 발생할 수 있는 거죠. 이를 피해가기 위해 soname 이란 걸 사용해서 버전 관리를 합니다만, 어쨌든 하나의 프로그램에 몽땅 포함되는 정적 라이브러리에 비해서는 복잡하고 그만큼 신경 쓸 게 늘어난다는 건 분명합니다.

다음 사이트는 공유 라이브러리에 대한 이런 저런 내용이 잘 정리되어 있습니다. 관심 있으신 분들은 참고하세요.

공유 라이브러리 소스 파일 컴파일

공유 라이브러리는 “링킹할 때는 어떤 기능을 사용할 것이다 표시만 해놓고, 실제 결합은 응용 프로그램이 실행될 때 이뤄진다”고 했죠? 이 얘기는 다시 말해 어떤 함수가 호출될 때 실제로 어떤 주소에 그 함수가 있게 될지 미리 알 수 없다는 뜻입니다.

라이브러리를 정적으로 링킹할 때에는 오브젝트의 위치가 바로 결정되기 때문에, 호출되는 함수의 참조(레퍼런스)를 실제 주소값(위치)로 바로 교체할 수 있습니다. 그래서 실행 중에 함수 호출을 만나면, 해당위치로 바로 점프하게 됩니다.

하지만 실행 시점에 로딩되는 공유 라이브러리의 경우에는 이게 불가능합니다. 대신 응용 프로그램이 실행될 때, 호출되는 함수들의 목록을 하나의 테이블로 만들어 두고, 공유 라이브러리를 로드하면서 이 테이블에 각 함수의 주소값을 채운 다음, 함수 호출을 만날 때마다 이 테이블을 참조해서 해당 함수의 주소값으로 점프합니다.

이런 이유로 공유 라이브러리에 포함시킬 소스 파일을 컴파일 할 때는 위치 독립적으로 만들 필요가 있게 되는데요, 설명은 꽤 복잡했지만 실제 컴파일 작업은 gcc 호출할 때 위치 독립적인 코드를 생성하게 하는 -fPIC 옵션을 추가하는 것으로 끝입니다. 다음과 같이 말이죠

너무 간단해서 허탈할 지경이지만, 우리 빌드 시스템에 적용하기 위해서는, 정적 라이브러리와 공유 라이브러리를 같이 지원해야 하므로, 공유 라이브러리인지 아닌지를 결정할 변수가 하나 필요합니다.

그럼 이제 공유 라이브러리인 경우에는 -fPIC 옵션을 주도록 컴파일 규칙을 고치는 것은 어렵지 않습니다. 공유 라이브러리일 때에만 전용 옵션 변수를 정의해서 컴파일 규칙에 추가하면 되니까요.

간단하죠? 컴파일이 끝났으니, 이제 이것들을 공유 라이브러리로 묶어야 하는데, 이게 목적 파일들을 ar로 묶은 다음 심볼 인덱싱하는 것으로 끝인 정적 라이브러리에 비해 고려할 게 좀 있습니다.

공유 라이브러리의 버전과 호환성

앞에서 공유 라이브러리의 호환성 문제를 잠깐 얘기했죠? 공유 라이브러리는 하나 이상의 응용 프로그램들이 공유하고 있기 때문에, 버그 수정이라든지 기능 추가 등으로 인해 라이브러리를 업데이트 해야 할 때 호환성 문제를 일으킬 소지가 있습니다.

다시 말해, 변경할 내용이 어떤 함수 내의 한 두줄을 수정하는 수준이면, 그냥 라이브러리를 새로 빌드한 다음 라이브러리 파일을 업데이트하면 됩니다. 하지만 만약에 다른 응용 프로그램이 사용 중인 함수들의 인터페이스가 바뀐다든지 동작 방식이 완전히 변경되는 경우에는, 라이브러리 파일을 그냥 업데이트 했을 때 다른 응용 프로그램들이 오동작 할 수도 있다는 것입니다.

결국 이런 호환성 문제를 피하려면, 공유 라이브러리의 새 버전이 이전 버전과 호환성을 유지하는 경우와 그렇지 않은 경우를 구분해서 처리할 어떤 메커니즘이 필요한데, 리눅스에서는 이를 라이브러리의 버전을 통해서 다음과 같이 처리합니다.

(1) 먼저 공유 라이브러리 버전을 결정할 때, 메이저 버전과 마이너 버전으로 구성합니다. 예전 버전과 호환성이 유지될 때에는 마이너 버전만 증가시키고, 하위 호환성이 없을 때에는 메이저 버전을 증가시킵니다. 라이브러리 버전 정책을 이렇게 정하고 나면, 업데이트 할 때 라이브러리의 버전 비교만으로 호환성 여부를 쉽게 파악할 수 있습니다.

(2) 공유 라이브러리 파일을 만들 때, 2가지 이름을 부여합니다. 먼저 파일 시스템 상에서의 실제 이름(real name)이 필요한데, 이건 버전마다 다르게 구분할 수 있도록 파일 이름에 전체 버전, 그러니까 메이저 버전과 마이너 버전을 모두 포함시킵니다. 보통 공유 라이브러리는 lib 으로 시작하고 .so 로 끝나는 게 관례니까 libXXX.so.major_version.minor_version 이런 식으로 되겠죠? MyProtocol 의 경우라면, libMyProtocol.so.1.0 가 실제 이름이 됩니다.

(3) 다음으로 실제 이름에서 마이너 버전을 제외한 이름을 soname 이라고 정하고, 이 값은 공유 라이브러리 파일 어딘가에다 저장해 둡니다. (libMyProtocol.so.1.0 의 soname은 libMyProtocol.so.1 이 되겠죠?) 이렇게 해 두면, 라이브러리 파일들의 실제 이름과는 상관없이, 파일 내의 soname이 동일한 라이브러리 파일들은 서로 호환성을 유지하는 버전이라고 판단할 수 있게 됩니다.

(4) 그리고 응용 프로그램을 링킹할 때에는, 해당 응용 프로그램이 나중에 실행할 때 링킹할 공유 라이브러리의 soname 정보를 응용 프로그램에 표시해 둡니다. (참고로 어떤 응용 프로그램이 어떤 공유 라이브러리를 필요로 하는 지는 ldd 명령을 사용하면 확인 할 수 있습니다.)

(5) 리눅스 시스템은 응용 프로그램이 실행될 때, 해당 응용 프로그램이 필요로 하는 공유 라이브러리들을, 지정된 경로들을 뒤져서 찾은 다음 이를 동적으로 로드해 줍니다.

잠깐만요. 그런데 응용 프로그램은 공유 라이브러리들의 soname 정보만 가지고 있다면서요. 그런데 파일 시스템 상에는 존재하는 공유 라이브러리 파일의 실제 이름은 soname 과 다르니까 못 찾을 거 같은데요…

오! 정말 예리한 지적입니다. 사실 그게 핵심입니다. (1) ~ (5)까지, 응용 프로그램은 실제 이름이 아닌 soname 만 가지고 라이브러리들을 찾도록 만들었기 때문에, 이렇게만 하면 응용 프로그램 실행할 때 공유 라이브러리를 찾지 못한다는 에러가 나오면서 실행이 중단됩니다. soname 은 라이브러리 파일과 응용 프로그램에 기록된 정보에 불과하므로, 파일 시스템 상에서의 실제 이름으로 존재하는 라이브러리 파일과 연결해 줄 방법이 필요한데 다행히 리눅스에서는 symbolic link 를 이용해서 간단하게 연결할 수 있습니다.

즉, 실제 이름으로 라이브러리 만들 때, 이에 대한 symbolic link 를 soname으로 하나 만들면 오케이입니다. 그러면 soname으로 공유 라이브러리를 찾을 때, 해당 symbolic link가 가리키는 실제 이름의 공유 라이브러리의 파일을 찾게 되고, 그 파일에는 필요로 하는 soname이 들어 있기 때문에, 정상적으로 공유 라이브러리를 찾아서 로딩할 수 있게 됩니다. 빙고.

이젠 soname 이 symbolic link로 존재하기 때문에, 호환성에 영향을 주지 않는(마이너 버전만 바뀌는) 경우에는, 새 버전의 공유 라이브러리 파일을 추가한 다음 symbolic link 만 새 버전의 파일을 가리키도록 바꿔주면 업데이트가 완료됩니다. 문제 없죠?

호환성이 없는(메이저 버전이 변경된) 완전 새 버전인 경우에는, 새 버전의 라이브러리 파일과 함께, 이에 대응되는 symbolic link를 같이 추가합니다. 비록 예전 버전을 쓰는 응용 프로그램들은 아마도 새 버전이 가지고 있을, 완전히 끝내주는 기능들의 혜택을 받을 수는 없겠지만, 적어도 새 버전 때문에 오동작하는 불상사는 일어나지 않는다는 게 보장됩니다.

흠… 설명이 길어졌지만 요약하자면 이렇습니다.

  • 공유 라이브러리를 빌드할 때에는 메이저 버전과 마이너 버전을 정합니다. 호환성이 유지될 때에는 마이너 버전값을 하나 올리고, 그렇지 않을 때에는 메이저 버전을 올리면서, 마이너 버전은 0으로 리셋합니다.
  • 공유 라이브러리의 빌드 결과 파일은 libXXX.so.major_version.minor_version 으로 작성합니다. 이게 공유 라이브러리의 실제 이름(real name)이 됩니다.
  • 동시에, 공유 라이브러리의 실제 이름에서 .minor_vesion 을 제외하는 부분, 즉 soname을 이름으로 가지는, 실제 이름의 파일을 가리키는 symbolic link를 하나 생성합니다.

공유 라이브러리 빌드

그럼 실제로 소스 파일들을 컴파일한 목적 파일들로부터 공유 라이브러리를 작성해 볼까요? MyProtocol 라이브러리 경우, 최초로 릴리즈하는 거니까 메이저 버전은 1, 마이너 버전은 0으로 정했다 치면, 다음과 같이 라이브러리를 만들 수 있습니다.

일단 -shared 옵션은 처음 보지만 딱 이름만 봐도 공유 라이브러리와 관련되었겠거니 짐작할 수 있지 않나요? 네, 짐작대로 다른 오브젝트들에 링크되어 실행 가능한 형태가 될 수 있도록 하는 옵션입니다. 공유 라이브러리 만들 때 그냥 넣어준다고 보시면 되겠네요.

-o 는 결과 파일 이름을 지정하는 옵션이므로 공유 라이브러리의 실제 이름(libMyProtocol.so.1.0)을 지정하고 있습니다. 그 뒤에 나오는 -Wl, 옵션부분이 공유 라이브러리의 soname을 지정하는 부분입니다. (-Wl 옵션은 예전에 라이브러리들의 의존성 문제를 해결하는 과정에서 한번 사용했었느데, 생각 나시나요? , 이후에 나오는 부분을 링커한테 넘겨주는 옵션입니다. Make 기반 빌드 시스템 (7): 빌드 디렉토리 구성 부분을 참고하세요.) -soname, 하고 soname(그러니까 libMyProtocol.so.1)을 지정합니다.

그럼 실제로 Makefile과 IncludePost.mk 파일에 적용해 볼까요? 먼저 MAJOR_VERSION 과 MINOR_VERSION 을 정합니다.

그리고, 공유 라이브러리일 경우에는 soname과 실제 이름을 변수로 정의한 다음, 이를 이용해서 실제 이름의 공유 라이브러리를 만들고 나서, 이에 대한 soname의 symbolic link를 만듭니다., 다음을 보시죠.

(6) ~ (7)은 각각 메이저 버전과 마이너 버전 값을 이용해서 soname과 실제 이름에 해당하는 변수를 정의합니다. 공유 라이브러리일 때 LIB_FULL_NAME은 (8) 처럼 정의되고요, 물론 정적라이브러리일 때에는 (10)처럼 정의됩니다.

(16) ~ (18) 은 공유 라이브러리를 만들고, 이에 대한 soname 의 symbolic link를 만들어서 이를 $PROJ_ROOT/Out/Library 디렉토리의 Debug 또는 Release 디렉토리로 옮깁니다.

make lib 해서 실행하면, Out/Library/Debug 디렉토리에 libMyProtocol.so.1 와 libMyProtocol.so.1.0 잘 만들어집니다. 오예~

이제 응용 프로그램들을 빌드해야 할 차례네요.

공유 라이브러리 링킹

응용 프로그램을 컴파일하고 링킹하기 앞서 서브 디렉토리의 라이브러리들부터 빌드 해야 하는데요, 이건 make liball targets 를 실행해서 한번에 빌드할 수 있습니다.

근데 어라? 서브 디렉토리들의 라이브러리들이 잘 만들어지는 건 당연한 건데, 응용 프로그램들을 빌드하는데 링킹 에러가 납니다. 뭐가 문제일까요…

-lMyProtocol 을 찾을 수 없다는 거네요… 아차, 이건 다음의 이유 때문입니다.

보통 링커한테 알려주는 라이브러리의 이름을 linker name이라고 하는데요, 라이브러리 파일 이름에서 lib 을 빼고, 정적 라이브러리일 때에는 .a를, 공유 라이브러리일 경우에는 .so 를 뺀 부분이 됩니다. 우리 빌드 시스템의 Makefile에서는 LIB_NAME 이라는 변수로 정의되어 있죠.

-l 옵션 바로 뒤에 linker name 을 붙여서 링커한테 넘겨주면, 링커는 linker name 에 해당하는 라이브러리가 있는 지를 찾는데요, 공유 라이브러리 파일을 먼저 찾고 없으면 정적 라이브러리를 검색합니다. 즉 libMyProtocol.so 파일이 있으면 이것으로 먼저 링킹하고, 없으면 libMyProtocol.a 파일을 가지고 링킹합니다.

참고로,

  • -l 옵션을 쓰지 않고 라이브러리 파일의 전체 경로가 포함된 파일 이름(…/Out/Library/Debug/libMyProtocol.so.1.0)를 써도 되지만, 그것보다는 -l 옵션을 쓰는 게 보통 권장됩니다. 정적 라이브러리와 공유 라이브러리의 전환하기에도 좋고, 타이핑 할 문자도 적죠.
  • 만약 동일한 linker name을 가지는 정적 라이브러리와, 공유 라이브러리가 같이 있는데, 꼭 정적 라이브러리로 링킹하게 하고 싶으면 링킹할 때 -static 옵션을 추가해 주면 됩니다.

자, 이렇게 놓고 보면 공유라이브러리와 관련된 이름은 모두 3개가 되네요. real name은 파일 시스템 상에서의 라이브러리 파일의 실제 이름입니다. soname은 호환성이 보장되는 논리적 이름으로서 라이브러리 내에 저장됩니다. linker name은 링커한테 알려주기 위한 lib 과 확장자를 제외한 이름이 됩니다.

이제는 링커가 -lMyProtocol 가 없다고 하는 게 이해 됩니다. -L 옵션으로 지정된 디렉토리들에, real name과 soname 에 해당하는 파일들은 있지만, linker name으로부터 매치되는 파일, 즉 libMyProtocol.so 파일이나 libMyProtocol.a 둘 중 어느 것도 없기 때문입니다. 따라서 이 문제를 해결하려면, 링커가 애타게 찾을 이름을 soname에 대한 symbolic link 로 만들어주면 되겠습니다.

이제 다시 make liball targets 하면, 라이브러리 디렉토리에 libMyProtocol.so.1.0, libMyProtocol.so.1, libMyProtocol.so 이렇게 하나의 파일과 2개의 symbolic 링크들이 잘 만들어지면서, 응용 프로그램들의 링킹도 잘 되는 것을 확인할 수 있습니다.

흐뭇한 마음에 링킹 에러 없이 완료된 빌드 메시지를 보는데요, 가만가만, 뭔가가 좀 이상합니다.

이런, 아까는 링킹 에러 때문에 미처 확인하지 못했는데, (10) 에서 TestConnection.c 을 컴파일하는데, 공유 라이브러리 소스 파일 컴파일할 때 지정한 -fPIC 옵션이 들어가 버린 게 눈에 들어 옵니다.

아, 이건 또 왜 그럴까요? 그런데 곰곰히 생각해보면, 현재 빌드 시스템에서 소스 코드(.c)로부터 목적 파일(.o)로 컴파일하는 규칙은 딱 하나, 다음의 규칙입니다.

소스 코드 컴파일 할 때에는 LIB_SRCS 든 TARGET_SCRS 든 구분않고 동일한 규칙 하나로 처리됩니다. 그러니까 LIB_SRCS 파일들을 컴파일할 때 IS_SHARED = 1 인 경우 -fPIC 옵션을 들어가도록 규칙을 수정하면, 당연히 TARGET_SRCS 의 경우에도 똑같이 -fPIC 옵션이 적용될 수 밖에 없습니다.

응용 프로그램의 소스 파일에 -fPIC 옵션을 포함해서 컴파일하고 링킹했을 때 실행 자체는 됩니다만, 공유 라이브러리에 포함되지 않는 소스 코드를 위치 독립적으로 컴파일하는 게 맘에 걸립니다. (혹시 이와 관련해서 보충 설명해 주실 수 있는 분 계시면, 댓글 달아 주세요. 미리 감사 드립니다.)

어쨌든 응용 프로그램에서는 -fPIC 가 적용되지 않도록 하려면, TARGET_SCRS 에 속한 소스 코드일 때는 $(SHARED_FLAGS) 를 빼는 방법을 찾아야 합니다. 그 정도야 make 문자열 함수를 쓰면 가능하지 않을까요? 이렇게 말입니다.

(3)에서 나온 $(findstring find, in) 은 in 문자열에서 find 문자열을 찾아 보고, 있는 경우에는 find 를, 없을 때에는 빈 문자열을 반환하는 make 문자열 함수입니다. 그리고 $< 는 첫번 째 선행규칙을나타내는 자동 변수니까 컴파일하는 소스 코드가 될 테고요. 따라서 $< 가 $(TARGET_SRCS)에 포함되어 있는지 여부를 가지고, 다시 말해 $(findstring…) 결과가 $< 인지 빈 문자열인지를 가지고, 라이브러리 소스인지 응용 프로그램 소스인지를 판단할 수 있다는 거죠.

$(findstring…) 실행 결과가 빈 문자열이면 이건 라이브러리의 소스 파일일 테니까, 공유 라이브러리일 때에 -fPIC로 지정되는 $(SHARED_FLAGS) 를 포함시킴으로써, 공유 라이브러리의 소스 파일들에 대해서만 -fPIC 옵션을 적용시킬 수 있을 것 같습니다.

아쉽게도 위의 코드는 제대로 동작하지 않습니다. 왜냐하면, make는 Makefile을 읽을 때 조건들을 평가합니다. 그런데, 자동 변수들은 명령들이 실행되기 전에는 정의되지 않기 때문에, 결과적으로 조건의 테스트 안에서 자동 변수들을 사용할 수 없게 됩니다. 따라서 위의 조건은 항상 true가 되어 TARGET_SRCS 의 소스 파일들의 컴파일 시 여전히 -fPIC 옵션이 적용됩니다.

하지만 여기서 그만둘 수는 없죠. 음… 조금만 더 생각해 보자면, 지금 ifeq … else … endif 으로 분기해야 하는 부분은 복수의 변수들 선언이나 규칙과 같이 Makefile의 구조적 변경사항과 관련된 것이 아니라, 단순히 레시피의 명령어 부분입니다. 이럴 때에는 함수 문맥 내에서 조건 확장이 이루어지는, 그래서 자동 변수 사용 여부와는 상관없는 $(if condition, then-part[, else-part]) 함수를 사용할 수 있습니다. condition이 참이면 then-part를, 그렇지 않으면 else-part가 실행됩니다. (else-part는 생략 가능하고요)

다음을 보세요.

(9) ~ (11) 의 $(if …) 함수는 $(findstring $< $(TARGET_SRCS))가 빈 문자열이면, 다시 말해 library 소스 파일이면 $(SHARED_FLAGS) 없이, 그렇지 않으면 컴파일 옵션에 $(SHARED_FLAGS)를 포하시켜 컴파일을 수행합니다. 자 이제 다시 실행해 볼까요?

(3), (5) 를 보면, IS_SHARED = 1 일 때에도 TARGET_SRCS 에 정의된 소스 파일들의 컴파일 결과는 -fPIC 옵션이 적용되지 않습니다. 원하는 바를 모두 이루었습니다. ($(if…) 함수를 사용해서 라이브러리 소스인지 여부를 간단하게 판단할 수 있음을 알려 주신 mug896님께 감사드립니다.)

나머지, clean 과 의존성 규칙들

이제 큰 것들은 다 되었으니 마지막으로 clean 과 의존성 규칙만 손 보면 되겠네요. 먼저 clean은 공유 라이브러리일 때 symbolic link까지 삭제해 주는 걸 추가하는 것으로 오케이입니다.

depend 규칙은, 먼저 LIB_SRCS와 TARGET_SRCS 를 다르게 취급해야 하니까, 한번에 depend 하던 것을 나눠서 하도록 바꿔줄 필요가 있습니다.

다음으로, TARGET_NAMES 의 라이브러리들에 의한 의존성 규칙도 손을 봐 줄 필요가 있습니다.

(5) 에서 예전에는 정적 라이브러리만 작성했기 때문에, 그냥 lib%.a 로 치환했었죠. 이제는 .so 도 있을 수 있기 때문에 두 가지에 모두 의존하도록 바꾸어야 합니다. 제일 쉬운 방법은 확장자랑 상관없이 모든 라이브러리 이름에 의존할 수 있도록, .a 대신 와일드카드 문자를 사용해서 .* 로 치환하면 되겠죠? 다음과 같이 말입니다.

불행히도 이렇게 바꾸고 실행하면, 다음의 에러가 나타납니다.

libMyNetwork.* 파일을 만들 규칙이 없다니, 이게 웬일이람? 싶습니다. 더 이상한 것은 다시 make 를 수행하면 이번에 에러가 없이 targets 까지 모두 잘 빌드됩니다. 응? 하지만 make cleanall 해 준 다음, 다시 make 또는 make liball targets 를 실행하면 동일한 에러가 발생합니다.

이상하죠? 이것은 make가 변수를 정의하거나 함수의 매개변수 안에서는 와일드카드 문자 확장을 하지 않고, * 문자를 실제 문자열의 일부로 간주하기 때문인데요, 일반적으로 선행조건이나 레시피에서 와일드카드 문자를 사용하면, 우리가 보통 아는대로 동작(와일드카드 문자 확장)하게 됩니다. 다음을 보세요.

clean 규칙을 실행하면 레시피에 있는 *.o 는 와일드카드 문자 확장이 일어나서 모든 .o 파일을 가리키게 되어 모든 .o 파일들을 삭제하게 됩니다. 마찬가지로, print 규칙을 실행하면, 선행규칙에 있는 *.c는 확장되어 모든 .c 파일들을 출력합니다. 하지만 $(DEPEND_LIBS:-l%=$(ROOT_LIB_DIR)/$(OBJS_DIR)/lib%.*)의 경우에는 와일드카드 문자 * 가 확장되지 않고 문자열의 일부로 포함되기 때문에, …/libMyNetwork.* 문자열 자체가 선행규칙에 포함되어 버립니다.

첫 번째로 make를 실행할 때, 이 규칙을 parsing하는 시점에는 .a 든 .so 든 아무런 파일도 없기 때문에 그런 파일 없다고 에러를 뱉어내는 거죠. 그렇지만 targets를 링킹할 때 에러가 났지만, 이미 라이브러리 파일 자체는 빌드되었기 때문에, 두 번째로 make를 실행할 때에는 라이브러리 파일이 존재하므로 문제없이 링킹됩니다.

자, 그럼 어떻게 할까요? 제일 쉬운 방법은 (아마도 이럴 때 쓰라고 만들어 둔?) wildcard 함수를 사용하는 겁니다. $(wildcard pattern…) 함수는 makefile안의 어떤 곳에서도 사용될 수 있고요, 주어진 파일 이름 패턴들 중의 하나와 매치되는 현존하는 파일들 이름들의 공백으로 분리된 리스트에 의해서 대체합니다. 그래서 text 에서 pattern 을 replacement로 바꿔주는 $(patsubst pattern,replacement,text) 함수를 아래와 같이 사용하면 원하는 의존성 규칙을 깔끔하게 작성할 수 있습니다.

총정리

휴우, 드디어 총정리의 시간입니다. 다음은 2개의 공용 include 파일들인 IncludePre.mk와 IncludePost.mk 과, 이를 사용하는 Makefile 의 원형인 MakeTemplate.mk 입니다.

어떤 라이브러리를 공유 라이브러리로 만들려면 Makefile에서 다음 항목들을 설정합니다.

  • IS_SHARED = 1
  • MAJOR_VERSION = major_version_of_shard_library
  • MINOR_VERSION = minor_version_of_shard_library

이렇게 설정하고 나면, Out/Library 디렉토리의 Debug 또는 Release 디렉토리에 다음의 파일들이 생성됩니다.

  • real name의 공유 라이브러리 파일: libXXX.so.major_version.minor_version (예: libMyProtocol.so.1.0)
  • soname 에 해당하는  symbolic link 파일: libXXX.so.major_version (예: libMyProtocol.so.1) -> libXXX.so.major_version.minor_version 을 가리킵니다.
  • linker name에 해당하는 symbolic link 파일: libXXX.so (예: libMyProtocol.so) -> libXXX.so.major_version 을 가리킵니다.

그리고 빌드된 공유 라이브러리를 링킹한 응용 프로그램들을 실행하려면, 몇 가지 방법이 있지만 제일 쉬운 방법은 LD_LIBRARY_PATH 환경변수를 공유 라이브러리 파일이 있는 디렉토리로 설정하는 것입니다. 예를 들면 디버그용으로 빌드했다면, 다음과 같이 LD_LIBRARY_PATH 를 export 하고 나서 응용 프로그램을 실행합니다.

공유 라이브러리를 지원하느라, IncludePost.mk 가 좀 복잡해졌지만, 이제는 빌드할 라이브러리의 타입을 정적이든 동적이든 간편하게 지정할 수 있게 되었으니 그 가치는 충분하겠죠? 자, 이제 우리의 영업맨한테 으쓱거리면서, ‘사소한 거’ 다 되었으니 계약이나 빨랑 체결해 오라고 다그치는 일만 남았네요. 개발자한테도 이런 날들이 좀 있어야 하지 않겠어요?

 

이 글과 관련된 글 목록:

This entry was posted in 소프트웨어 and tagged , , , , , , , , , , , , , , , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.