GNU/Linuxの共有ライブラリーのバージョンとアプリケーションバイナリインターフェース(ABI) - 2022-08-08 - ククログ

ククログ

株式会社クリアコード > ククログ > GNU/Linuxの共有ライブラリーのバージョンとアプリケーションバイナリインターフェース(ABI)

GNU/Linuxの共有ライブラリーのバージョンとアプリケーションバイナリインターフェース(ABI)

最近、GNU/Linuxの共有ライブラリーのビルドシステムの改良業務をしている福田です。

共有ライブラリーのバージョンについて考えるには、共有ライブラリーの構成の仕組みと、アプリケーションバイナリインターフェース(ABI) という概念について把握する必要がありました。

普段何となく共有ライブラリーを利用する側としてはあまり意識してきませんでしたが、 共有ライブラリーを作って提供する側にとっては重要な概念です。

GNU/Linuxシステムにおいて、共有ライブラリーのメジャーバージョンは、その他の桁のバージョンに比べて特別な意味を持ちます。 メジャーバージョンが変わることは、アプリケーションバイナリインターフェース(ABI)の互換性がなくなることを示すことが一般的です。

本記事では、共有ライブラリーの構成とABIについて紹介します。

共有ライブラリーはシンボリックリンクとその本体から構成される

共有ライブラリーは、次のようにシンボリックリンクとその本体から構成されます。

libwebp.so -> libwebp.so.6.0.2
libwebp.so.6 -> libwebp.so.6.0.2
libwebp.so.6.0.2

このように、シンボリックリンクには次の2種類があります。

  • バージョンなし: libwebp.so
  • バージョンあり: libwebp.so.6

そしてこの例では、どちらも同じファイルであるlibwebp.so.6.0.2を指しています。

通常のアプリケーションは、バージョンありの方(libwebp.so.6)を用いて動作します。 一方でバージョンなしの方(libwebp.so)は、開発用に用います。

開発段階ではバージョンを意識せずに済む方が便利です。-lwebpというようにライブラリー名を指定するだけでリンクできるからです。バージョンを意識する場合は/usr/lib/x86_64-linux-gnu/libwebp.so.6/usr/lib/x86_64-linux-gnu/libwebp.so.6.0.2といったように目的の共有ライブラリーのパスを指定しないといけません。 一方で共有ライブラリーを使うアプリケーションにとっては、このようにメジャーバージョン毎にシンボリックリンクを分けることがとても重要になってきます。

共有ライブラリーのシンボリックリンクは、メジャーバージョン毎に共存できる

前章の例を見ると、メジャーバージョンがバージョンありのシンボリックリンクの名前(libwebp.so.66の部分)に含まれていることが分かります。

メジャーバージョンが変わらなければ、シンボリックリンクの名前は同じままです。 一方で、メジャーバージョンが変わると、シンボリックリンクの名前も変わります。

これはつまり、共有ライブラリーのシンボリックリンクは、メジャーバージョン毎に共存できるということです。

アプリケーションにとって、共有ライブラリーのメジャーバージョンは他のバージョンとは異なる意味を持っているのです。

アプリケーションバイナリインターフェース(ABI)の互換性を管理する

以上の仕組みは、アプリケーションバイナリインターフェース(ABI)に利用されます。

API(アプリケーションプログラミングインターフェース)が、ソースコードレベルでのインターフェースを指すのに対し、 ABIはバイナリレベルでのインターフェースを指します。

このような共有ライブラリーの構成により、ABIの互換性を上手く管理することができます。

ABIの互換性と共有ライブラリーのメジャーバージョン

アプリケーションバイナリインターフェース(ABI)とは、バイナリレベルでのインターフェースのことです。

ABIの互換性について、具体例で考えてみます。

次のようなシンプルなライブラリーを作ります。

/* hello1.h */
void hello(const char *name);
/* hello1.c */
#include <stdio.h>
#include "hello1.h"

void
hello(const char *name)
{
  printf("Hello %s!\n", name);
}

これを共有ライブラリーとしてビルド1し、シンボリックリンクを作成します。

$ cc -shared -olibhello.so.1 -Wl,-soname,libhello.so.1 hello1.c
$ ln -s libhello.so.1 libhello.so

# 次のような共有ライブラリーの構成になります
libhello.so -> libhello.so.1*
libhello.so.1*

この共有ライブラリーを用いるアプリケーションを作ります。

/* app1.c */
#include "hello1.h"

int
main(void)
{
  hello("Me");
  return 0;
}

ビルドして実行します。 開発用のシンボリックリンクとしてバージョンなしのlibhello.soを用意してあるので、-lhelloで指定できて便利ですね。

$ cc -oapp1 -Wl,-rpath=$PWD app1.c -L. -lhello
$ ./app1
Hello Me!

