玩式草子─ソフトウェアとたわむれる日々

第32回SQLiteでRDB再入門その2]

前回紹介したように、しばらくPythonからSQLiteをイジってみたところ、本格的なRDBの機能を思ったよりも簡単に使えることがわかりました。そこで、実際にバイナリファイルの依存性情報を扱うためのコードを書いてみることにしました。

ざっと考えて、必要な機能は以下の3つになるでしょう。

  • ①システム上の全てバイナリファイルの依存性情報を調べる
  • ②その情報をデータベースに登録する
  • ③バイナリファイルや共有ライブラリの名前からデータベースを検索する

このうち、①と②は一連の処理なので一つのスクリプト、③は別のスクリプトにしておくのが便利そうです。

もっとも、最終的には2本のスクリプトにするにしても、筆者がPythonのオンラインドキュメントを首っぴきでないとコードを書けないレベルなので、もう少し小さな部分から書き出すことにしました。

依存性情報データベースの設計

前回紹介したように、あるバイナリファイルが参照している共有ライブラリはlddコマンドで調べることができます。

$ ldd /usr/bin/sed 
    linux-vdso.so.1 =>  (0x00007fff3a35b000)
    libacl.so.1 => /usr/lib64/libacl.so.1 (0x00007f526a0c6000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f5269d5c000)
    libattr.so.1 => /usr/lib64/libattr.so.1 (0x00007f5269b58000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f526a2cd000)

一方、今回利用しようとしているSQLiteはRDBタイプのデータベースなので、データは(テーブル)の形に記録します。

さて、上記のような依存関係の情報は、どのような表に記録するのが便利でしょう? 単純に「バイナリファイルごとに参照しているライブラリを記録する」なら、以下のような形になりそうです。

バイナリファイル共有ライブラリ1共有ライブラリ2共有ライブラリ3共有ライブラリ4 ...
/usr/bin/sedlinux-vdso.so.1libacl.so.1libc.so.6libattr.so.1....

こういう風に記録するとバイナリファイルと共有ライブラリの対応関係は明確になりますが、参照する共有ライブラリの数はバイナリファイルごとに違うので、行ごとに欄の数がまちまちになりそうです。

行ごとに欄の数が異なっていると、共有ライブラリからそれを参照しているバイナリファイルを調べる際などに厄介でしょう。そこで行数はだいぶ大きくなるものの、それぞれの共有ライブラリごとに1行にまとめることにしました。

バイナリファイル共有ライブラリ
/usr/bin/sedlinux-vdso.so.1
/usr/bin/sedlibacl.so.1
/usr/bin/sedlibc.so.6
/usr/bin/sedlibattr.so.1
........

こういう形で整理するならば、共有ライブラリ名だけでなく実際のパス名も加えた方が便利でしょう。また、バイナリファイルも日常使うコマンド名だけでも引けるようにしておく方が便利そうです。そこでデータベースは各行が4つの欄を持つ表の形で記録することにしました。

コマンド名ファイル名共有ライブラリ名ライブラリへのパス
sed/usr/bin/sedlinux-vdso.so.1
sed/usr/bin/sedlibacl.so.1/usr/lib64/libacl.so.1
sed/usr/bin/sedlibc.so.6/lib64/lib.so.6
sed/usr/bin/sedlibattr.so.1/usr/lib64/libattr.so.1
sed/usr/bin/sed/lib64/ld-linux-x86-64.so.2

この例のうち、linux-vdso.so.1はカーネルが提供する仮想的な共有ライブラリなのでライブラリへのパスは存在しません。また、ld-linux-x86-64.so.2はバイナリファイルと共有ライブラリをメモリ上にロードして実行可能な状態にするローダで、正確に言えば共有ライブラリとは異なります。

データベース作成スクリプト

前節で示した表を実現するために、こんなPythonスクリプトを書いてみました。このスクリプトでは、SQLiteのデータベースをdepends.sql3とし、先に紹介した依存情報を収める表をdependsとしています。

 1: #! /usr/bin/python
 2: 
 3: import sqlite3
 4: 
 5: def init_db(dbname):
 6:     conn = sqlite3.connect(dbname)
 7:     conn.execute('''CREATE TABLE depends
 8:        (base TEXT, path TEXT, soname TEXT, realname TEXT)''')
 9:     conn.close
