使用Cython将Python代码编译成静态链接可执行文件

12

我有一个纯Python脚本,想要分发到Python配置未知的系统上。因此,我希望将Python代码编译为独立可执行文件。

我运行cython --embed ./foo.py没有问题,得到foo.c。然后,我运行

gcc $(python3-config --cflags) $(python3-config --ldflags) ./foo.c

当运行python3-config --cflags时,会得到以下输出:

-I/usr/include/python3.5m -I/usr/include/python3.5m  -Wno-unused-result -Wsign-compare -g -fdebug-prefix-map=/build/python3.5-MLq5fN/python3.5-3.5.3=. -fstack-protector-strong -Wformat -Werror=format-security  -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes

python3-config --ldflags 则会返回以下内容:

-L/usr/lib/python3.5/config-3.5m-x86_64-linux-gnu -L/usr/lib -lpython3.5m -lpthread -ldl  -lutil -lm  -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions

通过这种方式,我获得了一个动态链接的可执行文件,并且可以毫无问题地运行。 ldd a.out 的结果如下:

 linux-vdso.so.1 (0x00007ffcd57fd000)
 libpython3.5m.so.1.0 => /usr/lib/x86_64-linux-gnu/libpython3.5m.so.1.0 (0x00007fda76823000)
 libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fda76603000)
 libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fda763fb000)
 libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007fda761f3000)
 libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fda75eeb000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fda75b4b000)
 libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007fda7591b000)
 libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fda756fb000)
 /lib64/ld-linux-x86-64.so.2 (0x00007fda77103000)

现在,我尝试向gcc添加选项-static,但结果出现错误:

/usr/bin/ld: dynamic STT_GNU_IFUNC symbol `strcmp' with pointer equality in `/usr/lib/gcc/x86_64-linux-gnu/6/../../../x86_64-linux-gnu/libc.a(strcmp.o)' can not be used when making an executable; recompile with -fPIE and relink with -pie
collect2: error: ld returned 1 exit status

我检查了使用ldd列举出来的所有共享库,发现它们都已经安装为静态库。所以这是否是由python3-config给出的选项不兼容引起的呢?


真的猜测-export-dynamic看起来像一个嫌疑犯吗? - chrisb
"-export-dynamic" 是关于“动态符号表”的(根据gcc文档)。我不知道这是什么。无论如何,删除它会产生一个错误:/usr/bin/ld: unrecognized option '-Wl,-O1'。gcc将不再识别“-Wl”选项。然而,如果没有这些选项,我会得到“文件格式不被识别”的错误。 - phlegmax
我认为-Xlinker -export-dynamic是一个标志 - (XLinker将该标志转发给链接器)。尝试整体删除它? - chrisb
你的链接器命令行是怎样的?foo.o 应该在静态链接库之前。 - ead
@ead,我不确定我理解你的问题。我输入的命令是gcc $(python3-config --cflags) $(python3-config --ldflags) -static ./foo.c - phlegmax
显示剩余2条评论
1个回答

11

经验问题明显源于链接器(gcc在幕后启动了一个链接器,要查看它 - 只需使用-v以详细模式启动gcc)。因此,让我们从简短的提醒开始,了解链接过程的工作原理:

链接器保留其需要解析的所有符号的名称。最初只有符号main。当链接器检查库时会发生什么?

  1. 如果是静态库,则链接器查看此库中的每个对象文件,如果该对象文件定义了一些要查找的符号,则包括整个对象文件(这意味着某些符号得到解析,但可能会添加一些新的未解决符号)。链接器可能需要多次通过静态库。

  2. 如果是共享库,则链接器将其视为由单个巨大的对象文件组成的库(毕竟,我们必须在运行时加载此库,并且不必多次通过来修剪未使用的符号):如果至少有一个所需符号则链接整个库(实际上链接发生在运行时,这是一种干运行),否则-整个库被丢弃,再也不会重新查看。

例如,如果您链接:

gcc -L/path -lpython3.x <other libs> foo.o 

无论是Python3.x是共享库还是静态库,都会遇到一个问题:当连接器看到它时,它只查找符号main,但这个符号在python-lib中没有定义,因此python-lib被丢弃并再也不会被查看。只有当连接器看到目标文件foo.o时,它才意识到需要整个Python-Symbol,但现在为时已晚。

