0%

Python-SWIG 初探以及其 gdb 调试

SWIG is used with different types of target languages including common scripting languages such as Javascript, Perl, PHP, Python, Tcl and Ruby. The list of supported languages also includes non-scripting languages such as C#, Common Lisp (CLISP, Allegro CL, CFFI, UFFI), D, Go language, Java including Android, Lua, Modula-3, OCAML, Octave, Scilab and R.

上面那段是 SWIG 的官网介绍,简单地说,SWIG 是一个用于 C/C++ 和高层脚本语言交互的工具。脚本语言在 SWIG 的辅助下可以直接调 C/C++ 的程序。

简单试了一下 SWIG 在 Python 里面的用法,然后测试了一下 gdb 以及 VS 的远程调试(红红火火恍恍惚惚,其实想记录的重点是这个)。


以下内容均在 WSL 里面完成。

Python-SWIG

直接 apt-get 把 SWIG 装上,然后就可以开始瞎搞了。

1. 首先随便写个 C 的函数

官方的文档里面用的是个简单的阶乘的例子,这里也用这个好了。

cfact.h

1
int fact(int n);

cfact.c

1
2
3
4
5
6
7
8
#include "cfact.h"

int fact(int n)
{
if (n < 0) return 0;
else if (n == 0) return 1;
else return n * fact(n-1);
}

2. 写 SWIG 部分的内容,并且生成对应的封装代码

创建一个 SWIG 的文件:

cfact.i

1
2
3
4
5
6
7
8
%module cfact

%{
#define SWIG_FILE_WITH_INIT
#include "cfact.h"
%}

int fact(int n);

然后用 swig 命令生成封装代码:

1
$ swig -python cfact.i

调用完了之后会生成两个新的文件 cfact.py 以及 cfact_wrap.c

3. 编译生成 Python 模块

Python 与 C/C++ 的交互是以模块的方式进行的,C/C++ 代码编译生成运行库之后,以模块的方式链接到 Python 的运行时中。

Python 自带一个 distutils 工具用于创建扩展模块,使用这个工具也很简单,只要写一个配置文件即可:

setup.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from distutils.core import setup, Extension

cfact_module = Extension('_cfact',
sources=['cfact_wrap.c', 'cfact.c'],
)

setup (name = 'cfact',
version = '0.1',
author = "Chen Fan",
description = """Simple swig test""",
ext_modules = [cfact_module],
py_modules = ["cfact"],
)

在目录下执行:

1
$ python setup.py build

之后即可编译得到完整的 Python 模块。

1
2
3
4
5
6
7
8
9
10
11
12
$ python setup.py build
running build
running build_py
creating build
creating build/lib.linux-x86_64-2.7
copying cfact.py -> build/lib.linux-x86_64-2.7
running build_ext
building '_cfact' extension
creating build/temp.linux-x86_64-2.7
x86_64-linux-gnu-gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I/usr/include/python2.7 -c cfact_wrap.c -o build/temp.linux-x86_64-2.7/cfact_wrap.o
x86_64-linux-gnu-gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I/usr/include/python2.7 -c cfact.c -o build/temp.linux-x86_64-2.7/cfact.o
x86_64-linux-gnu-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -D_FORTIFY_SOURCE=2 -g -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security build/temp.linux-x86_64-2.7/cfact_wrap.o build/temp.linux-x86_64-2.7/cfact.o -o build/lib.linux-x86_64-2.7/_cfact.so

可以看到 cfact_wrap.ccfact.c 首先被编译成 build/temp.linux-x86_64-2.7/ 目录下的 cfact_wrap.ocfact.o 两个文件。

之后再编译成一个完整的 _cfact.so 运行库。

切到 build/lib.linux-x86_64-2.7/ 目录下就能用 Python 测试啦:

1
2
3
4
5
6
7
8
9
10
11
12
# jcf @ J-CF-YOGA in ~/swig_test [21:36:17]
$ cd build/lib.linux-x86_64-2.7