10: 
11: def insert_db(dbname, t):
12:     conn = sqlite3.connect(dbname)
13:     try:
14:         print "inserting ", t
15:         conn.execute('INSERT INTO depends VALUES(?, ?, ?, ?)', t)
16:         conn.commit()
17:     except sqlite3.Error, e:
18:         print "An error occurred:", e.args[0]
19:         conn.rollback()
20: 
21: def main():
22:     dbname = './depends.sql3'
23:     init_db(dbname)
24:     data = (('sed', '/usr/bin/sed', 'linux-vdso.so.1', ''),
25:             ('sed', '/usr/bin/sed', 'libacl.so.1', '/usr/lib64/libacl.so.1'),
26:             ('sed', '/usr/bin/sed', 'libc.so.6', '/lib64/libc.so.6'),
27:             ('sed', '/usr/bin/sed', '', '/lib64/ld-linux-x86-64.so.2'))
28: 
29:     for i in data:
30:         insert_db(dbname, i)
31: 
32: if __name__ == "__main__":
33:     main()

1行目はこのスクリプトを解釈するためのコマンドで、このスクリプトに実行属性を付けて、実行可能にした際に起動すべきコマンド(/usr/bin/python)を指定しています。3行目はPythonに標準添付されているsqlite3モジュールを読み込みます。

5行目から9行目がSQLiteのデータベースにdependsという表を作る処理で、この手順をinit_db()という関数にしています。init_db()では、指定したSQLiteのデータベース(この例では22行目で指定しているdepends.sql3)にSQLコマンドを発行してdependsという表を作ります。この表は、先に示した共有ライブラリごとに1つの行で、各行が4つの欄を持ちます。

各欄の名称を先の例と対応させるとこういう形になります。

コマンド名ファイル名共有ライブラリ名ライブラリへのパス
basepathsonamerealname

11行目から19行目が、このdependsという表にデータを登録するための関数(insert_db())です。この関数は、引数としてSQLiteのデータベース名と登録すべきデータのタプルを受け取って、そのデータを表に登録します。

この関数では、12行目でSQLiteのデータベースと接続し、15行目でデータをdependsという表に登録しようとします。13行目のtry:は、登録処理を試してみて、うまく行かなかった場合には17行目からのエラー処理部を実行するという指定です。

21行目から30行目までがこのスクリプトのmain()部で、SQLiteのデータベース名(depends.sql3)init_db()に渡して初期化(表を作成)し、24から27行目までに作ったサンプルデータのタプルを29行目からのループでinsert_db()に渡してデータベースに登録しています。

32行目と33行目はPython的なお約束ですが、このスクリプトを起動した際にmain()関数を実行するための指定です。

このスクリプトを script01.py いう名前で保存して実行すると、同じディレクトリにdepends.sql3というSQLiteのデータベースファイルが作成され、そこに24行目から27行目に登録したサンプルデータが登録できました。

$ python script01.py
inserting  ('sed', '/usr/bin/sed', 'linux-vdso.so.1', '')
inserting  ('sed', '/usr/bin/sed', 'libacl.so.1', '/usr/lib64/libacl.so.1')
inserting  ('sed', '/usr/bin/sed', 'libc.so.6', '/lib64/libc.so.6')
inserting  ('sed', '/usr/bin/sed', '', '/lib64/ld-linux-x86-64.so.2')

depends.sql3をsqlite3コマンドで調べてみると、このスクリプトで登録したデータを確認できます。

$ sqlite3 depends.sql3 
SQLite version 3.7.10 2012-01-16 13:28:40
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select * from depends;
sed|/usr/bin/sed|linux-vdso.so.1|
sed|/usr/bin/sed|libacl.so.1|/usr/lib64/libacl.so.1
sed|/usr/bin/sed|libc.so.6|/lib64/libc.so.6
sed|/usr/bin/sed||/lib64/ld-linux-x86-64.so.2
sqlite> select * from depends where soname like "%acl%";
sed|/usr/bin/sed|libacl.so.1|/usr/lib64/libacl.so.1

