C
Linux
ライブラリ
初心者向け

C言語 ライブラリってなに?外部公開している情報ってなんなの?

はじめに

今回の記事の主題は以下となります。

  1. 外部に公開されるライブラリの情報ってなに?ざっくり解説
  2. 公開情報の確認方法
  3. 公開情報の制限方法

内容としては、こちらの記事の掘り下げとなります。
改めてライブラリって何だっけと公開APIってなんなのよってところからまとめてます

外部に公開されるライブラリの情報とは?

まずライブラリってなんなのよ?

プログラムはmain関数から始まり、mainが呼び出す関数や使用するデータを利用してプログラムの機能を実現します。
main関数から呼ばれる先の関数たち、使用されるデータ達がないと動作しないので、必ずプログラムのどこかに実体があることになります。

program.png

そうやってプログラムを作っていくと、ふと手を抜きたくなるケースが出てきます。
毎回全く同じ関数を使わなければいけないのです。そんな全く同じ関数をまとめる仕組みがライブラリです。

ライブラリ.png

ライブラリを利用するには

ライブラリは、プログラムのコンパイル時にライブラリリンク、もしくはリンクと呼ばれるライブラリとの紐づけを行います。

ライブラリリンクを行うためには、ライブラリ側が以下のことをしている必要があります。

  • 他のプログラムが必要としている関数、データをライブラリが利用可能な状態にしていること

図でいうところの利用されるapllo()やコップを情報公開してあげていないといけないわけですね。これが今回のメインテーマ、外部公開している情報です。
このリンク付けで関数情報が解決すれば、プログラムはライブラリを利用することが可能となります。この時ライブラリの種類によって利用方法が異なります。

静的ライブラリ(.a)

コンパイル時にプログラムが内部にそのまま取り込んでしまう形式のライブラリです。ビルド時にプログラムに取り込まれるため、プログラム実行時にはもうすでに関数が存在している状態になるので、何も考えずにプログラムが使用できます
半面ライブラリを丸ごと取り込むので、サイズが大きくなります

static_lib.png

共有ライブラリ(.so)

コンパイル時にプログラムがそのライブラリ情報だけを覚え、実行時に対象のファイルとの紐づけを行います。(ライブラリをリンクするといった呼ばれ方をします)

shared_lib.png

共有ライブラリの場合、プログラムはライブラリの情報しか持っていない状態なので、プログラムが実行する際に対象のライブラリがリンク出来る状態にする必要があります。

こちらの仕組みはこのようになっています

簡単に言うと、lddコマンドで対象のライブラリパスが出力されてばOK。そうでないならライブラリがないかビルド or 環境変数でパスを通す必要があります。以下のように=>の右側がどこかに紐づいていればOKです。

$ ldd /usr/bin/curl
        linux-vdso.so.1 (0x00007ffe307ac000)
        libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 (0x00007f85cce4e000)
...
        libssl.so.1.1 => /usr/lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007f85cbb4b000)
        libcrypto.so.1.1 => /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007f85cb6d3000)
...
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f85c6c95000)

補足

2018/05/16追記

記事も間違えて使ってしまったことですし、ライブラリの正式名称。この場を借りてまとめさせていただきます。

名称 概要
静的ライブラリ Linux xx.a, Windows xxx.lib プログラムに取り込まれる形式のライブラリ
共有ライブラリ Linux xx.so, Windows xxx.dll プログラム起動時にリンクされるライブラリ
動的ライブラリ Linux dlopen(), Windows LoadLibrary() プログラムが動作中にリンクするライブラリ

xx.a, 色々なプログラムに流動的に取り込まれるので動的ライブラリって感じがしてしまうんですよね。
「動的…あ、違う違う取り込まれ方から来る名称だ」みたいな脳内整理しないとよく間違えます。
…多分自分だけですね。ハイ

外部に公開されるライブラリの情報 外部リンケージ

長々と前振りをしました。ここからが本番です。なんやかんや言いましたが、外部に公開される情報とは何なのか?
プログラム用語でいうと外部リンケージがあるという状態です。(あえてこの記事では公開情報という言い回しをメインで使用します)

C言語の言葉で凄くざっくり書くと、基本はstaticで定義されていない関数、変数です。
こちらCとC++で微妙に考え方が違うので注意。C++はクラスがstaticでないなら、そのローカル変数がstaticでも公開情報となる扱いだそうです。C++だと公開情報にならないstaticとなるstaticの違いを把握すれば良さそうですね。C++についての詳細はこちらを参照ください。この記事ではCの話に絞ります。

