问题就那么发生了:
PHP的项目在生产服务器运行时,基本都是明文的文件,如果要保护代码的话,多数使用商业的加密软件,比如Zend Guard或者其他软件。zend也就是php程序语言Zend Engine语法分析引擎的公司出品。该产品会对php代码进行加密转换,费用大约是$600每年(大约4000人民币),按照公司授权。zend guard收费倒不是主要问题,如果单单购买加密功能的话,项目性能会奇差,相信很多朋友都遇到了,尤其是webgame中,刚开服时,几乎都是cpu 100%,load 奇高。使用时,需要服务端在装一个解密的程序,每个脚本文件,在每次被访问时,都需要解码、验证授权。这种繁琐的步骤,无疑浪费了时间、系统资源,也是我们一个项目当初在“友好邻邦”服务器上出现超高占用CPU的原因。(OS load占用高达100倍)
这些都是些什么玩意:
这产品是加密,如果想要程序执行流程优化的话,Zend又很“周到”的为我们提供另外一款产品 Zend Server,每年只需要最低支付$1695美元(大约11000人民币)(其他黄金版、白金版需要上万美元)。。。这样一来,我们的产品被绑架在zend上,如果我们需要定制功能的话,那是不可想象的。 如果是zend产品有bug,那我们只能干等着,等他们解决。除了zend这款,还有另外一款代码加密产品:ioncube,费用倒是不高,大约400美元每年。但用户量少,产品是否稳定不清楚,不开源,社区支持不是很好。
找寻:
有没有一款可以保护代码,社区支持好,用户量大,反馈处理及时,费用低的产品吗? 幸运的是,有这么一款开源产品APC:http://pecl.php.net/package/APC Alternative PHP Cache (APC)是一款php代码加速产品,该产品作为php程序的一个拓展,是一个开放自由的PHP opcode 缓存。它的目标是提供一个自由、 开放,和健全的框架用于缓存和优化PHP的中间代码。
早在3-4个月之前,鸟哥博客上一篇文章《关于PHP的编译和执行分离》中提到APC来作为PHP代码保护的方案。从文中可以看出,鸟哥的想法是每个php文件,导出一个opcode 的bin文件,加载时,也是挨个加载,这样也实现了代码保护,但一个项目几百个php文件的话,也得相应存在几百个bin文件,量比较大,操作比较复杂,管理不方便,不好做版本验证(以后会提到)。末学比较倾向于单个bin文件的导出,单个opcode bin文件的加载。而且,单个bin文件的加载,可以避免项目中出现部分文件跟整体版本不一致的情况发生,运维同事再也不用担心个别文件跟整个项目版本不一致的情况了。
原来是你:
APC是替换了php zend引擎的zend_compile_file函数,来决定是否重新对php脚本文件的读取,扫描、解释、编译等步骤。启用apc之后,将跳过读取、扫描、解释、编译这几步,节省内存、CPU开销,直接执行OPCODE。Apc还提供对php源码的opcode的缓存,导出到一个二进制文件中,还提供加载这个二进制文件。起初,我们开始使用这个功能时,遇到该拓展的多个BUG,都已经提交到php官方,分别是: BUG #62757 、BUG #62765 、BUG #62825 ,并积极配合php内核组开发成员重现BUG,抓获coredump,提取bug demo代码,在PHP官方成员-APC拓展开发组长Xinchen Hui(以下称鸟哥 laruence)的帮助下,很快的修复了这些BUG 。
如此繁琐:
这样仍存在一个繁琐的步骤,即php-fpm每次启动时,没有自动加载bin文件,需要管理员去手动执行或者访问脚本,(该脚本内使用apc_bin_load/apc_bin_loadfile函数去加载bin文件)让php-fpm加载bin文件,对于单大区多台前端,以及单服务器运行多个php-fpm主进程来说,如何判断那个php-fpm主进程是否成功加载,这是个很麻烦的问题。对于程序执行opcode清除之后,又需要再次加载,这样是个非常复杂繁琐的过程,也容易出问题。
自力更生:
可否在fpm启动时,自动加载bin的功能呢?末学阅读了APC的源码,照葫芦画瓢的添加了自动加载的opcode bin文件的功能。
我们是在 APC SVN http://svn.php.net/viewvc/pecl/apc/trunk/ 的 r327454版本基础上,在apc_module_init函数中,模块初始化时,增加了opcode bin文件的自动加载功能,patch代码见:https://github.com/cfc4n/cnxct/blob/master/apc_r327454_add_preload_binfile.patch。有兴趣的朋友可以git使用下。当然,末学水平有限,难免存在BUG,还请见谅。
后来,末学将此patch提交到PHP官方,请他们审核一下,是否可以收入apc官方中,免得我们以后都一直作为patch来使用。或者协助完善,或者给出更好的解决办法。
很伤心,未被采纳,原因是这是小众的需求。但末学表示不解,那apc_preload_path这个自动加载user cache的功能不也是小众需求吗?或许,这是他们委婉的拒绝方式。
(同时,还发现了APC自带的一个未公布的功能,自动加载user cache的功能,配置项为apc.preload_path,配置的值是目录地址,目录下可有多个文件,这些文件也应该是apc_bin_dump/apc_bin_dumpfile函数导出的user cache。看来,官方也有这么个打算。但为何没有实现 opcode bin文件 的自动加载呢?)
如何使用:
opcode bin文件导出与导入:
导出的php脚本,末学提供一份,需要的朋友,可以参照修改一下即可:
https://github.com/cfc4n/cnxct/blob/master/apc_dump.php
导出成功后,会在PROJECTROOT目录下面生成一个dumproot目录,dumproot目录中的所有文件,就是你需要上传到生产服务器上的文件。bin目录中,会生成两个文件,一个是opcode的bin文件,一个是生成之前的php文件的md5 hash值。其他目录就是项目被保护的目录,目录中文件都是空的php文件。将dumproot目录上传至生产服务器,保持项目路径正确,重启fpm即可。可通过apc拓展源码包内的apc.php查看被缓存文件数。如图:
版本更新、回滚:
遇到bug需要修复时,只需要在opcode bin文件导出服务器上,重新导出一个bin文件,将此bin文件上传到生产服务器,替换到之前的bin文件即可。偶尔会遇到项目因种种原因,需要对项目进行回滚,如果使用我们的方案之后,会发现所有php文件都已经是空的了。而我们的回滚更方便,只需要将之前需要回滚的版本的bin文件,替换到当前的文件即可。或者更改php.ini中apc.preload_binfile的路径。
版本检测
当怀疑个别文件不是某个版本的程序是,怎么办呢?apc提供了另外一个参数 apc.file_md5来存储被cache文件的md5 hash。但当使用已到处的opcode bin文件时,apc却没有重新抓去md5 hash,也没有重新存入apc中,导致我们看到的md5值是个错误的hash值。为此,末学斗胆做了修改,并提供了patch,且提交该BUG #63491到pecl apc官方,斗胆请官方采纳。当打上该patch之后,您可以在apc.php 看到这些信息。并且,跟导出bin文件时,生成的md5 hash文件中校对以确认哦。(2012/12/17 更新,官方已修复)
批量检测也是可以的,参加末学另一个脚本:https://github.com/cfc4n/cnxct/blob/master/check.php
APC其他已知bug:
一、php5.4.x中,class中的array()静态成员属性在跨服务器使用时,产生core,已经提交到 BUG #63636(此bug由recoye发现)
临时解决方案如下:
使用php5.4.x时,不要在class中定义 $_user = array(),直接定义为 $_user;
不要使用php5.4.x
删除或注释apc_compile.c的1992、2003行(apc 3.1.13),即 zval_ptr_dtor(&src->default_static_members_table[i]); 字样的代码。
等待PHP官方解决,或微博@鸟哥 ,催鸟哥解决
二、web访问apc_dump.php 去生成的bin文件,在cli模式下使用时,会在RSHUTDOWN时,即请求结束时,回收系统资源时,会产生core。(该结果的php版本为php5.3.x,初步确认为上一个bug有密切关系,暂未提交至bugs.php.net)
这两个bug以末学的理解来看,与末学的patch无关,为apc的bug。
注意事项:
导出bin文件的服务器上,不要开启 apc.preload_binfile ,避免bin文件存在时,使用bin文件内的代码,而不使用php文件内的代码,导致导出的代码不是你想要的。
php5.4.x class中array 静态成员以及属性
操作系统32bit、64bit的导出的bin文件不能相互乱用。(乱用将产生core)
导出服务器的web路径跟目标生产服务器的web路径一致
生产服务器使用bin文件之后,必须存在同名的php文件,内容可以为空。
apc.ttl、apc.gc_ttl参数保持为0
apc.serializer 如果改用其他序列化函数发生问题,那么请保持为空,或者为 apc.serializer=php
apc.num_files_hint的值务必大于等于cache files 的数量,可以先设置大点,再通过生产服务器上观察一段时间来确认
啰嗦一下:
关于32bit\64bit服务器上导出opcode bin文件的乱用问题,将产生如下core
01 |
Program received signal SIGSEGV, Segmentation fault. |
02 |
0x00002aaab0af8442 in apc_unswizzle_bd (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:598 |
03 |
598 if (bd->swizzled_ptrs[i]) { |
04 |
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.80.el6_3.6.x86_64 libxml2-2.7.6-8.el6_3.3.x86_64 nss-softokn-freebl-3.12.9-11.el6.x86_64 zlib-1.2.3-27.el6.x86_64 |
05 |
(gdb) bt |
06 |
#0 0x00002aaab0af8442 in apc_unswizzle_bd (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:598 |
07 |
#1 apc_bin_load (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:887 |
08 |
#2 0x00002aaab0ae829b in zif_apc_bin_loadfile (ht=, return_value=0x2aaaaaaf1f88, return_value_ptr=, this_ptr=, return_value_used=) |
09 |
at /home/cfc4n/APC-3.1.13/php_apc.c:1549 |
10 |
#3 0x00000000006ef31a in zend_do_fcall_common_helper_SPEC (execute_data=) at /home/cfc4n/php-5.4.8/Zend/zend_vm_execute.h:642 |
11 |
#4 0x00000000006dca10 in execute (op_array=0x2aaaaaaf16a8) at /home/cfc4n/php-5.4.8/Zend/zend_vm_execute.h:410 |
12 |
#5 0x0000000000676f7e in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/cfc4n/php-5.4.8/Zend/zend.c:1309 |
13 |
#6 0x000000000061c7ce in php_execute_script (primary_file=0x7fffffffe2a0) at /home/cfc4n/php-5.4.8/main/main.c:2482 |
14 |
#7 0x000000000071c763 in do_cli (argc=2, argv=0x7fffffffe6a8) at /home/cfc4n/php-5.4.8/sapi/cli/php_cli.c:988 |
15 |
#8 0x000000000071ce64 in main (argc=2, argv=0x7fffffffe6a8) at /home/cfc4n/php-5.4.8/sapi/cli/php_cli.c:1364 |
16 |
(gdb) f 0 |
17 |
#0 0x00002aaab0af8442 in apc_unswizzle_bd (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:598 |
18 |
598 if (bd->swizzled_ptrs[i]) { |
19 |
(gdb) l |
20 |
593 bd->crc = crc_orig; |
21 |
594 |
22 |
595 UNSWIZZLE(bd, bd->entries); |
23 |
596 UNSWIZZLE(bd, bd->swizzled_ptrs); |
24 |
597 for (i=0; i < bd->num_swizzled_ptrs; i++) { |
25 |
598 if (bd->swizzled_ptrs[i]) { |
26 |
599 UNSWIZZLE(bd, bd->swizzled_ptrs[i]); |
27 |
600 if (*bd->swizzled_ptrs[i] && (*bd->swizzled_ptrs[i] < ( void *)bd)) { 601 UNSWIZZLE(bd, *bd->swizzled_ptrs[i]); |
28 |
602 } |
因为long之类类型,在不同bit操作系统上的长度不一致导致内存越界的BUG。 末学不知道这算不算bug,或者您别混用,或者APC程序上也可以解决…
惊喜:
不光可以代码保护,性能提升,方便运维,还可以防黑客入侵哦。(项目目录不允许创建新文件,那么即使黑客改了php脚本,php仍会去apc里读,也不会去读取它。哪怕黑客执行了 apc_cache_clean清除opcode cache,我们的补丁仍会自动加载,那么黑客的行为还是阻拦了。)
备注:
此patch已经在我们的“友好邻邦”服务器上稳定运行3-4个月左右,无异常案例,各位可放心使用。
Apc不算一个php encoder,算是个cache,一个opcode cache,起到中间码缓存的作用,故本文中用“代码保护”,而不是“代码加密”。 但实质上实现了我们的目的,起到保护源代码的作用,那怕可以逆向,也是有一定难度的。
PS:感谢Ivon的openstack,感谢终极修炼师的协助测试。
PPS:
如果你能保证php文件不会被SAPI形式访问到的话,只会被include/require等访问到的话,甚至连同名空文件都不用放。比如单入口框架的项目,只要放个空的入口文件即可。 (感谢recoye纠正)
缓存450多个文件,命中率100%,只是20个配置文件没导出在bin文件里,导致第一次没命中。