これでデータベースに登録する部分ができました。

依存関係情報収集スクリプト

依存関係を記録する側の処理は目処がついたので、次にバイナリファイルの依存関係情報を収集する方法を考えます。

バイナリファイルの依存関係情報は、ディレクトリ構造をたどりながらバイナリファイルを見つけると共に、それぞれのバイナリファイルが参照している共有ライブラリを調べあげる必要があります。

ディレクトリ構造をたどる処理は、Pythonのos.walk()で対応できそうです。一方、ファイルの種類を調べるfileコマンドや共有ライブラリを調べるlddコマンドの同等品はPythonのライブラリには存在しないようなので、これらは外部コマンドとして利用する必要があるでしょう。

また、バイナリファイルと共有ライブラリの関係は、('sed', '/usr/bin/sed', 'libc.so.6', '/lib64/libc.so.6')のようなタプル構造に収めてやれば、最初のスクリプトがそのまま流用できるはずです。

このように考えて次のようなスクリプトを書いてみました。このスクリプトは少し長めなので、3つくらいの部分に分けて紹介してみます。


 1: #! /usr/bin/python
 2: 
 3: import os, subprocess
 4: 
 5: def get_elfs(path):
 6:     test = os.walk(path,followlinks=False)
 7:     elfs = []
 8:     for root, dirs, files in test:
 9:         for i in files:
10:             path = os.path.join(root,i)
11:             if os.path.islink(path) == False:
12:                 if check_elf(path) :
13:                     print("{0} is ELF".format(path))
14:                     elfs.append(path)
15:     return elfs
16: 
17: def check_elf(testfile):
18:     res = subprocess.check_output(['file', testfile])
19:     if res.find('ELF') > 0 and res.find('dynamically linked') > 0 :
20:         return True
21:     else:
22:         return False
23: 

1行目はこのスクリプトをコマンドとして使うための設定、3行目はPythonのモジュールを読み込む指定で、ファイル名を操作するためのosモジュールと外部コマンドを実行して情報を得るためのsubprocessモジュールをimportしています。

5行目から15行目が指定したディレクトリ以下のバイナリファイルを調べる関数です。この関数では先にimportしたosモジュールの提供するos.walk()という機能を使って、引数として与えられたディレクトリ以下にあるファイルを調べあげ、それぞれのファイルに対して17行目から定義しているcheck_elf()という関数を使ってバイナリファイルであるかどうかを調べています。

バイナリファイルであれば、7行目で初期化しているelfsというリストにそのファイルのフルパス名を追加していきます。引数として与えられたディレクトリ以下の全ファイルのチェックが終了すれば、バイナリファイルの一覧リストelfsを返します。

17行目から22行目がバイナリファイルかどうかを判断するcheck_elf()関数です。この関数では、先にimportしたsupprocessモジュールにあるsubprocess.check_output()という機能を使って、外部コマンドであるfileサブプロセスとして実行し、実行結果として受け取った文字列に"ELF"と"dynamically linked"という文字列があるかを調べています。

これらの文字列があれば共有ライブラリを参照しているバイナリファイルであると判断できるのでTrueを返し、見つからなければFalseを返します。


24: def get_depends(file):
25:     depends = []
26:     try:
27:         res = subprocess.check_output(['ldd', file])
28:         tmp = res.splitlines()
29:         for i in tmp:
30:             depends.append(i.lstrip())
31: 
32:     except subprocess.CalledProcessError:
33:         print("error occured to ldd {0}. maybe different archs?".format(file))
34: 
35:     return depends
36: 
37: def split_parts(l):
38:     (soname, sep, last) = l.partition(' => ')
39:     if soname == 'linux-vdso.so.1' or soname == 'linux-gate.so.1' :
40:         realname = ''
41:     elif soname.find('ld-linux') > 0:
42:         (t1, t2, t3) = soname.partition(' (')
43:         soname = ''
44:         realname = t1
45:     else:
46:         (realname, sep, last2) = last.partition(' (')
47:     return (soname, realname)
48: 