# jcf @ J-CF-YOGA in ~/swig_test/build/lib.linux-x86_64-2.7 [21:41:45]
$ python
Python 2.7.6 (default, Oct 26 2016, 20:30:19)
[GCC 4.8.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import cfact
>>> cfact.fact(5)
120
>>>

上 gdb 调试

Python 进程是已经在运行了的,要用 gdb 调就只能附加上进程号来启动,可以在 Python 里面 import os 然后用 os.getpid() 获取进程号,或者用 top/htop 啥的直接看一下 Python 的进程号,然后在 gdb 里面启动即可。

1
$ gdb -p pid

gdb 正常启动,然后这时候可能会报找不到库的调试信息的错误,直接添加 fact 的断点会加不上。用 info sharedlibrary 命令看一下现在追踪到的库信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
0x00007fa4d2be59f0 0x00007fa4d2bf2471 Yes /lib/x86_64-linux-gnu/libpthread.so.0
0x00007fa4d282f520 0x00007fa4d2974183 Yes /lib/x86_64-linux-gnu/libc.so.6
0x00007fa4d2600ed0 0x00007fa4d26019ce Yes /lib/x86_64-linux-gnu/libdl.so.2
0x00007fa4d23f0f10 0x00007fa4d23f1804 Yes /lib/x86_64-linux-gnu/libutil.so.1
0x00007fa4d21d1e00 0x00007fa4d21e1bf8 Yes (*) /lib/x86_64-linux-gnu/libz.so.1
0x00007fa4d1ec5610 0x00007fa4d1f34056 Yes /lib/x86_64-linux-gnu/libm.so.6
0x00007fa4d2e00ae0 0x00007fa4d2e1b490 Yes /lib64/ld-linux-x86-64.so.2
0x00007fa4d1b32720 0x00007fa4d1b33ef6 Yes (*) /usr/lib/python2.7/lib-dynload/readline.x86_64-linux-gnu.so
0x00007fa4d18f30d0 0x00007fa4d1913ee5 Yes (*) /lib/x86_64-linux-gnu/libreadline.so.6
0x00007fa4d16bc3d0 0x00007fa4d16c8028 Yes (*) /lib/x86_64-linux-gnu/libtinfo.so.5
No ./_cfact.so
(*): Shared library is missing debugging information.

_cfact.so 由于不在 gdb 的默认库搜索路径下,所以找不到,需要手动加上路径。

关于 gdb 的库搜索策略,参见这篇博文

这里简单地在 solib-search-path 里面加上 _cfact.so 的位置就好啦:

1
2
3
4
5
6
(gdb) show solib-search-path
The search path for loading non-absolute shared library symbol files is .
(gdb) set solib-search-path :/home/jcf/swig_test/build/lib.linux-x86_64-2.7/
Reading symbols from /home/jcf/swig_test/build/lib.linux-x86_64-2.7/_cfact.so...done.
Loaded symbols for /home/jcf/swig_test/build/lib.linux-x86_64-2.7/_cfact.so
(gdb)

可以看到 set 完之后 _cfact.so 马上就被自动被载入完成了。

这时候在 fact 这个函数上设断点就能够正常找到了:

1
2
3
4
(gdb) break fact
Breakpoint 1 at 0x7fa4d14a3020: file cfact.c, line 5.
(gdb) c
Continuing.

在 Python 窗口里面再调一次 cfact.fact 函数,gdb 成功断在了这里:

1
2
3
4
5
6
7
8
9
10
11
Breakpoint 1, fact (n=n@entry=6) at cfact.c:5
5 if (n < 0) return 0;
(gdb) l
1 #include "cfact.h"
2
3 int fact(int n)
4 {
5 if (n < 0) return 0;
6 else if (n == 0) return 1;
7 else return n * fact(n-1);
8 }(gdb)

调用栈也是比较清晰的,Python 通过生成的封装文件 cfact_wrap.c 最后才调用到最初写的 cfact.c 文件中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) bt
#0 fact (n=n@entry=6) at cfact.c:5
#1 0x00007fa4d14a1c45 in _wrap_fact (self=<optimized out>,
args=<optimized out>) at cfact_wrap.c:3119
#2 0x0000000000523f6d in PyEval_EvalFrameEx ()
#3 0x0000000000567d14 in ?? ()
#4 0x0000000000465a2d in PyRun_InteractiveOneFlags ()
#5 0x0000000000465b49 in PyRun_InteractiveLoopFlags ()
#6 0x00000000004661fe in PyRun_AnyFileExFlags ()
#7 0x0000000000466d92 in Py_Main ()
#8 0x00007fa4d2831f45 in __libc_start_main (main=0x466e50 <main>, argc=1,
argv=0x7fffcd30af38, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7fffcd30af28) at libc-start.c:287
#9 0x0000000000577c2e in _start ()

