[Linux] Dynamic Link Library の話 その1、あなたのプログラムはどの *.so を使ってるの?

早いもので2月も終わり、3月に入りました. 2022年も6分の1が過ぎました.

今日は春を思わせるような陽気、公園の梅の花も咲き始めてきました.

さて、先日仕事で開発したプログラムがどんなDynamic Link Library (*.so) (以降は共有ライブラリと書きます) を使っているのか調べる機会がありましたのでその話を書きます.

自分が開発しているプログラムはどんな *.so を使っているの?

仕事ではLinuxが動作する組込み機器の開発を行ってますが, その機器にインストールされてる Dynamic Link Libraryのひとつがバージョンアップされたことに伴い、自分が開発したプログラムがそれを使っているのかどうかの確認が必要でした.

そこで同僚と使っている共有ライブラリを調べる方法は?という話となり、ちょっと調べてみました.

プログラム例として

iptables などで知られるThe netfilter.org project の成果物のひとつ、libnetfilter_conntrack のサンプルとして以下のように公開されているプログラムを例にします.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <arpa/inet.h>
#include <libmnl/libmnl.h>
#include <libnetfilter_conntrack/libnetfilter_conntrack.h>

static int data_cb(const struct nlmsghdr *nlh, void *data)
{
	struct nf_conntrack *ct;
	char buf[4096];
	ct = nfct_new();
	if (ct == NULL)
		return MNL_CB_OK;

	nfct_nlmsg_parse(nlh, ct);
	nfct_snprintf(buf, sizeof(buf), ct, NFCT_T_UNKNOWN, NFCT_O_DEFAULT, 0);
	printf("%s\n", buf);
	nfct_destroy(ct);

	return MNL_CB_OK;
}

int main(void)
{
	struct mnl_socket *nl;
	struct nlmsghdr *nlh;
	struct nfgenmsg *nfh;
	char buf[MNL_SOCKET_BUFFER_SIZE];
	unsigned int seq, portid;
	int ret;

	nl = mnl_socket_open(NETLINK_NETFILTER);
	if (nl == NULL) {
		perror("mnl_socket_open");
		exit(EXIT_FAILURE);
	}

	if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
		perror("mnl_socket_bind");
		exit(EXIT_FAILURE);
	}
	portid = mnl_socket_get_portid(nl);

	nlh = mnl_nlmsg_put_header(buf);
	nlh->nlmsg_type = (NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET;
	nlh->nlmsg_flags = NLM_F_REQUEST|NLM_F_DUMP;
	nlh->nlmsg_seq = seq = time(NULL);

	nfh = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg));
	nfh->nfgen_family = AF_INET;
	nfh->version = NFNETLINK_V0;
	nfh->res_id = 0;

	ret = mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);
	if (ret == -1) {
		perror("mnl_socket_recvfrom");
		exit(EXIT_FAILURE);
	}

	ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
	while (ret > 0) {
		ret = mnl_cb_run(buf, ret, seq, portid, data_cb, NULL);
		if (ret <= MNL_CB_STOP)
			break;
		ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
	}
	if (ret == -1) {
		perror("mnl_socket_recvfrom");
		exit(EXIT_FAILURE);
	}

	mnl_socket_close(nl);
	return 0;
}

上のソースコードを以下のようにビルドして nfct-mnl-dump というバイナリを生成.

共有ライブラリは libnetfilter_conntrack.so libmnl.so を利用します.

$ gcc nfct-mnl-dump.c -o nfct-mnl-dump -lnetfilter_conntrack -lmnl -L/usr/local/lib

elfファイルから参照する共有ライブラリを調べる.

file コマンドで生成したプログラム nfct-mnl-dump を調べます.

$ file nfct-mnl-dump
nfct-mnl-dump: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=0d96297c0cd79ba0454b6c61b392336f59c14c60, not stripped

ARM CPUを搭載した Chromebook を使ってビルドしたため, 生成されたプログラムは ARM aarch64 アーキテクチャの ELF 64-bit フォーマットのファイルです.

全くの余談ですが、このブログを書くのに使っている Chromebook の話はこちら

ELFファイルにはdynamic sectionがあり, readelf コマンドでそのセクションを見ることができます.

$ readelf -d nfct-mnl-dump
Dynamic section at offset 0x1da8 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libnetfilter_conntrack.so.3]
0x0000000000000001 (NEEDED) Shared library: [libmnl.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

readelf -d によってプログラム nfct-mnl-dump は libnetfilter_conntrack.so.3, libmnl.so.0, libc.so.6 を参照しているということが分かります.

これで対象のプログラム nfct-mnl-dump が使っている共有ライブラリが全て表示されるのか?というと…そうでないケースもあります.

ELFファイルのDynamic Sectionから見えない共有ライブラリ?とは

先の readelf -d によってプログラム nfct-mnl-dump が必要とする共有ライブラリのひとつが libnetfilter_conntrack.so.3 と表示されました.

その libnetfilter_conntrack.so.3 を file コマンドにかけてみたのが以下の結果

$ file /usr/local/lib/libnetfilter_conntrack.so.3.8.0
/usr/local/lib/libnetfilter_conntrack.so.3.8.0: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=613f28375014193f0c08e8440116f0521abaaf76, with debug_info, not stripped

libnetfilter_conntrack.so.3 もまた ELFファイルであり, その dynamic section をreadelf -d で見ると

$ readelf -d /usr/local/lib/libnetfilter_conntrack.so.3.8.0

Dynamic section at offset 0x1adc8 contains 21 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libnfnetlink.so.0]
0x0000000000000001 (NEEDED) Shared library: [libmnl.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libnetfilter_conntrack.so.3]

