[Python] Segmentation Fault 발생 시 gdb로 stack trace 해 보기 Develop Tip

파이썬으로 개발을 하다보면 제일 까다로운 문제 중의 하나는 
갑자기 Python (CPython) 프로그램 자체가 Segmentation Fault 를 발생하고 
죽는 문제 입니다.

일반적인 파이썬 프로그램 자체는 C로 작성한 프로그램 처럼 
Segmentation Fault 를 발생하며 죽는 경우는 거의 없습니다.

대신, 외부 so 모듈을 이용한다든지 등에서 충분히 발생할 수 있습니다.

우선 테스트한 머신은 우분투 16.04 LTS 버전 입니다.


다음과 같은 파이썬 스크립트가 있습니다.

$ cat dumpcore.py 
class Foo:

    def bar(self):
        from ctypes import string_at
        string_at(0xDEADBEEF) # this code will cause Python to segfault


def main():
    f = Foo()
    f.someattr = 42
    f.someotherattr = {'one':1, 'two':2, 'three':[(), (None,), (None, None)]}
    f.bar()


if __name__ == "__main__":
    main()

위의 프로그램을 파이썬 3.5에서 돌려보면 (VirtualEnv 이용)

(python3) future@FS:~$ python dumpcore.py 
Segmentation fault (core dumped)

마찬가지로 파이썬 2.7에서도
(python2) future@FS:~$ python dumpcore.py 
Segmentation fault (core dumped)

동일하게 오류가 발생합니다.

발생 원인은 직접 ctypes로 접근 불가능한 메모리를 접근하려고 하기 때문에
발생하는 것입니다.
(C 에서도 아주 쉽게 포인터 참조 오류가 발생할 수 있지요)

이와 같은 경우 gdb로 쉽게 원인을 찾을 수 있는 방법을 소개 합니다.


1) libpython.py 다운로드

$ mkdir -p ~/.config/gdb && cd ~/.config/gdb
# python 3.5 인 경우
$ wget https://hg.python.org/cpython/rawfile/3.5/Tools/gdb/libpython.py
# python 2.7 인 경우
$ wget https://hg.python.org/cpython/rawfile/2.7/Tools/gdb/libpython.py


2) .gdbinit

$ vi ~/.gdbinit
python
import gdb
import sys
import os
sys.path.insert(0, os.path.expanduser("~/.config/gdb"))
def setup_python(event):
    import libpython
gdb.events.new_objfile.connect(setup_python)
end


3) gdb 실행

참고! python 2.7 인 경우에는 python-dbg 를 이용해야 합니다.
# python 2.7 인 경우
$ sudo apt-get install python-dbg

# python 2.7 인 경우
$ gdb --args python-dbg dumpcore.py

# python 3.5 인 경우
$ gdb --args python dumpcore.py

...
(gdb) run
해당 스크립트를 실행하면...

Starting program: /opt/python3/bin/python dumpcore.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
strlen () at ../sysdeps/x86_64/strlen.S:106
106 ../sysdeps/x86_64/strlen.S: No such file or directory.
(gdb)

위와 같이 세그폴트가 발생하였음을 알 수 있구요...

만약 일반적인 stack trace 인 

(gdb) bt
#0  strlen () at ../sysdeps/x86_64/strlen.S:106
#1  0x00007ffff6764cd9 in string_at.lto_priv.109 (ptr=0xdeadbeef <error: Cannot access memory at address 0xdeadbeef>, 
    size=<optimized out>) at /build/python3.5-ZBQ2p1/python3.5-3.5.2/Modules/_ctypes/_ctypes.c:5219
#2  0x00007ffff6772e20 in ffi_call_unix64 () from /opt/python3/lib/python3.5/lib-dynload/_ctypes.cpython-35m-x86_64-linux-gnu.so
#3  0x00007ffff677288b in ffi_call () from /opt/python3/lib/python3.5/lib-dynload/_ctypes.cpython-35m-x86_64-linux-gnu.so
#4  0x00007ffff676d01a in _call_function_pointer (argcount=2, resmem=0x7fffffffd970, restype=<optimized out>, 
    atypes=<optimized out>, avalues=0x7fffffffd950, pProc=0x7ffff6764cc0 <string_at.lto_priv.109>, flags=4357)
    at /build/python3.5-ZBQ2p1/python3.5-3.5.2/Modules/_ctypes/callproc.c:811
