CUBE SUGAR CONTAINER

技術系のこと書きます。

ltrace(1) で共有ライブラリの呼び出しを追いかける

Linux システムでは ltrace(1) を使うことで共有ライブラリの呼び出しを調べることができる。 今回は、いくつかの例を用いて使い方についてざっくりと見ていく。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.4 LTS"
$ uname -r
4.15.0-111-generic
$ ltrace -V
ltrace version 0.7.3.
Copyright (C) 1997-2009 Juan Cespedes <cespedes@debian.org>.
This is free software; see the GNU General Public Licence
version 2 or later for copying conditions.  There is NO warranty.

もくじ

下準備

あらかじめ、利用するパッケージをインストールしておく。 尚、ltrace 以外は使い方のサンプルとして取り上げているだけ。

$ sudo apt-get -y install ltrace libc-bin gcc wget python3-requests python3-numpy

自作のプログラムを使った例

まずは最も単純な例として、ハローワールドするだけのプログラムを試してみよう。

次のような C 言語のソースコードを用意する。

$ cat << 'EOF' > greet.c
#include <stdio.h>
#include <stdlib.h>


int main(void) {
    printf("Hello, World!\n");
    return EXIT_SUCCESS;
}
EOF

上記をビルドする。

$ gcc -Wall -o greet greet.c

これで ELF フォーマットの実行可能ファイルがえきる。

$ file greet
greet: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=1771a2c7c6ded094f15254590870089080689968, not stripped

次のとおり、実行するとただメッセージを出力して終了する。

$ ./greet 
Hello, World!

このように、ただハローワールドするだけのプログラムでも標準 C ライブラリ (libc) はダイナミックリンクされている。

$ ldd greet
    linux-vdso.so.1 (0x00007ffd31d9e000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7081218000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f708180b000)

このプログラムを ltrace(1) 経由で実行してみよう。 すると、共有ライブラリの呼び出しに関する情報が出力される。

$ ltrace ./greet > /dev/null
puts("Hello, World!")                                                                                          = 14
+++ exited (status 0) +++

puts(3) は標準 C ライブラリの提供している API のひとつで、詳細は man を参照のこと。

$ man 3 puts

たとえば -l オプションを指定すると特定の共有ライブラリの呼び出しだけを追跡できる。 ただし、上記のサンプルでは libc の API しか呼び出していないので関係ない。

$ ltrace -l libc.so.6 ./greet > /dev/null
greet->puts("Hello, World!")                                                                                   = 14
+++ exited (status 0) +++

echo(1) を使った例

次は echo(1) を使ってみよう。 こちらも標準 C ライブラリがダイナミックリンクされている。

$ echo "Hello, World"
Hello, World
$ ldd $(which echo)
    linux-vdso.so.1 (0x00007ffe8dddf000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f87d2478000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f87d2a72000)

ltrace(1) 経由で実行すると、先ほどよりも色々な API が呼び出されていることがわかる。

$ ltrace echo "Hello, World"
getenv("POSIXLY_CORRECT")                                                                                      = nil
strrchr("echo", '/')                                                                                           = nil
setlocale(LC_ALL, "")                                                                                          = "C.UTF-8"

...

__freading(0x7fc11d6c3680, 0, 4, 2880)                                                                         = 0
fflush(0x7fc11d6c3680)                                                                                         = 0
fclose(0x7fc11d6c3680)                                                                                         = 0
+++ exited (status 0) +++

wget(1) を使った例 (LibSSL)

もうちょっと複雑な例として wget(1) を試してみよう。

ただし、次は libc ではなく libssl の呼び出しを追跡する。 次のように wget(1) は libssl をダイナミックリンクしている。

$ ldd $(which wget) | grep ssl
    libssl.so.1.1 => /usr/lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007f85b9051000)

libssl の呼び出しに限定して wget(1)ltrace(1) 経由で実行してみよう。 たとえば Google のウェブサイトを取得させてみる。

$ ltrace -l libssl.so.1.1 wget -qO /dev/null https://google.co.jp
wget->OPENSSL_init_ssl(0, 0, 0x55af441e1ec0, 0)                                                                = 1
wget->OPENSSL_init_ssl(0x200002, 0, 0x7fffffff, 0)                                                             = 1
wget->TLS_client_method(0x7f9b550c891c, 129, 0, 0x55af42583904)                                                = 0x7f9b550c28e0

...

wget->SSL_pending(0x55af441efec0, 0x55af44201a30, 127, 0x55af441f5270)                                         = 2
wget->SSL_peek(0x55af441efec0, 0x55af44201a30, 127, 1)                                                         = 2
wget->SSL_read(0x55af441efec0, 0x55af44201a30, 2, 0x7f9b54447eb7)                                              = 2
+++ exited (status 0) +++

ちゃんと呼び出しがトレースできていることがわかる。

Python を使った例 (LibSSL)

次は Python のインタプリタについて、同じように libssl の呼び出しを追いかけてみる。 注意すべき点として、このような例では Python のバイナリは直接は LibSSL をリンクしていない。

