Linux下C++实现PHP扩展

目前堡垒机在数据库中的密码存储为rc4加密的密文,以前PHP在读取密码后需要调用后台系统命令,通过读取后台系统命令的stdout来获取解密后的明文密码。

这种调用方式对于一次的加密或解密,这样一次系统命令的调用时间成本可能在10ms,并不会产生太大的问题。而目前一个校验所有账户密码复杂度的需求要求在加载一个页面的时候校验所有账号的密码是否符合要求,如果账号规模比较庞大的时候,问题来了,2w个账号时,请求的响应时间达到了5m19s,而如果注释掉解密的部分,响应时间在2-3s。 为了优化这个问题,我们首先尝试使用xmlrpc的方式调用python的c++扩展来提高响应的速度,效果也不错,响应时间提高到了20s左右。经过排查后发现,2w条数据在xmlrpc调用时序列化和反序列化的时间占用了20s中的15s。为了进一步提高响应速度,而加解密的源码又是c++的代码实现,于是想到了使用c++来实现一个PHP的扩展,可能效果会比较好,于是开始动手实现这个rc4的PHP扩展。

使用C++编写PHP扩展

1 准备工作

首先在开发机上获取堡垒机php对应版本的源码并解压。

yangshuofei@IPS-dev3 ~/src $ tar xvf php-5.5.25.tar.bz2

2 编写config文件

config.m4 文件是编译基础中最核心的文件,这个文件主要是用autoconf 来产生configure 配置文件,继而自动生成大家所熟悉的Makefile文件。
可以自己书写config.m4文件,也可以由Shell脚本ext_skel 来生成样板,这里我采用的是后者。
进入源码的ext文件夹,执行./ext_skel命令如下:

yangshuofei@IPS-dev3 ~/src $ cd php-5.5.25/ext/
yangshuofei@IPS-dev3 ~/src/php-5.5.25/ext $ ./ext_skel --extname=rc4
Creating directory rc4
Creating basic files: config.m4 config.w32 .svnignore rc4.c php_rc4.h CREDITS EXPERIMENTAL tests/001.phpt rc4.php [done].

To use your new extension, you will have to execute the following steps:

1.  $ cd ..
2.  $ vi ext/rc4/config.m4
3.  $ ./buildconf
4.  $ ./configure --[with|enable]-rc4
5.  $ make
6.  $ ./sapi/cli/php -f ext/rc4/rc4.php
7.  $ vi ext/rc4/rc4.c
8.  $ make

Repeat steps 3-6 until you are satisfied with ext/rc4/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.

在ext目录下多了一个叫rc4的目录。进入该目录,会发现目录下有几个文件:

yangshuofei@IPS-dev3 ~/src/php-5.5.25/ext $ cd rc4
yangshuofei@IPS-dev3 ~/src/php-5.5.25/ext/rc4 $ ls -l
total 32
-rw-r--r-- 1 yangshuofei yangshuofei    3 Mar 14 17:27 CREDITS
-rw-r--r-- 1 yangshuofei yangshuofei    0 Mar 14 17:27 EXPERIMENTAL
-rw-r--r-- 1 yangshuofei yangshuofei 1954 Mar 14 17:27 config.m4
-rw-r--r-- 1 yangshuofei yangshuofei  275 Mar 14 17:27 config.w32
-rw-r--r-- 1 yangshuofei yangshuofei 2787 Mar 14 17:27 php_rc4.h
-rw-r--r-- 1 yangshuofei yangshuofei 5002 Mar 14 17:27 rc4.c
-rw-r--r-- 1 yangshuofei yangshuofei  493 Mar 14 17:27 rc4.php
drwxr-xr-x 2 yangshuofei yangshuofei 4096 Mar 14 17:27 tests

然后可以根据提示来修改config.m4文件,这里有几个重要的宏命令如下:

  • dnl 是注释;
  • PHP_ARG_WITH或者PHP_ARG_ENABLE指定了PHP扩展模块的工作方式,前者意味着不需要第三方库,后者正好相反;
  • PHP_REQUIRE_CXX用于指定这个扩展用到了C++;
  • PHP_ADD_INCLUDE 指定PHP扩展模块用到的头文件目录;
  • PHP_CHECK_LIBRARY指定PHP扩展模块PHP_ADD_LIBRARY_WITH_PATH定义以及库连接错误信息等;
  • PHP_ADD_LIBRARY(stdc++,””,EXTERN_NAME_LIBADD)用于将标准C++库链接进入扩展;
  • PHP_SUBST(EXTERN_NAME_SHARED_LIBADD)用于说明这个扩展编译成动态链接库的形式;
  • PHP_NEW_EXTENSION 用于指定有哪些源文件应该被编译,文件和文件之间用空格隔开;

