Egloos | Log-in
F/OSS study
F/OSS study
[glibc] 공유 라이브러리 심볼 버전 관리
glibc: 2.10.1


glibc는 공개된 심볼(함수, 변수)들의 호환성을 보장하기 위해 각 심볼마다 버전을 부여한다.
간단한 hello world 프로그램을 gcc로 컴파일한 후에 다음과 같이 실행하면 이를 확인할 수 있다.

$ readelf -s a.out | head

Symbol table '.dynsym' contains 5 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.0 (2)
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.0 (2)
     4: 080484bc     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used

여기서 주의깊게 봐야할 부분은 3번 항목 puts@GLIBC_2.0 부분이다.
원래 소스 코드에서는 printf() 함수를 통해 "Hello world" 문자열을 출력하도록 했지만
컴파일러가 최적화 과정에서 좀 더 가벼운 puts() 함수로 대체하였기 때문에 그냥 printf와 동일하다고 봐도 될 것이다.
여기서 GLIBC_2.0 부분이 바로 심볼 버전에 해당하는 것이며,
puts() 함수는 2.0 버전 이후로 동작이 (좀 더 정확히는 ABI가) 변경되지 않았다는 것을 짐작할 수 있다.

이렇게 심볼마다 버전을 별도로 관리하게 되면 라이브러리가 수정된 이후에도
불필요하게 기존 프로그램을 재컴파일하지 않아도 되므로 기존 프로그램과의 호환성을 더 높일 수 있게된다.

간단한 예제를 하나 생각해 보자.
먼저 다음과 같은 함수를 제공하는 라이브러리를 하나 만든다.

dso-version.c:
int dso_2powerof(int order)
{
  return 1 << order;
}

버전을 지정하기 위해서는 map 파일이 필요하다.

dso-version.map:
LIBDSO_0.1 {
  global: dso_2powerof;
  local: *;
};

여기서는 버전 이름을 LIBDSO_0.1로 정했지만 이름 자체에 특별한 제약은 없다.
이제 이러한 버전이 적용된 공유 라이브러리를 만들려면 다음과 같이 빌드하면 된다.

$ gcc -shared -o libdso.so -fPIC -Wl,--version-script=dso-version.map dso-version.c

이제 이를 사용하는 프로그램을 하나 만들어보자.
사용하는 프로그램 입장에서는 버전에 대해서 고려할 필요가 없으니
다음과 같이 단순히 사용하면 된다. (귀찮으니 헤더 파일은 생략한다...)

dso-user.c:
#include <stdio.h>
#include <stdlib.h>

extern int dso_2powerof(int order);

int main(int argc, char *argv[])
{
  int n = 5, pow;

  if (argc > 1)
    n = strtol(argv[1], NULL, 10);
 
  pow = dso_2powerof(n);
  printf("2 to the power of %d is %d(%#010x)\n", n, pow, pow);
  return 0;
}

출력 결과는 다음과 같다.

$ gcc -o dso-v0.1 dso-user.c -L. -ldso -Wl,-rpath,.
$ ./dso-v0.1
2 to the power of 5 is 32(0x00000020)

dso-v0.1 파일에 기록된 버전 정보를 보려면 다음 명령을 이용하면 된다.

$ readelf -V dso-v0.1

Version symbols section '.gnu.version' contains 8 entries:
 Addr: 00000000080482ec  Offset: 0x0002ec  Link: 6 (.dynsym)
  000:   0 (*local*)       2 (LIBDSO_0.1)    0 (*local*)       0 (*local*)    
  004:   3 (GLIBC_2.0)     3 (GLIBC_2.0)     3 (GLIBC_2.0)     1 (*global*)   

Version needs section '.gnu.version_r' contains 2 entries:
 Addr: 0x00000000080482fc  Offset: 0x0002fc  Link: 7 (.dynstr)
  000000: Version: 1  File: libc.so.6  Cnt: 1
  0x0010:   Name: GLIBC_2.0  Flags: none  Version: 3
  0x0020: Version: 1  File: libdso.so  Cnt: 1
  0x0030:   Name: LIBDSO_0.1  Flags: none  Version: 2

버전 정보는 크게 두 가지로 나뉘는데
각 심볼에 따른 버전 정보는 .gnu.version 섹션에 저장되며
단순히 각 dynamic symbol에 해당하는 버전 정보를 정수값으로 저장한다.
위의 예제에서 총 8개의 심볼이 있는데 그 중 두 번째 항목이 LIBDSO_0.1 버전에 해당하는
정수값 2를 저장하고 있다. 이를 아래와 같이 심볼 테이블과 연관시켜보면 의미가 확실해 질 것이다.

$ readelf -s dso-v0.1 | head -12