#5  _ctypes_callproc (pProc=0x7ffff6764cc0 <string_at.lto_priv.109>, argtuple=<optimized out>, flags=4357, 
    argtypes=(<built-in method from_param of _ctypes.PyCSimpleType object at remote 0xb3ecd8>, <built-in method from_param of _ctypes.PyCSimpleType object at remote 0xb39768>), restype=<_ctypes.PyCSimpleType at remote 0xb35ac8>, checker=0x0)
    at /build/python3.5-ZBQ2p1/python3.5-3.5.2/Modules/_ctypes/callproc.c:1149
#6  0x00007ffff6760fcb in PyCFuncPtr_call.lto_priv.89 (self=self@entry=0x7ffff7fc9d90, inargs=<optimized out>, 
    kwds=<optimized out>) at /build/python3.5-ZBQ2p1/python3.5-3.5.2/Modules/_ctypes/_ctypes.c:3856
#7  0x00000000005bc727 in PyObject_Call () at ../Objects/abstract.c:2165
#8  0x000000000051f756 in do_call (nk=<optimized out>, na=<optimized out>, pp_stack=0x7fffffffdc70, func=<optimized out>)
    at ../Python/ceval.c:4936
...

위와 같이 C레벨의 코드가 나와 어디서 부터 오류가 발생하였는지 알 수 없습니다.

대신, py-bt 라는 명령을 내리면,

(gdb) py-bt
Traceback (most recent call first):
  File "/usr/lib/python3.5/ctypes/__init__.py", line 491, in string_at
    return _string_at(ptr, size)
  File "dumpcore.py", line 5, in bar
    string_at(0xDEADBEEF) # this code will cause Python to segfault
  File "dumpcore.py", line 12, in main
    f.bar()
  File "dumpcore.py", line 16, in <module>
    main()

위와 같이 깔끔하게 dumpcore.py 스크립트의 5번째 줄에서 오류가 발생되었음을 확인할 수 있습니다.


참고로

(gdb) py-list
 486    _string_at = PYFUNCTYPE(py_object, c_void_p, c_int)(_string_at_addr)
 487    def string_at(ptr, size=-1):
 488        """string_at(addr[, size]) -> string
 489    
 490        Return the string at addr."""
>491        return _string_at(ptr, size)
 492    
 493    try:
 494        from _ctypes import _wstring_at_addr
 495    except ImportError:
 496        pass

위와 같이 py-list 명령으로 마지막 오류 발생 파이썬 스크립트를 직접 확인할 수 있습니다.


어느분께는 도움이 되셨기를...


핑백

덧글

  • 개밟자 2017/11/17 14:24 # 삭제 답글

    많은 도움이 되었습니다..
  • 지훈현서아빠 2017/11/18 16:47 #

    도움이 되셨다니 저의 보람입니다~ ^^
  • 석사과정학생 2019/04/14 02:00 # 삭제 답글

    정말 많은 도움이 되었습니다. 졸업논문 쓰는 도중에 막혀버려서 반쯤 포기하고 있었는데, 덕분에 졸업이 한 걸음 더 가까워졌습니다. 감사합니다.
  • 지훈현서아빠 2019/04/14 06:54 #

    프로그램이 동작하려면 Stack, Heap 등의 메모리를 이용하게 되는데 이때 프로세스가 사용하기로 되어 있는 주소를 벗어나면서
    위와 같은 문제가 주로 발생합니다.
    암튼 도움이 되셨다니 저의 보람입니다~ ^^
  • 학생 2019/05/17 10:31 # 삭제 답글

    실행해보았는데 정작 중요한 py-bt에서 Unable to python frame이라는 오류가 뜨고 아무 내용이 출력되지 않네요 ㅠㅠ혹시 어떤 오류일지 감이 오시는게 있으신가요?
댓글 입력 영역

구글애드텍스트