この条件だけだと困ること、それはライブラリ内でファイルを跨いで利用した関数は全部公開情報になってしまう!
というわけで、以降は公開情報の確認方法、制限手段の提示を行います。
こちらの記事と一部重複します。

公開情報(外部リンケージ)の確認方法

nmコマンド

nmコマンドで公開情報の確認が出来ます。

例えば以下の場合:

まずプログラム本体を確認。

$nm -D test
                 w __cxa_finalize
                 w __gmon_start__
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U __libc_start_main
                 U memcmp
                 U __printf_chk
                 U publisher_free
                 U publisher_new
                 U publisher_publish
                 U publisher_subscribe
                 U publisher_unsubscribe
                 U puts
                 U __stack_chk_fail

小文字はローカルの関数、詳細は略。
問題は大文字。Uが未解決、つまり共有ライブラリとして実行時にリンクが必要な関数となります。publisher_XXX等は自作関数なので、公開されていなければ使えません。

一方リンクされるライブラリはというとこんな感じ

nm  .libs/libpublisher.so
...
0000000000000f90 t dputil_list_pop
0000000000000f50 t dputil_list_pull
0000000000000f20 t dputil_list_push
0000000000000ee0 t dputil_lock
0000000000000f00 t dputil_unlock
...
                 U pthread_mutex_init@@GLIBC_2.2.5
                 U pthread_mutex_lock@@GLIBC_2.2.5
                 U pthread_mutex_unlock@@GLIBC_2.2.5
                 U __pthread_register_cancel@@GLIBC_2.3.3
                 U __pthread_unregister_cancel@@GLIBC_2.3.3
                 w __pthread_unwind_next@@GLIBC_2.3.3
...
00000000000009b0 T publisher_free
0000000000202090 b publisher_g
0000000000000a00 T publisher_new
0000000000000b20 T publisher_publish
0000000000000a90 T publisher_subscribe
0000000000000ae0 T publisher_unsubscribe
0000000000000910 t register_tm_clones
                 U __sigsetjmp@@GLIBC_2.2.5
                 U __stack_chk_fail@@GLIBC_2.4
0000000000202078 d __TMC_END__

Tが公開情報です。ちゃんとpublisher_xxxがありますね。つまり実行時にlibpublisher.so.0.0.0がリンクできる状態になっていればOKというわけです。pthread_mutex_init等がUになっていますが、こちらは標準関数なので普通にリンクされます。

ちなみに上記ローカル関数にあるdputil_xxx, libpublisher.soに取り込まれていますが実は静的ライブラリの関数だったりします。

$ nm .libs/libdputil.a

dp_util.o:
00000000000000b0 T dputil_list_pop
0000000000000070 T dputil_list_pull
0000000000000040 T dputil_list_push
0000000000000000 T dputil_lock
0000000000000020 T dputil_unlock
                 U _GLOBAL_OFFSET_TABLE_
                 U pthread_mutex_lock
                 U pthread_mutex_unlock

こちらもUになっていないということは、コンパイル時にlibpublisher.soが取り込んでくれたわけですね。

objdumpコマンド

objdumpコマンドでもプログラムの持つ情報をダンプできます。こちらはリンク以外の様々な情報を確認できるため使いこなせれば解析に役立つようです(=アセンブラ読めますに近い話だと思いますが)

objdump -t libpublisher.so.0.0.0

libpublisher.so.0.0.0:     file format elf64-x86-64

SYMBOL TABLE:
...
0000000000000ee0 l     F .text  0000000000000012              dputil_lock
...
0000000000000000       F *UND*  0000000000000000              free@@GLIBC_2.2.5
...
0000000000000ae0 g     F .text  0000000000000032              publisher_unsubscribe
...

こんな感じでgが付いてると公開情報lはローカルUNDは未解決です。細かな言葉の意味はスルーの方向でお願いします。

公開情報(外部リンケージ)の制限方法

--version-scriptによるAPI制限

前の記事同様です。XXX.mapに公開する関数を指定し、-Wl,--version-script,libtimelog.mapをビルドオプションに追加

LDFLAGS+=-Wl,--version-script,libtimelog.map
libtimelog.map
{
  global:
    timetestlog_init;
    timetestlog_store_printf;
    timetestlog_exit;
  local: *;
};

シンプルでわかりやすい半面、設定ファイルを作らなければいけないのが面倒ですかね。

-fvisibility=hidden