Symbol table '.dynsym' contains 8 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND dso_2powerof@LIBDSO_0.1 (2)
     2: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     3: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     4: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.0 (3)
     5: 00000000     0 FUNC    GLOBAL DEFAULT  UND strtol@GLIBC_2.0 (3)
     6: 00000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.0 (3)
     7: 080485ec     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used

이러한 심볼의 버전을 제공하는 라이브러리에 대한 정보는 .gnu.version_r 섹션에 저장되며
위의 예제에서는 GLIBC_2.0 버전을 제공하는 libc.so.6 (정수값 3)과
LIBDSO_0.1 버전을 제공하는 libdso.so (정수값 2) 의 두 라이브러리가 필요하다.

이렇게 한 동안 프로그램을 잘 사용하고 있었는데 어느날 버그를 발견했다.
dso_2powerof() 함수의 인자로 음수값이 들어가면 정의되지 않은(undefined) 동작이 발생할 수 있다.
gcc의 경우에는 rotate/shift right와 같이 동작하지만 이러한 특성에 의존하는 것은 올바르지 않다.

$ ./dso-v0.1 -5
2 to the power of -5 is 134217728(0x08000000)

그렇다면 라이브러리를 수정할 필요가 있다.
몇 가지 방법이 있을 수 있겠지만 일단 단순히 음수가 들어오는 경우에는 0을 반환하기로 하자.
이제 수정된 버전을 0.2라고 하면 libdso.so는 0.1과 0.2 버전을 모두 제공해야 하고
새로 링크되는 프로그램들은 기본적으로 0.2 버전을 사용하도록 알려줘야 한다.
이를 위해서는 다음과 같이 소스가 약간 지저분하게 변경되어야 한다.

dso-version.c:
int dso_2powerof_v2(int order)
{
  if (order < 0)
    return 0;
  return 1 << order;
}

extern int dso_2powerof_v1(int)
  __attribute__((alias("dso_2powerof_v2")));

asm (".symver dso_2powerof_v1,dso_2powerof@LIBDSO_0.1");
asm (".symver dso_2powerof_v2,dso_2powerof@@LIBDSO_0.2");

가장 중요한 부분은 맨 아래의 두 줄이다.
.symver라는 어셈블리 directive를 이용하여 버전을 지정할 수 있다.
자세히 보면 0.2 버전의 경우 @ 마크가 2개 붙어있는데 이는 0.2 버전이 기본적으로 사용된다는 것을 의미한다.

하지만 버전을 지정하려면 기존의 함수 이름을 변경해야 한다.
따라서 함수 이름 뒤에 (0.2 버전을 의미하는) _v2를 붙였다. (물론 아무 이름이나 선택해도 된다.)
0.2 버전의 경우에는 0.1 버전의 구현과 호환되기 때문에 예전 버전에서도 동일하게 동작하기 위해서
단순히 0.1 버전의 구현을 0.2 버전의 alias로 처리했다. (똑같은 함수를 두 번 구현할 필요가 없다!)

버전을 지정하는 map 파일도 마찬가지로 변경되어야 한다.

dso-version.map:
LIBDSO_0.1 {
  global: dso_2powerof;
  local: *;
};

LIBDSO_0.2 {
  global: dso_2powerof;
} LIBDSO_0.1;

LIBDSO_0.2 항목을 추가하여 dso_2powerof() 함수의 0.2 버전이 제공된다는 것을 지정한다.
LIBDSO_0.2는 LIBDSO_0.1의 다음 버전 임을 알리기 위해 마지막에 이전 버전을 적고
local 목록은 중복되지 않도록 LIBDSO_0.1 한 곳에만 적어준다.
(어차피 이들은 외부로 공개되지 않기 때문에 버전도 적용되지 않는다.)

이제 라이브러리와 프로그램을 다시 빌드하면 0.2 버전을 사용하도록 지정될 것이다.

$ gcc -shared -o libdso.so -fPIC -Wl,--version-script=dso-version.map dso-version.c
$ gcc -o dso-v0.2 dso-user.c -L. -ldso -Wl,-rpath,.
$ readelf -V dso-v0.2

Version symbols section '.gnu.version' contains 8 entries:
 Addr: 00000000080482ee  Offset: 0x0002ee  Link: 6 (.dynsym)
  000:   0 (*local*)       0 (*local*)       0 (*local*)       2 (GLIBC_2.0)  
  004:   3 (LIBDSO_0.2)    2 (GLIBC_2.0)     2 (GLIBC_2.0)     1 (*global*)   