無事Hello Me!と表示されました。

次に、このライブラリーを機能拡張してみます。ファイル名はhello2にしておきます。

/* hello2.h */
/* 引数の数が増えているのでABIが違う!(APIも違う) */
void hello(const char *name, const char *message);
/* hello2.c */
#include <stdio.h>
#include "hello2.h"

void
hello(const char *name, const char *message)
{
  printf("Hello %s! %s\n", name, message);
}

引数の数が増えているので、ABIとAPIの双方とも異なっています。 ここで、機能拡張したhello2ライブラリーを、先ほどと同じ名前の共有ライブラリーにしてみましょう。

# ABIが違うけど共有ライブラリーの名前は同じ(libhello.so.1)にしちゃえ!
$ cc -shared -olibhello.so.1 -Wl,-soname,libhello.so.1 hello2.c

この状態で先ほどのapp1を、ビルドし直さずにそのまま実行してみます。

$ ./app1
Hello Me! _�

このように出力にゴミが混じってしまいます。 関数が削除されたり名前が変わったりした場合などは、クラッシュすることもありえます。

さてここで重要なのは、アプリケーション(app1)はビルドし直していない、ということです。 共有ライブラリーの場合、アプリケーションの実行時に実装が読み込まれます。 そのため、共有ライブラリーが更新されただけで、アプリケーション側が動かなくなることがありえるのです。

このように、アプリケーションのバイナリは変化していませんが、共有ライブラリーのバイナリが変化することで、 アプリケーションが動かなくことがあります。 これがABIの互換性を失ってしまうということです。

この問題を回避するために、機能拡張したhello2を別の共有ライブラリーにしてhello1と共存させてみましょう。

$ cc -shared -olibhello.so.1 -Wl,-soname,libhello.so.1 hello1.c
$ cc -shared -olibhello.so.2 -Wl,-soname,libhello.so.2 hello2.c

# 次のような共有ライブラリーの構成になります
libhello.so -> libhello.so.1*
libhello.so.1*
libhello.so.2*

hello2で動作するアプリケーションapp2を作成します。

/* app2.c: hello2用のアプリケーション */
#include "hello2.h"

int
main(void)
{
  hello("Me", "Yay!");
  return 0;
}

app2をビルドして、app1とそれぞれ動作を確認します。

$ cc -oapp2 -Wl,-rpath=$PWD app2.c libhello.so.2
$ ./app1 # ビルドし直していない!libhello.so.1をhello1.cベースに戻しただけ。
Hello Me!
$ ./app2
Hello Me! Yay!

app1app2の双方とも正常に動作します。

特にapp1の方はビルドし直していないのに、動作が直っています。 ABIの互換性が回復したからです。

このようにABIの互換性を失う場合は共有ライブラリーを分けて共存させることで、互換性を失い動かなくなることを防げます。 新しいバージョンのlibhello.so.2がインストールされても、アプリケーション側がlibhello.so.1を利用し続ければ、 意図せず互換性を失い動かなくなってしまうことはありません。 最新の共有ライブラリーで動作したい別のアプリケーションがあった場合は、libhello.so.2の方を利用できます。

上例では簡単のためlibhello.so.1libhello.so.2は実ファイルでしたが、実際にはこれらもシンボリックリンクとし、 libhello.so.1.5.0libhello.so.2.0.1のようなフルバージョンの入った共有ライブラリーにそれぞれリンクさせます。

このため一般的に、アップデートによりABIの互換性を失う場合は共有ライブラリーのメジャーバージョンを上げ、互換性を保てる場合はメジャーバージョンを変えません。 マイナーバージョンなどについては好みで変更します。 例えば、どのバージョンの共有ライブラリーがインストールされているのかを特定できるようにするために使えます2

まとめ

本記事では、GNU/Linuxの共有ライブラリーのバージョンとアプリケーションバイナリインターフェースについて紹介しました。

共有ライブラリーのメジャーバージョンは他の桁のバージョンと比べて特別な意味を持っており、ABIの互換性を失う場合に上げる必要がある、ということを知って頂けたら幸いです。

クリアコードではこのように業務で学んだことも含めて成果を公開することを重視しています。 業務の成果を公開する職場で働きたい人はクリアコードの採用情報をぜひご覧ください。

  1. gccではなくccコマンドを用います。ccコマンドはシステムのC Compilerです。 GNU/Linuxならccコマンドはgccであることが多いですが、FreeBSDやmacOSならclang等になります。 gccコマンドは通常はGCCがないと動かないので、GCCでコンパイルすることが必須の場合に使う必要があります。 そうでないならば、ccコマンドで十分です。

  2. 参考: http://archive.linux.or.jp/JF/JFdocs/Program-Library-HOWTO/shared-libraries.html