共有ライブラリ libnetfilter_conntrack.so.3 もまた他の共有ライブラリ libmnl.so.0libnfnetlink.so.0 を必要としていることが分かります.

対象のプログラム nfct-mnl-dump は libmnl.so.0 も直接必要としているのは先の readelf -d の結果から分かりますが, libnfnetlink.so.0 が最終的に必要だったとは nfct-mnl-dump を readelf -d を実行した結果からは分かりません.

すなわち,

プログラムが必要としている共有ライブラリAが
さらに別の共有ライブラリBを参照した場合,
プログラムファイルの ELF の Dynamic Section からは

共有ライブラリBの存在が分からない

ということです.

必要とする共有ライブラリをもれなく知る方法・その1 /proc/PID/maps を見る

メモリ上でプログラムと共有ライブラリがリンクされた結果を見るのが一番確実な方法かと.

共有ライブラリを使うプログラムを実行時、プログラムがメモリ上にロードされる際に必要な共有メモリもメモリ上にロードされ、メモリ上でリンクされます.

実行されるプログラムのプロセスIDを PID とした場合,

/proc/PID/maps

にそのメモリマップが表示されます.

以下はemacs (Process ID 568) のメモリマップの抜粋です.

$ cat /proc/568/maps
7268c53000-726bb6d000 r-xp 00000000 00:2c 54619 /usr/lib/aarch64-linux-gnu/libLLVM-7.so.1
726bb6d000-726bb82000 —p 02f1a000 00:2c 54619 /usr/lib/aarch64-linux-gnu/libLLVM-7.so.1
726bb82000-726bf73000 r–p 02f1f000 00:2c 54619 /usr/lib/aarch64-linux-gnu/libLLVM-7.so.1
726bf73000-726bf9f000 rw-p 03310000 00:2c 54619 /usr/lib/aarch64-linux-gnu/libLLVM-7.so.1

メモリマップ上にリンクされた共有ライブラリを見ることができます.


ただ, 常駐するプロセスであればプロセスIDを調べて /proc/PID/mapsを読むこともできますが、一瞬にして終了するような, たとえば ls コマンド等はプロセスiDを調べることが困難.

全てのプログラムでこの方法が使えるわけではありません.

ではそういう実行してすぐ終了するプログラムが使用している共有ライブラリを調べるにはどうするのか?

必要とする共有ライブラリをもれなく知る方法・その2 /usr/bin/ldd を使って使用する共有ライブラリを調べる

プログラムと共有ライブラリの動的依存関係を調べるには /usr/bin/ldd を使う方法があります.

先のプログラム nfct-mnl-dump に対して ldd コマンドを実行した結果.

$ ldd -d ./nfct-mnl-dump
linux-vdso.so.1 (0x000000757b954000)
libnetfilter_conntrack.so.3 => /usr/lib/aarch64-linux-gnu/libnetfilter_conntrack.so.3 (0x000000757b8da000)
libmnl.so.0 => /lib/aarch64-linux-gnu/libmnl.so.0 (0x000000757b8c4000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x000000757b752000)
libnfnetlink.so.0 => /usr/lib/aarch64-linux-gnu/libnfnetlink.so.0 (0x000000757b73b000)
/lib/ld-linux-aarch64.so.1 (0x000000757b926000)

ELF ファイルの Dynamic section では見えなかった libnfnetlink.so.0 に対する依存関係も表示されています.

/usr/bin/ldd は動的なリンカー/ローダーである /lib/ld.so を実行するシェルスクリプトであることはあまり知られてないかもしれません.

以下は私の使っている環境(ARM CPU の Chromebook)にインストールされている /usr/bin/ldd の抜粋です.

#! /bin/bash
# Copyright (C) 1996-2018 Free Software Foundation, Inc.
# This file is part of the GNU C Library.

# The GNU C Library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# The GNU C Library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with the GNU C Library; if not, see
# <http://www.gnu.org/licenses/>.

# This is the `ldd' command, which lists what shared libraries are
# used by given dynamically-linked executables.  It works by invoking the
# run-time dynamic linker as a command and setting the environment
# variable LD_TRACE_LOADED_OBJECTS to a non-empty value.

# We should be able to find the translation right at the beginning.
TEXTDOMAIN=libc
TEXTDOMAINDIR=/usr/share/locale

RTLDLIST=/lib/ld-linux-aarch64.so.1
warn=
bind_now=
verbose=
<snip>

動的なリンカー/ローダー /lib/ld-linux-aarch64.so.1 をこのスクリプト中で呼出しています.

動的なリンカー/ローダーのファイル名はシステムによって異なります.

