使用APC来保护PHP代码

问题就那么发生了:
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 #62757BUG #62765BUG #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发现)
临时解决方案如下:

    1. 使用php5.4.x时,不要在class中定义 $_user = array(),直接定义为 $_user;
    2. 不要使用php5.4.x
    3. 删除或注释apc_compile.c的1992、2003行(apc 3.1.13),即 zval_ptr_dtor(&src->default_static_members_table[i]); 字样的代码。
    4. 等待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

Program received signal SIGSEGV, Segmentation fault.
0x00002aaab0af8442 in apc_unswizzle_bd (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:598
598             if(bd->swizzled_ptrs[i]) {
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
(gdb) bt
#0  0x00002aaab0af8442 in apc_unswizzle_bd (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:598
#1  apc_bin_load (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:887
#2  0x00002aaab0ae829b in zif_apc_bin_loadfile (ht=, return_value=0x2aaaaaaf1f88, return_value_ptr=, this_ptr=, return_value_used=)
    at /home/cfc4n/APC-3.1.13/php_apc.c:1549
#3  0x00000000006ef31a in zend_do_fcall_common_helper_SPEC (execute_data=) at /home/cfc4n/php-5.4.8/Zend/zend_vm_execute.h:642
#4  0x00000000006dca10 in execute (op_array=0x2aaaaaaf16a8) at /home/cfc4n/php-5.4.8/Zend/zend_vm_execute.h:410
#5  0x0000000000676f7e in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/cfc4n/php-5.4.8/Zend/zend.c:1309
#6  0x000000000061c7ce in php_execute_script (primary_file=0x7fffffffe2a0) at /home/cfc4n/php-5.4.8/main/main.c:2482
#7  0x000000000071c763 in do_cli (argc=2, argv=0x7fffffffe6a8) at /home/cfc4n/php-5.4.8/sapi/cli/php_cli.c:988
#8  0x000000000071ce64 in main (argc=2, argv=0x7fffffffe6a8) at /home/cfc4n/php-5.4.8/sapi/cli/php_cli.c:1364
(gdb) f 0
#0  0x00002aaab0af8442 in apc_unswizzle_bd (bd=0x2aaaaaaf3798, flags=) at /home/cfc4n/APC-3.1.13/apc_bin.c:598
598             if(bd->swizzled_ptrs[i]) {
(gdb) l
593         bd->crc = crc_orig;
594
595         UNSWIZZLE(bd, bd->entries);
596         UNSWIZZLE(bd, bd->swizzled_ptrs);
597         for(i=0; i < bd->num_swizzled_ptrs; i++) {
598             if(bd->swizzled_ptrs[i]) {
599                 UNSWIZZLE(bd, bd->swizzled_ptrs[i]);
600                 if(*bd->swizzled_ptrs[i] && (*bd->swizzled_ptrs[i] < (void*)bd)) { 601                     UNSWIZZLE(bd, *bd->swizzled_ptrs[i]);
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文件里,导致第一次没命中。

PPPS:
更新日期:2014/10/17
近来,好多朋友问我要完整版,鉴于apc已经被官方删了,那个SVN地址无法正常checkout,故我在硬盘里找了个压缩包,应该是可用的版本,如果有其他异常,请留言,谢谢。
适用于PHP5.3的完整拓展:点击这里下载包含patch的完整版apc

关注微信公众号,手机阅读更方便: 程序员的阅微草堂

知识共享许可协议莿鸟栖草堂CFC4N 创作,采用 知识共享 署名-非商业性使用-相同方式共享(3.0未本地化版本)许可协议进行许可。基于http://www.cnxct.com上的作品创作。转载请注明转自:使用APC来保护PHP代码

43 thoughts on “使用APC来保护PHP代码

  1. 很感谢你的分享,让我看到了摆脱zend guard的曙光。于是在用你的apc_dump.php windows上试验。提示“apc_bin_load string argument does not appear to be a valid APC binary dump due to size (2947 vs expected 2946),尝试了减少生成bin的php文件,问题依旧。希望博主给予解惑。不胜感激。

  2. bin_dump的文件怎么使用呢,网上找到的代码都不够详细,手册上也没有示例。
    我的代码:
    1 被dump的文件(/www/f1.php):

    &lt;?php
    echo \’GO GO GO 03&gt; \’.chr(10);
    function dosome(){
    echo date(\’Y-m-d H:i:s\’);
    }

  3. 2 执行dump的文件(/www/f2.php):
    &lt;?php
    $filename = \’/www/f1.php\’;
    $cache_filename = $filename.\’.bin\’;

    apc_bin_dumpfile(array($filename),null,$cache_filename);
    apc_bin_loadfile($cache_filename);

    dosome();
    echo $cache_filename.\’ loaded\’;

  4. 通过浏览器访问:
    http://aa.com/f2.php

    报错:
    PHP Fatal error: Call to undefined function dosome() in /www/f2.php on line 8

    =====================================
    求教,这个两个函数到底改如何使用?!!

  5. 因为字数限制,所以只能分3条(加这条共4条)发送了,请见谅,请从我发表的第一条看,谢谢了!

  6. 博主,我使用你的脚本导出了BIN文件,但是在对生成的空PHP文件进行访问时,什么也没有。
    还有我发现所有的BIN文件均为45个字节,无论原文件多大。求解答!谢谢了

    • hi,对空文件访问时,没任何显示的话,最好确定一下preloadfile的路径是否正确。 当执行导出之后,会把导出的文件清空的。 当你再次导出时,千万要记得,重新覆盖源文件,不然,多次导出时,会把清空后的空文件来导出的。 我猜下,你的情况应该是导出了情况后的文件。

  7. windows下面已经生成bin文件,请问怎么自动加载这些缓存的bin文件?这个问题很菜鸟,还是希望能大神点明,谢谢

    • 抱歉,这个补丁不适合win上的isapiapache2handler上做自动加载。 win上很难做以后的多webserver扩展。。。

  8. 你好,请问下如何打自动加载的apc补丁以及如何加载bin文件和md5文件,能否详细的写一篇文章出来呢?
    最好有图文,这样可以促进和帮助更多的人了解和熟知apc,请博主三思,感谢博主!

  9. 你好, 我在使用apc_bin_dumpfile函数的时候,总是会提醒我,但是有些比这个文件更大内容更多的文件却不会有这样的提醒。我是否需要配置某些apc的参数?
    Fatal error: apc_bin_dumpfile(): Exceeded bounds check in apc_bd_alloc_ex by 10 bytes. in /var/www/xtobject/www/func.php on line 45

    该行的代码为:
    $compiled = $compiled && apc_bin_dumpfile ( array($file), null, $file.’.bin’ );

    该缓存的详细资料如下:
    array(11) {
    ["type"]=>
    string(4) “file”
    ["device"]=>
    int(0)
    ["inode"]=>
    int(0)
    ["filename"]=>
    string(49) “/var/www/web/app/actions/user_server.php”
    ["num_hits"]=>
    float(0)
    ["mtime"]=>
    int(1425123147)
    ["creation_time"]=>
    int(1425123160)
    ["deletion_time"]=>
    int(0)
    ["access_time"]=>
    int(1425123160)
    ["ref_count"]=>
    int(0)
    ["mem_size"]=>
    int(28976)

  10. 太可惜了。我觉得你应该把这个写成一个通用的。不管在什么样的系统,主要用apc就很轻易实现这个。对php来说,那是革命性的突破 – “php的程序也可以编译发布!!!”。怎么php官方不考虑这种方法??

  11. 博主,你好!
    现在APC已经有支持PHP5.5的版本了,但是我按照文中提供的两个patch进行了修改,但是还是不能加载bin文件
    现在很多服务器的PHP版本都远高于PHP5.3了,若博主方便能否提供一份支持PHP5.5的完整包。
    这是支持php5.5的APC下载地址:http://git.php.net/?p=pecl/caching/apc.git;a=snapshot;h=08e2ce7ab5f59aea483d877e2bc19bb1a5bcc34f;sf=tgz
    万分感谢!

发表评论

电子邮件地址不会被公开。 必填项已用*标注