解决此问题的简单规则是:将目标文件放在首位!这意味着:

gcc -L/path  foo.o -lpython3.x <other libs> 

现在连接器知道它从python-lib中需要什么,当它第一次看到它时。

有其他方法可以实现类似的结果。

A)让链接器重复一组存档,只要在每次扫描中至少添加了一个新符号定义:

gcc -L/path --Wl,-start-group -lpython3.x <other libs> foo.o -Wl,-end-group

连接器选项-Wl,-start-group-Wl,-end-group表示连接器会对这组归档文件进行多次迭代,因此连接器有第二次(或更多)机会来包含符号。这个选项可能导致链接时间更长。

B) 打开选项--no-as-needed将导致只链接共享库(而且仅链接共享库),无论是否需要此库中定义的符号。

gcc -L/path -Wl,-no-as-needed -lpython3.x -Wl,-as-needed <other libs> foo.o

实际上,默认的ld行为是--no-as-needed,但gcc前端使用选项--as-needed 调用ld,所以我们可以在python库之前添加-no-as-needed来恢复其行为,然后再将其关闭。


现在讲一下你的静态链接问题。我不认为使用所有标准库的静态版本(包括glibc以上的所有库)是明智的做法,你应该只静态链接Python库。

链接规则很简单:默认情况下,连接器会先尝试打开共享库,然后再尝试静态库。例如对于库libmylib和路径AB,即

 -L/A -L/B lmylib

它尝试按照以下顺序打开库:

A/libmylib.so
A/libmylib.a
B/libmylib.so
B/libmylib.a

因此,如果文件夹A只有一个静态版本,则会使用该静态版本(无论文件夹B中是否有共享版本)。

由于很难确定到底使用了哪个库——这取决于您的系统设置,通常可以通过使用-Wl,-verbose开启链接器日志记录来进行故障排除。

通过使用选项-Bstatic,可以强制使用库的静态版本:

gcc  foo.o -L/path -Wl,-Bstatic -lpython3.x -Wl,-Bdynamic <other libs>  -Wl,-verbose -o foo

值得注意的事情:

  1. foo.o 在库之前被链接。
  2. 在 Python 库之后关闭静态模式,以便其他库可以动态链接。

现在:

 gcc <cflags> L/paths foo.c -Wl,-Bstatic -lpython3.X -Wl,-Bdynamic <other libs> -o foo -Wl,-verbose
...
attempt to open path/libpython3.6m.a succeeded
...
ldd foo shows no dependency on python-lib
./foo
It works!

是的,如果你链接静态的 glibc(我不推荐这样),你需要从命令行中删除 -Xlinker -export-dynamic

编译时没有使用 -Xlinker -export-dynamic 的可执行文件将无法加载一些依赖于该属性的 c 扩展模块,这些模块通过 ldopen 加载到该可执行文件中。


隐式 -pie 选项可能会导致问题。

最近的 gcc 版本默认使用了 pie-option。通常/有时,旧版本的 python 是由旧版 gcc 编译而成的,因此 python-config --cflags 将缺少现在必要的 -no-pie,因为当时不需要。在这种情况下,链接器将生成以下错误信息:

relocation R_X86_64_32S against symbol `XXXXX' can not be used when making a PIE object; recompile with -fPIC

在这种情况下,应在 <cflags> 中添加 -no-pie 选项。


非常感谢您详细而有趣的回答。我的命令行现在读取 gcc $(python3-config --cflags) -L/usr/lib/python3.5/config-3.5m-x86_64-linux-gnu -L/usr/lib ./foo.c -Wl,-Bstatic -lpython3.5m -Wl,-Bdynamic -lpthread -ldl -lutil -lm -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions -o foo -Wl,-verbose。然而,我仍然遇到了大量“未定义引用”错误,例如 /usr/lib/python3.5/config-3.5m-x86_64-linux-gnu/libpython3.5m.a(zlibmodule.o): In function 'PyInit_zlib': (.text+0x1ad): undefined reference to 'zlibVersion' - phlegmax
@phlegmax,我不知道在你的系统上查找所有依赖项出了什么问题。但显然,你需要将-lzlib添加到库中。实际上,所有所需库的列表都在你的问题中给出(即ldd a.out的结果)。 - ead

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接