-fvisibility=hiddenを指定すると、全関数がまず非公開になります。
その上で必要なものにだけ__attribute__((visibility("default")))を付けて公開しよう!という手法です。
コーディングルールで制御するのが好きな方にはぴったりのやり方ですね。

nmの紹介の際に利用したlibpublisher.soで、
-fvisibility=hidden指定、各公開関数にint __attribute__((visibility("default"))) publisher_new(size_t contents_num)
のように指定してビルドしました。

結果、libdputil.aのAPIがTになっているのが気になりますが、いい感じに制限できています。

nm -D .libs/libpublisher.so.0.0.0
0000000000202098 B __bss_start
                 U calloc
                 w __cxa_finalize
0000000000001210 T dputil_list_pop
00000000000011d0 T dputil_list_pull
00000000000011a0 T dputil_list_push
0000000000001160 T dputil_lock
0000000000001180 T dputil_unlock
0000000000202098 D _edata
00000000002020c0 B _end
0000000000001224 T _fini
                 U free
                 w __gmon_start__
0000000000000a00 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U pthread_mutex_init
                 U pthread_mutex_lock
                 U pthread_mutex_unlock
                 U __pthread_register_cancel
                 U __pthread_unregister_cancel
                 w __pthread_unwind_next
0000000000000c10 T publisher_free
0000000000000c80 T publisher_new
0000000000000da0 T publisher_publish
0000000000000d10 T publisher_subscribe
0000000000000d60 T publisher_unsubscribe
                 U __sigsetjmp
                 U __stack_chk_fail

私の好み

私はコードを書く時に公開/非公開の意識をしなくていいので--version-scriptの方が好きです。
最後にまとめてconfに書けばいいってのが自分にはわかりやすい。

2018/05/20 追記
後はこちらはスクリプトで自動化出来そうな点も好きです。
includeディレクトリを検索してversion-scriptを作成するスクリプトサンプルを作りました。
マクロへの反応がいまいちですが、それなりに利用できると思います。

#!/bin/sh

output_conf_map() {
        #{
        #  global:
        #    function_name;
        # ...
        #  local: *;
        #}

        #only get function list from header file
        HEADER_FUNC_LIST=`grep "[a-zA-Z](" -r $1 | grep -v "@brief" | grep -v "#define" | awk -F"(" '{print $1}' | awk -F " " '{print $NF}'`
        #template
        echo "{"
        echo "  global:"

        #show all function
        for data in $HEADER_FUNC_LIST
        do
                echo "    $data;"
        done

        #template end
        echo "  local: *;"
        echo "};"
}

INCLUDE_LIST=`find . -name include`
for inc_dir in $INCLUDE_LIST
do
        echo $inc_dir
        output_conf_map  $inc_dir
done

感想

まず謝罪から。本記事をまとめるきっかけは、ライブラリ公開制限の記事を投稿してコメントいただいた際に、「なんで共有ライブラリの公開API制限手段には色んな形があるんだろう?そもそも公開って何だろう?人はどうして生きるんだろう?」みたいな禅問答がはじまったのがきっかけでした。そのため、最初はプログラム界隈の言葉で正確に疑問を紐解くことを目指していました。

ただ、この辺りの話をするためにはかなり深堀しないと無理があり、本当に禅問答がはじまったので「私が整理したかったのはライブラリって何だっけ?の話だ」と思いなおしました。
おかげでミドルウェア上位開発者として理解できているのか出来ていないのかふわふわしていた部分がクリアになった気がします。

後ライブラリ周りで苦労することちょこちょこあるけど、確認方法がわかると不安が減りますね。特にOSS周り。ldd, nm超便利

参考

非常に丁寧にメモリの扱い方を説明したサイト。兼「あ、これ深堀したらハマるやつだ」と気付いたきっかけ
コードサイズを聞かれたら | 学校では教えてくれないこと | [技術コラム集]組込みの門 | ユークエスト株式会社

アセンブラから見る関数の扱い
古のテクニックを見せようと思ったら最近の技術の前にあっさり敗北した話 | ありえるえりあ

外部リンケージの定義
https://msdn.microsoft.com/ja-jp/library/k8w8btzz.aspx

C++でのライブラリ共有制限方法
C++形式の共有ライブラリの書き方(gcc編) - Qiita

nmの見方について
nmコマンドでオブジェクトからシンボルのリストを表示 - Qiita

ライブラリ名称
ライブラリ ‐ 通信用語の基礎知識