ext_skel默认生成的模块框架是针对C的,我们要使用C++进行PHP扩展,那除以上的PHP_REQUIRE_CXX,PHP_ADD_LIBRARY两个宏必需外,还要把rc4.c改名成rc4.cc。

需要注意的是,在config.m4里面也可以使用类似的Makefile语法,片段如下:

PHP_ARG_WITH(rc4, for rc4 support,
Make sure that the comment is aligned:
[  --with-rc4             Include rc4 support])
...
  COMMON_PATH="../../common"
  PHP_REQUIRE_CXX()
  PHP_SUBST(RC4_SHARED_LIBADD)
  PHP_ADD_INCLUDE($COMMON_PATH/crypt)
  PHP_ADD_LIBRARY(stdc++, 1, RC4_SHARED_LIBADD)
  PHP_ADD_LIBRARY_WITH_PATH(SashCommon, $COMMON_PATH, RC4_SHARED_LIBADD)
  CCFILES="rc4.cc"
  PHP_NEW_EXTENSION(rc4, $CCFILES, $ext_shared)
...

3 编写.h文件

修改php_rc4.h这个头文件。
由于TSRM.h这个文件所包含的函数和类都是用纯C语言写的,故应该使用extern来说明如下:

extern "C" {
#ifdef ZTS
#include "TSRM.h"
#endif
}

添加php函数声明如下:

PHP_FUNCTION(rc4_encrypt);  /* For encrypt string. */
PHP_FUNCTION(rc4_decrypt);  /* For decrypt string. */

如果该php_rc4.h头文件或者rc4.cc文件用到了C++语言中的一些容器或者一些函数,则需要在头文件中包含相应的c++库的头文件,否则会出现找不到相应的C++函数错误。

4 编写.cc文件

修改rc4.cc这个文件。
由于config.h、php.h、php_ini.h和ext/standard/info.h中包含的函数和类如TSRM.h一样,都是用纯C语言写的,所以也需要用extern说明如下:

extern "C" {
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
}
#include "php_rc4.h" 

而#include “php_ext_name.h”这句则已经不需要包含在extern “C”内,另外,ZEND_GET_MODULE这个宏命令也是需要特别申明如下:

#ifdef COMPILE_DL_RC4
BEGIN_EXTERN_C()
ZEND_GET_MODULE(rc4)
END_EXTERN_C()
#endif

总之,把一些C写的库或h用兼容的方式给解决。
由于已有现成的c++实现,于是引入相关头文件:

 #include "Crypt.h"

添加相关函数:

const zend_function_entry rc4_functions[] = {
    PHP_FE(rc4_encrypt, NULL)       /* For encrypt string. */
    PHP_FE(rc4_decrypt, NULL)       /* For decrypt string. */
    PHP_FE_END  /* Must be the last line in rc4_functions[] */
};

添加函数实现如下:

PHP_FUNCTION(rc4_encrypt)
{
    char *arg = NULL;
    int arg_len, len;
    const char *key = "key";
    int key_len = sizeof("key") - 1; 
    char *strg;
    const char *result = NULL;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s", 
        &arg, &arg_len, &key, &key_len) == FAILURE) {
        return;
    }
    result = SASH::Crypt::encryptWithKey(arg, key).c_str();

    len = spprintf(&strg, 0, "%s", result);
    RETURN_STRINGL(strg, len, 0);
}

函数自身的定义使用了宏PHP_FUNCTION(),该宏可以生成一个适合于Zend引擎的函数原型。逻辑本身分成语义各部分,取得调用函数的参数和逻辑本身。

为了获得函数传递的参数,可以使用zend_parse_parameters()API函数。下面是该函数的原型:

 zend_parse_parameters(int num_args TSRMLS_CC, char *type_spec, …);

zend_parse_parameters()函数大部分的代码看起来几乎都一样。ZEND_NUM_ARGS()向Zend Engine提供关于接收到的参数数量,TSRMLS_CC被用来保证线程安全,最后函数会返回SUCCESS或者FAILURE。在普通情况下zend_parse_parameters()将会返回SUCCESS;如果一个调用脚本传递的参数数量超过了函数定义的参数数量,或者传递的参数不能转换成合适的数据类型,Zend将会自动的输出一个错误信息,然后函数会优雅地把控制权返回给调用脚本。

可能指定的类型

可能指定的类型

