最近为了科学上网,安装了一个 ShadowSocksR 的 Python 版,起初运行得好好的,某次系统软件升级后突然崩溃了,再也无法正常启动了。
[1] 44820 abort
问题分析
没想到 Python 这种动态脚本语言也能崩溃,太神奇了,于是使用二分 print
大法开始定位问题。
很快定位到是这行代码崩溃了:
from ctypes import CDLL
# ...
lib = CDLL(path)
而且可以发现 path
是 /usr/lib/libcrypto.dylib
,Google 一下就明白了,是 OpenSSL 的动态链接库版本不兼容导致的。
回顾了一下最近的变更,发现果然 Homebrew 安装了一个 OpenSSL 的依赖,并且升级了。
因为我的 Python 是通过 Homebrew 和 pyenv 安装的,这两种方式都会从源码编译安装,并且会依赖 Homebrew 提供的最新版的 OpenSSL,在 /usr/local/opt/openssl/lib/libcrypto.dylib
。
而编译出来的 Python 在执行的时候,会重新根据系统环境去查找这个依赖,结果就找到了系统的版本,在 /usr/lib/libcrypto.dylib
。
版本不符,一个加载就直接崩溃了。顺便感慨一下,Node.js 似乎做得更好些,即使不能加载,崩溃前也会多透露一些有用的信息,可以节约不少时间。
解决依赖问题
问题定位了,看上去应该就很简单了,继续 Google 一下,很快就找到了解决方案。
添加环境变量到 ~/.zshrc
:
export DYLD_LIBRARY_PATH=/usr/local/opt/openssl/lib:$DYLD_LIBRARY_PATH
再次运行代码,还是崩溃了!
继续通过二分 print
大法排查,发现问题还在同一个地方!
我的内心也是崩溃的。
难道这个环境变量在 Python 中不支持?但是这是一个系统级的约定,Python 没道理越过它去重新实现一套吧。
抱着一丝希望,我在 Python 里把环境变量打出来了:
import os
print(os.environ)
print(os.environ['DYLD_LIBRARY_PATH'])
然后惊奇地发现,居然没有这个环境变量。于是我回到 Shell 再打了一遍,确实是存在的。
至此,我才发现强大的 Apple 居然还有这种能力:启动程序时强行篡改环境变量!
被 Apple 砸了一下
继续 Google 了一下,发现了 System Integrity Protection Guide: Runtime Protections。
大概是为了安全起见,新版的 MacOS 会在启动被保护的进程的子进程时,把所有的动态链接相关的环境变量过滤掉,比如 DYLD_LIBRARY_PATH
。
这就中招了。
这里有几种解法:
-
粗暴型
直接用新版替换掉系统的版本,就再也不用担心路径问题了。但是这样可能会有其他的副作用,比如系统中可能存在其他工具就依赖了系统版本的动态库,那就乱套了。
$ sudo rm /usr/lib/libcrypto.dylib $ sudo ln -s /usr/local/opt/openssl/lib/libcrypto.dylib /usr/lib/libcrypto.dylib
-
混乱型
将 Homebrew 的动态库链接到
/usr/local/lib
,这样也可以被找到了,而且系统原来的版本还在。但是同样可能存在一些软件查找到不正确的版本而崩溃。$ sudo ln -s /usr/local/opt/openssl/lib/libcrypto.dylib /usr/local/lib/libcrypto.dylib
-
克制型
魔改本地代码。这种方式完全遵守 Apple 的规范,丝毫不侵犯系统。
我在环境变量里加了一个自定义的前缀:
export DYLD_LIBRARY_PATH=/usr/local/opt/openssl/lib:$DYLD_LIBRARY_PATH export KEEP_DYLD_LIBRARY_PATH=DYLD_LIBRARY_PATH
然后在 Python 代码里手动更新环境变量:
DYLD_LIBRARY_PATH = os.environ.get('KEEP_DYLD_LIBRARY_PATH') if os.environ.get('DYLD_LIBRARY_PATH') is None and DYLD_LIBRARY_PATH is not None: os.environ['DYLD_LIBRARY_PATH'] = DYLD_LIBRARY_PATH
在加载 DLL 之前执行这段 patch,就可以正常加载了。
结语
一直忙碌在无聊的业务代码中,已经好久没有遇到这么有意思的问题了,感觉操作系统的水真的很深啊。