Version needs section '.gnu.version_r' contains 2 entries:
 Addr: 0x0000000008048300  Offset: 0x000300  Link: 7 (.dynstr)
  000000: Version: 1  File: libdso.so  Cnt: 1
  0x0010:   Name: LIBDSO_0.2  Flags: none  Version: 3
  0x0020: Version: 1  File: libc.so.6  Cnt: 1
  0x0030:   Name: GLIBC_2.0  Flags: none  Version: 2

이제 프로그램을 음수 인자를 주어서 실행해보면 0이 반환됨을 알 수 있다.
0.2 버전을 이용하여 빌드한 새 프로그램은 물론이고
예전에 0.1 버전을 이용하여 빌드한 프로그램(dso-v0.1)도 역시 적용된다.

$ ./dso-v0.1 -5
2 to the power of -5 is 0(0000000000)

여기까지는 심볼 버전 관리의 장점이 크게 느껴지지 않는다.
하지만 함수의 기본형이 변경되는 (즉 더 이상 예전 버전과 호환되지 않는) 수정이 필요한 경우에는
라이브러리의 soname을 변경하여 전체 프로그램을 다시 빌드하지 않고도
해당 심볼의 여러 버전을 제공하여 호환성을 유지할 수 있다.

위의 예제에서 함수의 인자로 int형이 표현할 수 있는 비트 범위보다 큰 값이 들어오는 경우는 고려하지 않았다.
아마 이러한 동작도 undefined behavior일텐데 이에 대한 에러 처리가 필요하다.
0.2 버전에서 고려한 음수의 경우도 있고 해서 다음과 같이 구현을 수정하기로 하고 버전은 0.3으로 지정한다.

dso-version.c:
int dso_2powerof_v3(int order, int *result)
{
  if (order < 0 || order >= 8 * sizeof(int))
    return -1;
 
  if (result)
    *result = 1 << order;
 
  return 0;
}

int dso_2powerof_v2(int order)
{
  if (order < 0)
    return 0;
  return 1 << order;
}

extern int dso_2powerof_v1(int)
  __attribute__((alias("dso_2powerof_v2")));

asm (".symver dso_2powerof_v1,dso_2powerof@LIBDSO_0.1");
asm (".symver dso_2powerof_v2,dso_2powerof@LIBDSO_0.2");
asm (".symver dso_2powerof_v3,dso_2powerof@@LIBDSO_0.3");

map 파일은 0.2 버전의 경우와 동일하게 0.3 버전을 위한 항목을 만들면되므로 설명은 생략한다.
이제 프로그램도 변경된 API를 따라서 변경되어야 한다.

dso-user.c:
#include <stdio.h>
#include <stdlib.h>

extern int dso_2powerof(int order, int *result);

int main(int argc, char *argv[])
{
  int n = 5, pow;

  if (argc > 1)
    n = strtol(argv[1], NULL, 10);
 
  if (dso_2powerof(n, &pow) < 0)
    printf("dso_2powerof: invalid argument: %d\n", n);
  else
    printf("2 to the power of %d is %d(%#010x)\n", n, pow, pow);
 
  return 0;
}

이제 라이브러리와 프로그램을 다시 빌드하면 예전 버전과 최신 버전 모두 잘 동작한다.

$ gcc -shared -o libdso.so -fPIC -Wl,--version-script=dso-version.map dso-version.c
$ gcc -o dso-v0.3 dso-user.c -L. -ldso -Wl,-rpath,.
$ ./dso-v0.1 -5
2 to the power of -5 is 0(0000000000)
$ ./dso-v0.2 32
2 to the power of 32 is 1(0x00000001)
$ ./dso-v0.3 -5
dso_2powerof: invalid argument: -5
$ ./dso-v0.3 32
dso_2powerof: invalid argument: 32
$ ./dso-v0.3
2 to the power of 5 is 32(0x00000020)


=== 참고 문헌 ===

by namhyung | 2010/02/22 02:08 | System | 트랙백 | 덧글(0)
트랙백 주소 : http://studyfoss.egloos.com/tb/5254916
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶

카테고리
General
Application
System
Kernel
Book
Tips
태그
sed linux git patch C CARM SMP glibc vcs scm compiler build emacs blktrace gcc script synchronization binutils bash documentation computer-architecture perf x86 CAaQA3 elf memory algorithm block-layer awk kernel
전체보기
이글루 파인더

최근 등록된 덧글
informsi yang bagus dan be..
by pordanaia at 08/05
Informsi yang bagus dan b..
by jamalsu at 08/01
as inforasi yang menarik http..
by jamalsu at 07/22
최근 등록된 트랙백
Tod's Ferrari Homme
by Tods Pas Cher,Kodak did ..
Mocassin Femme
by Mocassins Homme, I got so..
natural garcinia cambogia
by
rss

skin by jiinny


X