静的ライブラリで内部シンボルを隠蔽する方法(macOS)


ライブラリを作るとき、ライブラリ外部には公開インターフェースのみを見せて、内部のプライベート関数などを隠しておきたいというのは自然な発想だと思います。

ソースコードレベルに関しては単にヘッダを公開しなければ済む話ですが、場合によってはバイナリ内のシンボルも隠蔽したいケースがあります。ソースコードが単独なら static で問題ないわけですが、ライブラリ内で横断的に利用したい場合はそうもいきません。

# 動的ライブラリの場合

例えば以下のような構成のライブラリ libcounter.dylib があるとします(人工的な例なので内容は適当です)

```implement.h
// 非公開ヘッダ
extern int g_lib_local; // ライブラリ内のみで共有したいグローバル変数
int internal(void);     // ライブラリ内のプライベート関数
```

```implement.c
#include "implement.h"

int g_lib_local = 0;

// 後で比較のために使うstatic関数
static int get() 
{
   return g_lib_local++;
}

int internal(void)
{
    return get();
}
```

```interface.h
// 公開ヘッダ
int increment(void);
void reset(void);
```

```interface.c
#include "implement.h"

int increment(void)
{
    return internal();
}

void reset(void) {
    g_lib_local = 0;
}
```
ライブラリには interface.[ch] と implement.[ch] という2つのコンパイル対象があり、最終的なライブラリとしては interface.h で宣言された increment() と reset() を公開して、g_lib_local とinternal() はライブラリ内でのみ見せたいわけです。
しかし g_lib_local と internal() は定義されている implement.c ではなく、interface.c から利用されるため static にすることができません。このような場合、まず一般的な回答としては visibility 属性を利用します。

```internal.h
extern int __attribute__((visibility("hidden")))  g_lib_local;
int __attribute__((visibility("hidden"))) internal(void);
```

visibility=hidden で宣言されたシンボルはモジュール外に対して非公開の扱いとなります。実際に implement.o と interface.o から dylib を作って nm で状態を確認すると以下のようになります。

```
% nm libcounter.dylib
0000000000000f70 T _increment
0000000000001000 s _g_lib_local
0000000000000fa0 t _get
0000000000000f90 t _internal
0000000000000f80 T _reset
                 U dyld_stub_binder
```

hidden 属性を指定していない default の increment() と reset() が外部に公開(シンボルタイプ大文字)されており、g_lib_local と internal() は非公開(小文字)です。non-global を strip してやれば不要なシンボルも消えて公開インターフェースのみが残ります。

#静的ライブラリの場合

と、ここまではよいのですが、ここで implement.o の内容を確認してみます。

```
% nm implement.o
00000000000000d0 S _g_lib_local
0000000000000010 t _get
0000000000000000 T _internal
```

static関数である get が小文字なのはよいですが、g_lib_local も internal も大文字、つまりシンボルが外から参照できることが分かります。これはなぜかというと、visibility 属性自体はコンパイル時に与えられる値でも、それが実際に効力を発揮するのはリンク時の問題になるからです(そうでなければstaticと同じなので当然ですが)。

動的ライブラリの場合はそれ自体がリンクの結果であり、したがって生成された時点で visibility=hidden 対象の扱いも確定し、不要になったシンボルを非公開とできます。しかし静的ライブラリは単に .o を固めたアーカイブでしかないため、最終的にアプリなり動的ライブラリなりにリンクされるまではシンボルの扱いを保留しなければなりません。

では冒頭で述べたようなライブラリ・プライベートな関数・変数にhiddenは使えないのかというと、実は簡単な解決策があります。

要はリンカのシンボル解決を通っていないことが問題なのであって、リンカを通った .o を作ってやればいいわけです。具体的には ld の -r オプションを使います。

```
% ld -r implement.o interface.o -o merged.o
```

-r オプションは渡されたオブジェクトファイルをマージして新たなオブジェクトファイルを生成します。この時オブジェクトファイル内のシンボルも処理の対象となるため、visibility=hidden が付いていた対象はその時点でリンクが解決したものとみなされて非公開になります。

```
% nm merged.o
0000000000000048 s EH_Frame1
00000000000000b0 s EH_Frame1
0000000000000000 T _increment
0000000000000118 s _g_lib_local
0000000000000030 t _get
0000000000000020 t _internal
0000000000000010 T _reset
0000000000000060 s func.eh
0000000000000088 s func.eh
00000000000000c8 s func.eh
00000000000000f0 s func.eh
```

strip結果

```
0000000000000048 s EH_Frame1
00000000000000b0 s EH_Frame1
0000000000000000 T _increment
0000000000000010 T _reset
0000000000000060 s func.eh
0000000000000088 s func.eh
00000000000000c8 s func.eh
00000000000000f0 s func.eh
0000000000000020 t l001
0000000000000030 t l002
0000000000000118 s l003
```

increment() と reset() だけが残ってあとは匿名化されています。

リンカ等の動作はプラットフォーム毎の差異が大きいと思うので他の環境はまた違ってくると思います。macOSの場合は ld だけ通せば解決するということです。

(まあ通常は最終的なアプリができた段階でstripできれば十分だと思うので.aファイルのライブラリ・プライベートを必要とすることは少ないと思いますが、visibility=hidden の動きを理解する上でも良いのではないかと)