$ ldd $(which python3)
    linux-vdso.so.1 (0x00007ffca4dd4000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f78ac216000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f78abff7000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f78abdf3000)
    libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f78abbf0000)
    libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f78ab9be000)
    libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f78ab7a1000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f78ab403000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f78ac607000)

代わりに、同梱されている共有ライブラリが間接的にリンクしている。

$ python3 -c "import _ssl; print(_ssl.__file__)"
/usr/lib/python3.6/lib-dynload/_ssl.cpython-36m-x86_64-linux-gnu.so
$ ldd /usr/lib/python3.6/lib-dynload/_ssl.cpython-36m-x86_64-linux-gnu.so | grep ssl
    libssl.so.1.1 => /usr/lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007f181f27f000)

このような状況では ltrace(1) がブレークポイントを仕掛けるべき共有ライブラリを自動では認識できない。 そのため -l オプションで明示的に対象の共有ライブラリを指定しないと呼び出しが表示されないようだ。

$ ltrace -l libssl.so.1.1 python3 -c "import requests; requests.get('https://google.co.jp')"
--- SIGCHLD (Child exited) ---
--- SIGCHLD (Child exited) ---
--- SIGCHLD (Child exited) ---
_ssl.cpython-36m-x86_64-linux-gnu.so->TLS_method(0xa74560, 0, 0, 0)                                            = 0x7f2b498dd1a0
_ssl.cpython-36m-x86_64-linux-gnu.so->SSL_CTX_new(0x7f2b498dd1a0, 0, 0, 0)                                     = 0x25de450
_ssl.cpython-36m-x86_64-linux-gnu.so->SSL_CTX_get_verify_callback(0x25de450, 0x7f2b475ad5bc, 0, 0x7f2b498f8718) = 0

...

_ssl.cpython-36m-x86_64-linux-gnu.so->SSL_CTX_free(0x25de450, 0x9d2800, -3, 0x27f0070)                         = 0
_ssl.cpython-36m-x86_64-linux-gnu.so->SSL_free(0x27dbaf0, 0x7f2b475d15a8, -2, 2)                               = 0
_ssl.cpython-36m-x86_64-linux-gnu.so->SSL_CTX_free(0x22e8060, 0x9d2800, -3, 0x27f0070)                         = 0
+++ exited (status 0) +++

Python / NumPy を使った例 (LibBLAS)

最後に、おまけとして Python の NumPy が依存している libblas の呼び出しを追いかけてみる。 libblas の実装は Reference BLAS や ATLAS、OpenBLAS、Intel MKL など色々とある。 今回は apt-get(8) を使って NumPy をインストールして Reference BLAS が実装の例になる。

先ほどと同じように、libblas は NumPy に同梱されている共有ライブラリが間接的にリンクしている。 NumPy には Python/C API で書かれた、次のような共有ライブラリがある。

$ dpkg -L python3-numpy | grep so$ | grep -v test
/usr/lib/python3/dist-packages/numpy/core/_dummy.cpython-36m-x86_64-linux-gnu.so
/usr/lib/python3/dist-packages/numpy/core/multiarray.cpython-36m-x86_64-linux-gnu.so
/usr/lib/python3/dist-packages/numpy/core/umath.cpython-36m-x86_64-linux-gnu.so
/usr/lib/python3/dist-packages/numpy/fft/fftpack_lite.cpython-36m-x86_64-linux-gnu.so
/usr/lib/python3/dist-packages/numpy/linalg/_umath_linalg.cpython-36m-x86_64-linux-gnu.so
/usr/lib/python3/dist-packages/numpy/linalg/lapack_lite.cpython-36m-x86_64-linux-gnu.so
/usr/lib/python3/dist-packages/numpy/random/mtrand.cpython-36m-x86_64-linux-gnu.so

この中で ndarray を実装しているのが multiarray で、次のように libblas をリンクしている。

$ ldd /usr/lib/python3/dist-packages/numpy/core/multiarray.cpython-36m-x86_64-linux-gnu.so | grep blas
    libblas.so.3 => /usr/lib/x86_64-linux-gnu/libblas.so.3 (0x00007faf3e37a000)

試しに NumPy を使って配列のドット積を計算してみよう。 すると、内部的に LibBLAS の API を呼び出していることが確認できる。

$ ltrace -l libblas.so.3 python3 -c "import numpy as np; x = np.random.randn(100, 100); np.dot(x, x)"
multiarray.cpython-36m-x86_64-linux-gnu.so->cblas_dgemm(101, 111, 111, 100 <unfinished ...>
libblas.so.3->dgemm_(0x7ffc75477dd6, 0x7ffc75477dd7, 0x7ffc75477dc8, 0x7ffc75477dcc <unfinished ...>
libblas.so.3->lsame_(0x7ffc75477dd6, 0x7fd337a21140, 1, 1)                                                                  = 1
libblas.so.3->lsame_(0x7ffc75477dd7, 0x7fd337a21140, 1, 1)                                                                  = 1
<... dgemm_ resumed> )                                                                                                      = 0
<... cblas_dgemm resumed> )                                                                                                 = 0
+++ exited (status 0) +++

いじょう。