2007/02/05(Mon)
○ 共有ライブラリの後方互換性が壊れる時
やっぱり月末は(以下略)ということで、遅くなりましたが続きです。
# いきなり第3回(最終回)のつもりでしたが、やっぱり一回間を挟みます。
@後方互換とは
共有ライブラリの後方互換性とは、古い共有ライブラリを新しい共有ライブラリで置き換えても
リンクし直す事無しに完全に動作することを保障するということです。
ではどのような変更を行うとこの後方互換が損なわれるのでしょうか?
- 関数や変数(=シンボル)が共有ライブラリから無くなる
この記事の最後の例の通り、リンクローダが未定義シンボルを検出するとプログラムの実行を強制終了するので
わりと発覚しやすい問題です、ただしこれまで無視されてた同名シンボルが昇格した場合は厄介です。 *1 - 関数の引数などのインタフェースが変わる場合
Cでは言語仕様上、foo(int)とfoo(char *)は同時に宣言できないので
foo(int)もfoo(char *)も同じfooシンボルです、よってリンクローダではこの変更を検出できません。 *2foo_print.c
#include <stdio.h> #include <string.h> void foo_print(const char *s) { printf("%s\n", s); }
これをlibfoo.so.0としてコンパイルします。
$ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_print.c $ ln -sf libfoo.so.0 libfoo.so
このfoo_print()を呼び出すアプリケーションを用意します。
extern void foo_print(const char *); int main(void) { foo_print("hello, world."); return 0; }
コンパイルして実行します。
$ gcc -fstack-protector -Wl,-rpath,. -L. -lfoo -o test_foo main.c $ ./test_foo hello, world.
次にfoo_print()の仕様を変更し、引数の数を増やします。
--- foo_print.c.orig 2007-01-28 23:35:45.000000000 +0900 +++ foo_print.c 2007-01-28 23:43:50.000000000 +0900 @@ -2,7 +2,7 @@ #include <string.h> void -foo_print(const char *s) +foo_print(int num, const char *s) { - printf("%s\n", s); + printf("%d: %s\n", num, s); }
libfoo.so.0のみを作り直します。
$ rm -f libfoo.so libfoo.so.0 $ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_print.c $ ln -sf libfoo.so.0 libfoo.so
それではtest_fooを再コンパイルせずに実行してみましょう。
当然ですが表示結果は予期しないものになります。$ ./test_foo 134514549: @鄂
場合によってはSegmentation faultなどのエラーも発生するでしょう。 - 定数値が変更された場合
foo.h
foo_init.c#ifndef _FOO_H_ #define _FOO_H_ #define FOO_LEN 32 extern void foo_init(char *); #endif /*_FOO_H_*/
#include <stdio.h> #include <string.h> #include "foo.h" void foo_init(char *s) { printf("%s: FOO_LEN expected %d.\n", __func__, FOO_LEN); memset((void *)s, 0, FOO_LEN); }
これをlibfoo.so.0としてコンパイルします。
$ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_init.c $ ln -sf libfoo.so.0 libfoo.so
このFOO_LENとfoo_init()を使うアプリケーションを用意しましょう。
main.c#include <stdio.h> #include "foo.h" int main(void) { char s[FOO_LEN]; printf("%s: FOO_LEN is %d.\n", __func__, FOO_LEN); foo_init(s); return 0; }
コンパイルして実行します。
当然ですが問題なく動作します。$ gcc -fstack-protector -Wl,-rpath,. -L. -lfoo -o test_foo main.c $ ./test_foo main: FOO_LEN is 32. foo_init: FOO_LEN expected 32.
では仕様変更が発生し、FOO_LENを変更するとどんな結果になるでしょうか?
--- foo.h.orig 2007-02-04 21:20:59.921875000 +0900 +++ foo.h 2007-02-04 21:21:26.328125000 +0900 @@ -1,7 +1,7 @@ #ifndef _FOO_H_ #define _FOO_H_ -#define FOO_LEN 32 +#define FOO_LEN 128 extern void foo_init(char *); #endif /*_FOO_H_*/
libfoo.so.0のみを作り直します。
$ rm -f libfoo.so libfoo.so.0 $ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_init.c $ ln -sf libfoo.so.0 libfoo.so
それではtestを実行してみましょう。
gccのスタック保護機能により強制終了されてしまいました。$ ./test_foo [NetBSDの場合] main: FOO_LEN is 32. foo_init: FOO_LEN expected 128. Abort (core dumped) [Linuxの場合] main: FOO_LEN is 32. foo_init: FOO_LEN expected 128. *** stack smashing detected ***: ./test_foo terminated Aborted
これは、main側ではFOO_LEN = 32byteスタック上にメモリを確保し、先頭ポインタをfoo_initに渡しますが
コンパイルし直したfoo_init()側ではFOO_LEN = 128byteまでmemsetしようとして
スタックオーバーランが発生したからです。 - 型、構造体、配列などのsizeofが変わるケース
こちらもsizeof()が関数だと思っている人がやってしまいがちなミスです。
以下のサンプルを3.と同様の手順で実行すると、同じくスタック破壊が発生します。
foo.h
foo_init.c#ifndef _FOO_H_ #define _FOO_H_ struct foo { int foo_len; char foo_buf[32]; }; extern void foo_init(struct foo *); #endif /*_FOO_H_*/
main.c#include <stdio.h> #include <string.h> #include "foo.h" void foo_init(struct foo *p) { printf("%s: sizeof(*p) expected %d.\n", __func__, sizeof(*p)); memset((void *)p, 0, sizeof(*p)); }
#include <stdio.h> #include "foo.h" int main(void) { struct foo f; printf("%s: sizeof(f) is %d.\n", __func__, sizeof(f)); foo_init(&f); return 0; }
仕様変更
--- foo.h.orig 2007-01-25 00:32:24.000000000 +0900 +++ foo.h 2007-01-25 00:56:22.000000000 +0900 @@ -3,7 +3,7 @@ struct foo { int foo_len; - char foo_buf[32]; + char foo_buf[128]; };
#他にもobject formatの変更、特定のCPUに依存した最適化(gcc -mcpu=pentiumpro)などにより
#後方互換が失われるケースもありますがここでは触れません。
@非互換性を回避する
1.および2.の場合、そもそも消さずに互換性の為にそのまま残しておくという方法が一般的です。
*BSDでは、obsoleteな関数をリンクした時に警告が発せられる仕組みが用意されています。
この機能を試してみましょう。
例えばmktemp(3)はヘッダからこそまだ消されてませんが、安全性の問題からmkstemp(3)の使用が推奨されます。
NetBSDではmktemp(3)を呼び出すプログラムをコンパイルすると
cc -O2 -o hoge hoge.c
/var/tmp//ccy1ldHe.o: In function `main':
hoge.c:(.text+0x17): warning: warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp()
のように警告が発せられます。
この警告は__warn_referencesマクロによって実現されています。
cdefs_elf.h
73 #define __warn_references(sym,msg) \
74 __asm(".section .gnu.warning." #sym "\n\t.ascii \"" msg "\"\n\t.text");
75
59 __warn_references(mktemp,
60 "warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp()")
61
62 char *
63 mktemp(path)
64 char *path;
インラインアセンブラをつかって警告メッセージを埋め込んでいます。
glibcの場合はlink_warningマクロです。
libc-symbol.h
242 # define link_warning(symbol, msg) \
243 __make_section_unallocated (".gnu.warning." #symbol) \
244 static const char __evoke_link_warning_##symbol[] \
245 __attribute__ ((used, section (".gnu.warning." #symbol __sec_comment ))) \
246 = msg;
3.および4.については非常に厄介です。
予防という意味では、3.のケースであればfoo_init()を
foo_init.c
#include <stddef.h>
#include <string.h>
void
foo_init(char *s, size_t n)
{
printf("%s: n is %zd.\n",
__func__, n);
memset((void *)s, 0, n);
}
main.c
#include <stdio.h>
#include "foo.h"
int
main(void)
{
char s[FOO_LEN];
printf("%s: FOO_LEN is %d.\n",
__func__, FOO_LEN);
foo_init(s, sizeof(s));
return 0;
}
のように定数を使わないようなI/F設計にする、あるいは4.のケースであれば
foo.h
#ifndef _FOO_H_
#define _FOO_H_
struct foo;
extern struct foo *foo_init(void);
foo_init.c
#include <stdio.h>
#include <string.h>
#include "foo.h"
struct foo {
int foo_len;
char foo_buf[32];
};
struct foo *
foo_init()
{
struct foo *p;
printf("%s: sizeof(*p) expected %d.\n",
__func__, sizeof(*p));
p = malloc(sizeof(*p));
if (p != NULL)
memset((void *)p, 0, sizeof(*p));
return p;
}
main.c
#include <stdio.h>
#include "foo.h"
int
main(void)
{
struct foo *f;
f = foo_init();
return 0;
}
のように、malloc(3)を使って動的なメモリ確保を行うべきでしょう。
しかし定数やsizeofに依存したI/F設計になっている関数が既に存在している場合はどうでしょうか?
例えばC90AMD1で追加されたmbrtowc(3)やwcrtomb(3)の場合などです。
#include <locale.h>
#include <wchar.h>
int
main(void)
{
mbstate_t st;
const char *s = "あ";
size_t n = strlen(s), ret;
wchar_t wc;
char buf[MB_LEN_MAX];
setlocale(LC_CTYPE, "ja_JP.eucJP");
memset(&st, 0, sizeof(st));
ret = mbrtowc(&wc, s, n, &st);
...
memset(&st, 0, sizeof(st));
ret = wcrtomb(buf, wc, &st);
...
}
wcrtomb(3)はワイド文字wcをマルチバイトに変換してbufに書き込みますが
このbufの長さについては最低MB_LEN_MAXを呼出元が確保することになっています。
OpenBSDではMB_LEN_MAXはほとんどのarchでは1です。
*3
include/machine/limits.h
#define MB_LEN_MAX 1 /* no multibyte characters */
将来的にEUC-JPやUTF-8などのマルチバイト文字をサポートする為に
この値を増やそうとすれば3.のケースに当てはまり、後方互換性は失われます。
またCygwin/newlibではmbstate_tに4バイトまでしか解析中のマルチバイトを保持できません。
include/sys/_types.h
/* Conversion state information. */
typedef struct
{
int __count;
union
{
wint_t __wch;
unsigned char __wchb[4];
} __value; /* Value so far. */
} _mbstate_t;
ですのでUTF-8の5~6バイト文字は正しく扱えません。
newlib/libc/stdlib/mbtowc_r.c
198 /* five-byte sequence */
199 if (sizeof(wchar_t) < 4)
200 return -1; /* we can't store such a value */
よってmbstate_tのsizeofを変えようとするとこれまた4.のケースで後方互換が失われることでしょう。
このような場合、原則的には共有ライブラリのSONAMEを変更します。
共有ライブラリのバージョン管理は
- real name
この共有ライブラリのファイル名 - sonameこの共有ライブラリを作成した際にld(1)の-sonameオプションで指定した名前。
real name != sonameの場合、symlinkを貼ること。$ gcc -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0.0 foo.c $ ls -all libfoo.so.0.0 -rwxr-xr-x 1 tnozaki tnozaki 4467 Feb 5 21:30 libfoo.so.0.0 $ ln -sf libfoo.so.0.0 libfoo.so.0 $ ls -all libfoo.so.0 lrwxr-xr-x 1 tnozaki tnozaki 13 Feb 5 21:35 libfoo.so.0 -> libfoo.so.0.0 $ objdump --all-headers libfoo.so.0.0 | grep SONAME SONAME libfoo.so.0
- linker name
この共有ライブラリをリンクする為にld(1)の-lオプションに指定する名前
linker name != sonameの場合、symlinkを貼ること。$ ln -sf libfoo.so.0 libfoo.so $ ls -all libfoo.so lrwxr-xr-x 1 tnozaki tnozaki 13 Feb 5 21:36 libfoo.so -> libfoo.so.0.0 $ gcc -Wl,-rpath=. -L. -lfoo -o test_foo test_foo.c $ ldd test_foo test_foo: -lfoo.0 => ./libfoo.so.0 -lc.12 => /usr/lib/libc.so.12
で行います。
後方互換性を失った新しいlibfooのsonameをlibfoo.so.1に変更すれば
libfoo.so.0*とlibfoo.so.1*はファイルシステム上共存可能です。
再コンパイルする前の古いアプリケーションははlibfoo.so.0をそのまま参照し
新しくコンパイルしたバイナリはlibfoo.so.1をリンクするので干渉することはないはずです。
しかしGnomeやKDEなどの大量の、そして複雑な依存関係を持つ共有ライブラリの現代では
迂闊にSONAMEの変更するととんでもないトラブルが発生しかねないのです。
次回はそのトラブルと、それぞれのOSによって異なる回避策の予定です。
○ localedef(1)
ぼちぼち資料集め。
ISO/IEC TR14652
ポイントは
- charmapでISO-2022を扱う為に<escseq2022>、<include>を追加
- glibcで%aliasで行っていたcharmapの別名指定と同等の機能を<addset>として追加
- __STDC_ISO10646__との整合性の為に、シンボル名とUCS4のマッピングをするテーブルを
<repertoiremap>で指定できるように、glibcでの%repertoiremapと同等。
あたりかな。LC_*が増えてるのはどうでもいい。
とりあえずmklocale(1)捨ての為に、LC_CTYPEだけでも実装してみようかね。
<escseq2022>、<include>あたりはCitrus的に微妙だよな。
<escseq${encoding_module}> "param1";"param2";"escape sequence";
とでもして、他のstateful encodingの事も考慮しないとなぁ。。。
NetBSDのfparseln(3)マンセー、知らずに車輪の再発明をしてたのは内緒だ。