24行目から35行目がバイナリファイルの依存関係を調べるget_depends()関数です。この関数では、引数に指定されたファイルに対しsubprocess.check_output()を使って外部のlddコマンドを実行し、その結果を行単位に分割(28行目)して、先頭の空白を取り除いた(30行目)上でdependsというリストに収めて返します。

get_depends()関数には事前にELF形式であることをチェックしたファイルしか与えませんが、同じELF形式でも32ビット環境で64ビットなバイナリファイルを調べようとするとエラーになるので、念のためにtry:~except:で囲んでlddコマンドのエラーチェックをしています。

37行目から47行目はlddコマンドの出力を整形するためのsplit_parts()関数で、lddの出力である"libacl.so.1 => /usr/lib64/libacl.so.1 (0x00007fcbff85d000)"といった行を受け取って、共有ライブラリ名(libacl.so.1)と実際のパス(/usr/lib64/libacl.so.1)に分割して、両者をペアにして返しています。

lddの結果の中にはカーネルの提供する仮想的な共有ライブラリ(x86_64環境ではlinux-vdso.so.1、x86環境ではlinux-gate.so.1)やローダ(ld-linux)の情報も含まれるので、39行目から44行目でそれらに対応しています。


49: def main():
50:     search_dirs = ['/bin',  '/lib', '/lib64', '/sbin', '/usr', '/opt']
51:     for dir in search_dirs:
52:         print("searching {0}".format(dir))
53:         files = get_elfs(dir)
54:         list = []
55:         for path in files:
56:             base = os.path.basename(path)
57:             tmp = get_depends(path)
58:             for i in tmp:
59:                 (soname, realname) = split_parts(i)
60:                 t = (base, path, soname, realname)
61:                 list.append(t)
62: 
63:         for i in list:
64:             print i
65: 
66: if __name__ == "__main__":
67:     main()

49行目から64行目がmain()の処理で、まずsearch_dirsに調べたいディレクトリを指定しておき、それぞれのディレクトリに対して、get_elfs()でそのディレクトリ以下のバイナリファイルを調べ、その結果をfilesに収めておきます。

次に、filesに収めておいたそれぞれのバイナリファイルごとに、get_depends()で依存関係情報を調べ、その結果をsplit_parts()で分割してsonameとrealnameを取り出し、それらをバイナリファイルから得られるbase、pathと合わせて(base, path, soname, realname)という4要素のタブルにして、54行目で初期化しておいたリスト型の変数listに追加していきます。

こうして作成した依存関係情報のタプルを、先に紹介したスクリプトのinsert_db()に与えてデータベースに登録すればいいわけですが、このスクリプトでは、とりあえず動作確認のためにlistに記録したタプルを表示させることにしました。

このスクリプトをscript02.pyとしてテストしてみます。

$ python script02.py
searching /bin
/bin/bash is ELF
/bin/bzip2 is ELF
/bin/dd is ELF
/bin/cp is ELF
/bin/df is ELF
....
('bash', '/bin/bash', 'linux-vdso.so.1', '')
('bash', '/bin/bash', 'libreadline.so.6', '/lib64/libreadline.so.6')
('bash', '/bin/bash', 'libncursesw.so.5', '/lib64/libncursesw.so.5')
('bash', '/bin/bash', 'libdl.so.2', '/lib64/libdl.so.2')
('bash', '/bin/bash', 'libc.so.6', '/lib64/libc.so.6')
('bash', '/bin/bash', '', '/lib64/ld-linux-x86-64.so.2')
('bzip2', '/bin/bzip2', 'linux-vdso.so.1', '')
('bzip2', '/bin/bzip2', 'libbz2.so.1.0', '/lib64/libbz2.so.1.0')
....

ざっと見た限りでは、指定したディレクトリ(この例では/bin)以下のバイナリファイルを調べる機能も、それぞれのバイナリファイルごとに参照しているライブラリを調べる機能も動作しているようです。

この2つのスクリプトで最初にあげた3つの機能のうち1と2は実現できたので、次に両者を組み合わせて1つのコマンドにしていくわけですが、だいぶ長くなってきたのでそのあたりは次回に回しましょう。

おすすめ記事

記事・ニュース一覧