接下来是 VS 的瞎搞时间~


Visual Studio 中的远程进程调试

新版 VS 中加入了看上去吊炸天的远程 ssh 调试功能,自然是情不自禁地想试试。

在 WSL 中开好 ssh,在 VS 里面 调试 -> 附加到进程 -> SSH -> 选择之前添加好的 WSL 的 ssh 信息 -> 选择附加到 Native(GDB)代码 -> 在下面刷新出来的列表里面直接选前面正在运行的 Python 进程即可。

调试启动!

然后打开的调试界面基本是空的,什么都没有,首先也会有与前面 gdb 相同的加不上断点的问题,原因一样,因为这里调试的原理就是 ssh 过去开一个 gdb。

开始时我在 VS 里面找了很久都没找到哪里可以添加额外 gdb 信息的地方,后再在 VS 的官方文档里面终于翻到了 gdb 调试的命令交互方法。

在 VS 的 命令窗口 中输入 Debug.MIDebugExec 后面再跟 gdb 命令即可直接发送给 ssh 那一端的服务器 gdb 来执行。

一开始直接调用可能会报错:

1
2
>Debug.MIDebugExec info sharedlibrary
Error: Commands are only accepted when the process is stopped.

在调试中给上一个 中断 先进入 gdb 的交互中即可,在 Debug.MIDebugExec 后面跟上正常的 gdb 调试命令,具体的方法跟上面一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>Debug.MIDebugExec info sharedlibrary
From To Syms Read Shared Object Library
0x00007fa4d2be59f0 0x00007fa4d2bf2471 Yes /lib/x86_64-linux-gnu/libpthread.so.0
0x00007fa4d282f520 0x00007fa4d2974183 Yes /lib/x86_64-linux-gnu/libc.so.6
0x00007fa4d2600ed0 0x00007fa4d26019ce Yes /lib/x86_64-linux-gnu/libdl.so.2
0x00007fa4d23f0f10 0x00007fa4d23f1804 Yes /lib/x86_64-linux-gnu/libutil.so.1
0x00007fa4d21d1e00 0x00007fa4d21e1bf8 Yes /lib/x86_64-linux-gnu/libz.so.1
0x00007fa4d1ec5610 0x00007fa4d1f34056 Yes /lib/x86_64-linux-gnu/libm.so.6
0x00007fa4d2e00ae0 0x00007fa4d2e1b490 Yes /lib64/ld-linux-x86-64.so.2
0x00007fa4d1b32720 0x00007fa4d1b33ef6 Yes /usr/lib/python2.7/lib-dynload/readline.x86_64-linux-gnu.so
0x00007fa4d18f30d0 0x00007fa4d1913ee5 Yes /lib/x86_64-linux-gnu/libreadline.so.6
0x00007fa4d16bc3d0 0x00007fa4d16c8028 Yes /lib/x86_64-linux-gnu/libtinfo.so.5
No ./_cfact.so
>Debug.MIDebugExec set solib-search-path :/home/jcf/swig_test/build/lib.linux-x86_64-2.7/
Reading symbols from /home/jcf/swig_test/build/lib.linux-x86_64-2.7/_cfact.so...done.
Loaded symbols for /home/jcf/swig_test/build/lib.linux-x86_64-2.7/_cfact.so
=cmd-param-changed,param="solib-search-path",value=":/home/jcf/swig_test/build/lib.linux-x86_64-2.7/"

这时候在 断点 窗口里面已经可以成功加上 fact 函数的断点了。

加好断点之后 VS 继续,然后 Python 窗口调一次函数。

VS 的断点会马上生效,然后弹出一个窗口让选择源代码文件的位置,毕竟是远程机器上 ssh 得到的,这里的代码位置只能手动选择。

然后可以看到完整的各项信息。

用起来还是相当不错的。


我觉得 VS 调试的意义在于,Debug.MIDebugExec 命令就等于 gdb,所以能用 gdb 做的,VS 都能够做到,关键是能在这样一个方便的图形界面下调试才是最爽的。

以上。