表中的zval是用来存储PHP中所有用户空间变量的真实数据类型。三个“复杂”的数据类型,Resource, Array和Object,当他们的数据类型字母标示在zend_parse_parameters()中被使用的时候,Zend Engine会对其进行类型检查,但是它们在C语言中没有相对应的数据类型,所以不会有任何转换会被实际执行。

由于解密有默认key,所以我在rc4_encrypt中采用了可变参数”s|s”,可以这样来解读:我首先需要一个字符串类型的参数,然后一个管道字符说明接下来的参数列表是可选的(如果一个可选的参数在函数调用过程中没有被传递,那么zend_parse_parameters()不会改变已经传给它的参数值),后面的s表示我需要一个可选的字符串参数。后面的&arg, &arg_len, &key, &key_len以引用的方式传递进来,所以zend_parse_parameters()可以用参数值来填充它们。(扩展还支持可变参数 )

从C++函数向PHP返回值,并不能使用通常的return语句,Zend将返回值地址作为参数传给我们,return_value是Zend为我们预先创建的一个标准zval结构,相当于一个局部变量,用户获得返回值时就相当于对return_value进行赋值操作,我们只需填充它即可;return_value_used表明用户是否使用了返回值,0表明没有使用返回值,当函数结束后return_value的refcount将被减为0,并被销毁,因此,这种情况下完全可以不处理返回值;return_value_ptr用于返回引用,它需要和zend_function_entry.arg_info联合使用,通常都是NULL。

用于填充return_value的一组宏

用于填充return_value的一组宏

这些宏将在填充完return_value后,执行return语句。如果不想return,可以改用相应RETURN_xxx宏的RETVAL_xxx版本。
在这里我们使用了RETURN_STRINGL(strg, len, 0);来返回加密后的字符串。

5 生成.so

使用phpize生成configure(由于和开发机php版本不同,所以这里使用的是自己编译的phpize):

yangshuofei@IPS-dev3 ~/src/php-5.5.25/ext/rc4 $ /home/yangshuofei/php/bin/phpize  
Configuring for:
PHP Api Version:         20121113
Zend Module Api No:      20121212
Zend Extension Api No:   220121212

生成Makefile(由于和开发机php版本不同,所以这里使用的是自己编译的php-config):

 yangshuofei@IPS-dev3 ~/src/php-5.5.25/ext/rc4 $ ./configure 
 --with-php-config=/home/yangshuofei/php/bin/php-config

使用phpize生成configure执行文件后,可以使用./configure –help查看帮助信息,修改config.m4文件可以修改configure的帮助信息。每次修改了config.m4文件,需要使用清除临时文件 命令phpize –clean来完成消除configure。

生成.so,生成后会放在modules文件夹下:

yangshuofei@IPS-dev3 ~/src/php-5.5.25/ext/rc4 $ make
...
----------------------------------------------------------------------
Libraries have been installed in:
   /home/yangshuofei/src/php-5.5.25/ext/rc4/modules

If you ever happen to want to link against installed libraries
in a given directory, LIBDIR, you must either use libtool, and
specify the full pathname of the library, or use the `-LLIBDIR'
flag during linking and do at least one of the following:
   - add LIBDIR to the `LD_LIBRARY_PATH' environment variable
     during execution
   - add LIBDIR to the `LD_RUN_PATH' environment variable
     during linking
   - use the `-Wl,--rpath -Wl,LIBDIR' linker flag
   - have your system administrator add LIBDIR to `/etc/ld.so.conf'

See any operating system documentation about shared libraries for
more information, such as the ld(1) and ld.so(8) manual pages.
----------------------------------------------------------------------

Build complete.
Don't forget to run 'make test'.

6 在设备上使用

根据php.ini中的配置:

extension_dir = /etc/php/apache2-php5.3/ext

将生成的rc4.so文件放在设备的放在/etc/php/apache2-php5.3/ext文件夹下,
在/etc/php/apache2-php5.3/ext-active文件夹添加rc4.ini内容如下:

extension= rc4.so

然后重启apache:

Develop>apache2ctl restart

接下来就可以在php文件中调用so中的rc4_encrypt和rc4_decrypt函数来进行加解密了。

修改前后性能对比

使用扩展前响应时间5m19s

使用扩展前响应时间

使用扩展前响应时间

使用扩展后响应时间2.73s

使用扩展后响应时间

使用扩展后响应时间

如果您需要了解更多内容,可以
加入QQ群:486207500
直接询问:010-68438880-8669

Spread the word. Share this post!

Meet The Author

Leave Comment