4.5. 深入了解 BCC 模式
BCC 模式会把部分函数直接转换成为二进制代码,在根本上避免被还原成为 Python 函数。
BCC 模式需要配置 C 编译器,对于 Linux 和 Darwin 来说,一般不需要进行配置,只要默认的 gcc
和 clang
能工作就可以。在 Windows 环境下面,可以使用下面任意一种方式配置 clang.exe
,目前其它编译器还不支持:
如果已经有
clang.exe
,只要在其它路径直接运行clang.exe
不出错就可以。如果文件存在,但是无法在任意路径直接运行,可以配置环境变量PYARMOR_CC
来指定这个文件,例如:set PYARMOR_CC=C:\path\to\clang.exe
从 LLVM 官网 下载并安装预编译版本
从 Pyarmor 官网下载 clang-9.0.zip ,压缩包大小约为 26M 左右,里面只有一个可执行文件,解压后存放在 根目录 下面,默认是
%HOME%/.pyarmor
4.5.1. 启用 BCC 模式
配置好编译器之后,使用选项 --enable-bcc
启用 BCC 模式:
$ pyarmor gen --enable-bcc foo.py
模块级别的代码不会转换转换成为 C 的函数,模块的任何函数如果使用了不被支持的特性,也不会转换成为 C 函数,这么没有使用 BCC 模式加密的函数会根据选项使用其他方式进行加密。
4.5.2. 查看被 BCC 模式加密的函数
启用跟踪模式可以在跟踪日志文件 pyarmor.trace.log
中记录那些函数被转换成为了 C 函数。例如:
$ pyarmor cfg enable_trace=1
$ pyarmor gen --enable-bcc foo.py
查看跟踪日志中使用 trace.bcc
记录的内容:
$ ls pyarmor.trace.log
$ grep trace.bcc pyarmor.trace.log
trace.bcc foo:5:hello
trace.bcc foo:9:sum2
trace.bcc foo:12:main
第一条日志记录的是 foo.py
第5行的函数 hello
被转换成为 C 函数
第二条日志记录的是 foo.py
第9行的函数 sum2
被转换成为 C 函数
如果在 trace.bcc
之后的字符是 !
,那么意味着这个函数被 BCC 模式忽略。例如:
trace.bcc ! foo:29:Test.new (unsupported function "super")
4.5.3. 不转换特定的模块和函数
使用 BCC 模式加密脚本并不是完全和原来的脚本兼容,如果运行 BCC 模式加密脚本出现了兼容性问题,那么解决方案就是不要转换特定的模块,或者模块中特定的函数。
为了避免影响其他脚本,这里使用 模块私有配置 来修改单独修改模块的设置。
让 BCC 模式忽略一个模块 pkgname.modname
使用下面的命令:
$ pyarmor cfg -p pkgname.modname bcc:disabled=1
忽略模块中的函数或者方法使用下面的命令:
$ pyarmor cfg -p pkgname.modname bcc:excludes="name"
$ pyarmor cfg -p pkgname.modname bcc:excludes="name1 name2 name3"
$ pyarmor cfg -p pkgname.modname bcc:excludes="Class.method_1"
$ pyarmor cfg -p pkgname.modname bcc:excludes="Class.*"
如果没有选项 -p
的话,其他模块中同名函数和方法也会被忽略。
下面是一个示例脚本 foo.py
def hello_a():
pass
def hello_b():
pass
class Test(object):
def __init__(self):
pass
def hello_a():
pass
使用下面的任意一种方式来忽略其中的函数和方法:
$ pyarmor cfg -p foo bcc:excludes = "hello_a"
$ pyarmor cfg -p foo bcc:excludes = "hello_a hello_b"
$ pyarmor cfg -p foo bcc:excludes = "hello_*"
$ pyarmor cfg -p foo bcc:excludes = "Test.hello_a"
$ pyarmor cfg -p foo bcc:excludes = "Test.*"
$ pyarmor cfg -p foo bcc:excludes = "Test.__*__"
$ pyarmor cfg -p foo bcc:excludes = "hello_a Test.hello_a"
如果只需要 BCC 模式处理特定的函数,那么使用选项 bcc:includes:
# 恢复默认选项
$ pyarmor cfg bcc:excludes = ""
# BCC 模式只处理模块函数 "hello_a"
$ pyarmor cfg -p foo bcc:includes = "hello_a"
# BCC 模式还需要处理类方法 "Test.hello_a"
$ pyarmor cfg -p foo bcc:includes + "Test.hello_a"
# BCC 模式处理类 "Test" 的所有方法,除了 "__init__"
$ pyarmor cfg -p foo bcc:includes="Test.*" bcc:excludes="Test.__init__"
我们可以启用跟踪日志,查看这些语句的效果,看看这些模块和函数是否没有被 BCC 模式处理。例如:
$ pyarmor cfg enable_trace 1
$ pyarmor gen --enable-bcc foo.py
$ grep trace.bcc pyarmor.trace.log
另外一个例子,忽略 joker/card.py
但是使用 BCC 模式加密包 joker
的其他模块:
$ pyarmor cfg -p joker.card bcc:disabled=1
$ pyarmor gen --enable-bcc /path/to/pkg/joker
选项 bcc:includes 和 bcc:excludes 只对模块基本的函数和类有效,它们不能处理子函数和子类。例如
def hello():
def wrap():
pass
class Test:
def __init__(self):
pass
下面的命令无法忽略子函数 wrap
和子类 Test
:
pyarmor cfg bcc:excludes = "wrap hello.wrap Test.__init__ hello.Test.__init__"
忽略子函数和子类的唯一方法是直接忽略其所在的顶层函数 hello
:
pyarmor cfg bcc:excludes = hello
在 8.3.4 版本加入: 新增选项 bcc:includes.
在 8.3.4 版本发生变更: 选项 bcc:excludes 的使用方法发生了变化。在之前的版本:
# 忽略模块函数和所有的类方法 "hello_a"
pyarmor cfg bcc:excludes="hello_a"
# 在方法前指定类名称的话,无法忽略任何方法
pyarmor cfg bcc:excludes="Myclass.hello_a"
在现在的版本中:
# 需要使用下面的形式来忽略模块函数和所有的类方法 "hello_a"
pyarmor cfg bcc:excludes="hello_a *.hello_a"
# 可以指定类名称来忽略单独的类方法
pyarmor cfg bcc:excludes="Myclass.hello_a"
4.5.4. 改变的脚本特性
使用 BCC 模式加密后脚本和原来的脚本存在一些额外的不同
部分异常的提示信息和原来不一样
如果不是在异常处理的过程中,直接调用没有参数的 raise 抛出的异常类型不同
没有加密前
>>> raise RuntimeError: No active exception to reraise
在 BCC 模式加密的脚本中
>>> raise UnboundlocalError: local variable referenced before assignment
函数对象的属性,尤其是以双下划线开始的属性,例如
__qualname__
等等,在转换成为 C 函数之后都不存在,使用这些属性的函数加密后无法正常工作
4.5.5. 不支持的特性
使用了下列特性的函数无法转换成为 C 函数
unsupport_nodes = (
ast.ExtSlice,
ast.AsyncFunctionDef, ast.AsyncFor, ast.AsyncWith,
ast.Await, ast.Yield, ast.YieldFrom, ast.GeneratorExp,
ast.NamedExpr,
ast.MatchValue, ast.MatchSingleton, ast.MatchSequence,
ast.MatchMapping, ast.MatchClass, ast.MatchStar,
ast.MatchAs, ast.MatchOr
)
如果调用了下列任意一个内置函数,那么该函数也无法转换成为 C 函数:
exec,
eval
super
locals
sys._getframe
sys.exc_info
例如,下面这些函数都不会使用终极模式加密,因为它们或者使用了不支持的特性,或者调 用了不支持的函数:
async def nested():
return 42
def foo1():
for n range(10):
yield n
def foo2():
frame = sys._getframe(2)
print('parent frame is', frame)
4.5.6. 已知的一些问题
当格式化字符串出现语法错误, BCC 模式抛出 SystemError: NULL object passed to Py_BuildValue 而不是 SyntaxError 或者 ValueError.
在测试 lib/python3.12/test/test_fstring.py 中下列用例时发现: - test_invalid_syntax_error_message - test_missing_variable - test_syntax_error_for_starred_expressions - test_with_a_commas_and_an_underscore_in_format_specifier - test_with_an_underscore_and_a_comma_in_format_specifier - test_with_two_commas_in_format_specifier - test_with_two_underscore_in_format_specifier
生成 BCC 代码时候,Pyarmor 有时候报错 RuntimeError: link bcc code failed
可以尝试增加额外的编译选项 -DENABLE_BCC_MEMSET 来解决这个问题。例如,在 32位 Windows 下面可以使用下面的命令增加额外的编译选项:
pyarmor cfg windows.x86.bcc:cflags += " -DENABLE_BCC_MEMSET"
或者直接修改配置文件
pyarmor/cli/default.cfg
,这个选项的最终有效值应该是:Section: windows.x86.bcc cflags = --target=i686-elf-linux -O3 -Wno-unsequenced -fno-asynchronous-unwind-tables -fno-unwind-tables -fno-stack-protector -fPIC -mno-sse -std=c99 -c -DENABLE_BCC_MEMSET