ldd は対象のプログラムを実行する環境で実行すること、すなわち、クロス環境であればターゲットのシステム上で起動することが必要です.

もしクロス環境のターゲットのシステムに ldd が無い場合は、以下の様に動的なリンカー/ローダーを直接起動することでプログラムの共有ライブラリの依存関係を調べることができます.

$ /lib/ld-linux-aarch64.so.1 –list ./nfct-mnl-dump
linux-vdso.so.1 (0x000000797aaf5000)
libnetfilter_conntrack.so.3 => /usr/lib/aarch64-linux-gnu/libnetfilter_conntrack.so.3 (0x000000797aa7b000)
libmnl.so.0 => /lib/aarch64-linux-gnu/libmnl.so.0 (0x000000797aa65000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x000000797a8f3000)
libnfnetlink.so.0 => /usr/lib/aarch64-linux-gnu/libnfnetlink.so.0 (0x000000797a8dc000)
/lib/ld-linux-aarch64.so.1 (0x000000797aac7000)

上記は /lib/ld-linux-aarch64.so.1 –list で対象のプログラム nfct-mnl-dump の共有ライブラリの依存関係の一覧を表示しています.

またlddでは対象のプログラムだけではなく共有ライブラリ自信の他のライブラリへの依存関係をみることが可能です.

$ ldd /usr/local/lib/libnetfilter_conntrack.so
linux-vdso.so.1 (0x0000007b73a74000)
libnfnetlink.so.0 => /usr/lib/aarch64-linux-gnu/libnfnetlink.so.0 (0x0000007b739f4000)
libmnl.so.0 => /lib/aarch64-linux-gnu/libmnl.so.0 (0x0000007b739de000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000007b7386c000)
/lib/ld-linux-aarch64.so.1 (0x0000007b73a46000)
[/terminal]

lddでも依存関係が分からないケースがある…

では今までの共有ライブラリの依存関係を調べる方法が完璧か、というと…そうではなくて…依存関係を見過ごしてしまうケースを紹介します.

dlclose, dlerror, dlopen, dlsym – 動的リンクを行うローダーへの プログラミングインターフェース を使った場合、readelf -d, ldd では使用する共有ライブラリが見えません.

それらはプログラム起動後、プログラム内部で必要な共有ライブラリを呼出し、動的リンクを行うインタフェースです.

以下は dlopen のman コマンドに紹介されているサンプルコードです.

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <gnu/lib-names.h>  /* Defines LIBM_SO (which will be a
			       string such as "libm.so.6") */
int
main(void)
{
  void *handle;
  double (*cosine)(double);
  char *error;

  handle = dlopen(LIBM_SO, RTLD_LAZY);
  if (!handle) {
    fprintf(stderr, "%s\n", dlerror());
    exit(EXIT_FAILURE);
  }

  dlerror();    /* Clear any existing error */

  cosine = (double (*)(double)) dlsym(handle, "cos");

  /* According to the ISO C standard, casting between function
     pointers and 'void *', as done above, produces undefined results.
     POSIX.1-2003 and POSIX.1-2008 accepted this state of affairs and
     proposed the following workaround:

     *(void **) (&cosine) = dlsym(handle, "cos");

     This (clumsy) cast conforms with the ISO C standard and will
     avoid any compiler warnings.

     The 2013 Technical Corrigendum to POSIX.1-2008 (a.k.a.
     POSIX.1-2013) improved matters by requiring that conforming
     implementations support casting 'void *' to a function pointer.
     Nevertheless, some compilers (e.g., gcc with the '-pedantic'
     option) may complain about the cast used in this program. */

  error = dlerror();
  if (error != NULL) {
    fprintf(stderr, "%s\n", error);
    exit(EXIT_FAILURE);
  }

  printf("%f\n", (*cosine)(2.0));
  dlclose(handle);
  exit(EXIT_SUCCESS);
}

上記は算術ライブラリ libm.so を動的リンクし cos() を呼び出すサンプルプログラムです.

通常, cos() 関数はビルド時に -lm を指定しますが、上記サンプルコードは -lmは指定せず、dlclose, dlerror, dlopen, dlsymのライブラリ -ldl をリンクします.

$ gcc dlsym_sample.c -ldl -o dlsym_sample

生成されたプログラムの ldd の実行結果でも算術ライブラリ libm.so への依存関係は見えません.

$ ldd dlsym_sample
linux-vdso.so.1 (0x000000729b69c000)
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 (0x000000729b639000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x000000729b4c7000)
/lib/ld-linux-aarch64.so.1 (0x000000729b66e000)

ソースコード上の

handle = dlopen(LIBM_SO, RTLD_LAZY);

で初めてメモリ上に libm.so がリンクされ, その後

dlclose(handle);

でメモリ上から libm.soが削除される、という動作となります.

このようなプログラムの共有メモリの依存関係を調べるには注意が必要です. ソースコードから dlopen() している箇所を読んでいくしかなさそう、です.

参考文献

今回の記事を書くのに参考にしたのはこの本.

以前の記事にも書きましたが、

この手の情報がまとまった本ってほとんどないんですね…大変重宝しています.

では