gdbでpythonをデバッグ

8 min read読了の目安(約7200字

はじめに

pythonプロセスをgdbでアタッチして、python領域をデバッグする方法です。
gdbでC/C++のデバッグができますが、python領域は簡単にはみれないのでpython-debuginfoをつかって、デバッグする方法を記載します。
情報の元ネタは、参考文献[1]を参照。

どういう時につかえるか

  • pdbではスレッド間のアタッチ/デタッチができないので、マルチスレッドのデバッグできるようにどうにかしたい
  • 特定の環境でしかおきないバグや、長時間実行後にハングアップするなど、ログ出力でのデバッグなど調査がツラい
  • IDEでインスペクタなどがつかえない環境、pydevなどでsuspendしてもスレッドがとまってくれない

開発環境

  • python2.7 / python3.6.8 (どちらもyumでインストール)
  • Cent OS 7
    CentOS Linux release 7.7.1908 (Core)
  • GNU gdb
    GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-115.el7

セットアップ

  • デバッグ用のリポジトリを有効にして、python-debuginfoをインストール
    (python2.7の場合)
# debuginfo-install python

(python3の場合)

# debuginfo-install python3 libgcc

私自身は、以下のようにデバッグ用のリポジトリを有効にしてインストールする方法をやっていましたが、debuginfo-installのほうがスマートそうなので直しています。
yum --disablerepo='*' --enablerepo='*-debug*' install python-debuginfo

python3ではなぜかlibgccのdebuginfoもインストールしろ、と怒られたので追記しています。ほかにも必要なパッケージがあるかもしれません。足りない場合は、以下に記載するgdb起動時に、インストールが必要なものについてメッセージが出力されるはず?

使い方

  1. デバッグしたいプロセスのIDを取得
  2. 以下のコマンドでgdbを起動してアタッチ
$ gdb python <PID>
  1. python-debuginfo用のコマンドファイルを実行
    (python2.7の場合)
(gdb) source /usr/lib/debug/usr/lib64/libpython2.7.so.1.0.debug-gdb.py

(python3の場合)

(gdb) source /usr/lib/debug/usr/lib64/libpython3.6dm.so.1.0-3.6.8-18.el7.x86_64.debug-gdb.py

今回は、3.6.8をつかっているので上記ですが、/usr/lib/debug/usr/lib64/下を覗いて、適宜バージョンに対応するファイルを適用してください。

  1. 以下のコマンドがつかえるようになったので、実行してデバッグ
  • py-list
    該当範囲のPythonコード出力
  • py-bt
    該当Pythonコードのバックトレース
  • py-up
    Pythonスタックの上へ
  • py-down
    Pythonスタックの下へ
  • py-print
    Pythonスタックの変数表示
  • py-locals
    Pythonスタックの変数リスト表示

上記の説明は、参考文献[2]から引用させていただきました。
使い方などヘルプを探しましたが、見当たりませんでした。
どこかに情報あるかもしれませんが、コマンドファイル*.debug-gdb.pyのソースをみて、なんのコマンドがつかえるか/どう使うのかみてみるのが早いかもしれません。

デモ

  1. デッドロックするpythonプログラムを実行
    なんでもよいですが、この記事用に試しにつくったプログラムは以下です。
pythondebugggb.py
pythondebugggb.py
import threading
import time

class MultiThreadDeadLock(object):
    '''
    MultiThreadDeadLock
    デッドロックを意図的におこすテスト用の実装
    '''

    def __init__(self):
        '''
        constructor
        '''
        self._counter = 0
        self.main()

    def increment(self, lock):
        '''
        カウンタをインクリメントします。
        :param lock: (object) threading.Lock()で取得した排他用オブジェクト
        :return: None
        '''
        while True:
            time.sleep(0.1) # スレッド間の割り込み用に少し待たせる
            lock.acquire()
            self._counter += 1
            lock.release()

    def print_counter(self, lock):
        '''
        現在のカウンタを表示します。
        デッドロックを再現するため3より大きい場合に意図的にロックを解放せずにExceptionをraiseします。
        :param lock: (object) threading.Lock()で取得した排他用オブジェクト
        :return: None
        '''
        while True:
            time.sleep(0.1) # スレッド間の割り込み用に少し待たせる
            lock.acquire()
            print("counter = {}".format(self._counter))
            if self._counter > 3:
                # 意図的にExceptionをraiseして、デッドロックさせる。
                # 実際には、予期しないところでExceptionがraiseされることだろう...
                raise RuntimeError()
            lock.release()

    def main(self):
        '''
        メイン処理
        :return: None
        '''
        lock = threading.Lock()

        inc_thread = threading.Thread(target=self.increment, args=(lock,))
        print_thread = threading.Thread(target=self.print_counter, args=(lock,))
        inc_thread.start()
        print_thread.start()
        inc_thread.join()
        print_thread.join()

if __name__ == '__main__':
    MultiThreadDeadLock()
  1. プロセスIDをチェック
    psコマンドとかpgrepとかで探してください。

  2. gdbでアタッチして、コマンドファイル*.debug-gdb.pyを実行して、py-btを実行

$ gdb python3 7066
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-115.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /usr/bin/python3.6...Reading symbols from /usr/lib/debug/usr/bin/python3.6.debug...done.
done.
(中略)
done.
Loaded symbols for /lib64/libgcc_s.so.1
0x00007f5a083e8afb in futex_abstimed_wait (cancel=true, private=<optimized out>, 
    abstime=0x0, expected=0, futex=0x7f59fc000c10)
    at ../nptl/sysdeps/unix/sysv/linux/sem_waitcommon.c:43
43	      err = lll_futex_wait (futex, expected, private);
(gdb) source /usr/lib/debug/usr/lib64/libpython3.6dm.so.1.0-3.6.8-18.el7.x86_64.debug-gdb.py
(gdb) py-bt
Traceback (most recent call first):
  <built-in method acquire of _thread.lock object at remote 0x7f5a011328c8>
  File "/usr/lib64/python3.6/threading.py", line 1072, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
  File "/usr/lib64/python3.6/threading.py", line 1056, in join
    self._wait_for_tstate_lock()
  File "pythondebuggdb.py", line 68, in main
    inc_thread.join()
  File "pythondebuggdb.py", line 26, in __init__
    self.main()
  File "pythondebuggdb.py", line 72, in <module>
    MultiThreadDeadLock()
  1. スレッドを切り替え
(gdb) info threads
  Id   Target Id         Frame 
  2    Thread 0x7f5a01094700 (LWP 7067) "python3" 0x00007f5a083e8afb in futex_abstimed_wait (cancel=true, private=<optimized out>, abstime=0x0, expected=0, futex=0x2471020)
    at ../nptl/sysdeps/unix/sysv/linux/sem_waitcommon.c:43
* 1    Thread 0x7f5a08d20740 (LWP 7066) "python3" 0x00007f5a083e8afb in futex_abstimed_wait (cancel=true, private=<optimized out>, abstime=0x0, expected=0, futex=0x7f59fc000c10)
    at ../nptl/sysdeps/unix/sysv/linux/sem_waitcommon.c:43
(gdb) thread 2
[Switching to thread 2 (Thread 0x7f5a01094700 (LWP 7067))]
#0  0x00007f5a083e8afb in futex_abstimed_wait (cancel=true, private=<optimized out>, 
    abstime=0x0, expected=0, futex=0x2471020)
    at ../nptl/sysdeps/unix/sysv/linux/sem_waitcommon.c:43
43	      err = lll_futex_wait (futex, expected, private);
  • py-listで該当行を確認しつつ、別スレッド側をpy-btでスタックトレースを確認
(gdb) py-list
  31            :param lock: (object) threading.Lock()で取得した排他用オブジェクト
  32            :return: None
  33            '''
  34            while True:
  35                time.sleep(0.1) # スレッド間の割り込み用に少し待たせる
 >36                lock.acquire()
  37                self._counter += 1
  38                lock.release()
  39    
  40        def print_counter(self, lock):
  41            '''
(gdb) py-bt
Traceback (most recent call first):
  <built-in method acquire of _thread.lock object at remote 0x7f5a08c2f4e0>
  File "pythondebuggdb.py", line 36, in increment
    lock.acquire()
  File "/usr/lib64/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/lib64/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib64/python3.6/threading.py", line 884, in _bootstrap
    self._bootstrap_inner()

2スレッドともlock.acquire()で止まっているので、デッドロックだということがわかる。

参考文献

以下、脚注に記載

脚注
  1. Debugging an inactive python process ↩︎

  2. Qiita:Pythonでgdbを操作する。 ↩︎