用了这么多年的PHP,还真有幸地做过几次扩展,以前做的扩展,大都是在windows平台上,然后用的COM组件实现,也有一次写的应用程序,然后用TCP协议调用函数,却从来没有尝试过去写PHP模块,正好这次的PHP产品又有做扩展的需要,服务器也是多种操作系统的,这样的情况,何不尝试用PHP模块的方式来实现呢。我使用了NASM 汇编语言,这种开源的,跨平台的汇编语言。
为什么用汇编语言呢,其实PHP用C语言写模块的话,会有很多的宏调用,方便快捷,而我要用汇编语言去写第一个模块,主要就是想从PHP的文档和源代码共同地去研究PHP的内部,真正深入地了解它的执行。以后的开发肯定还是要用C语言的,因为它不仅仅是跨平台了,还可以跨CPU架构。
开始研究文档,PHP文档《PHP at the Core: A Hacker’s Guide to the Zend Engine》一章里很详细地讲述了PHP内核的运行机制及模块的加载方式,加上官方网站上下载的源代码,一切都不是秘密了。
PHP的模块有两种方式,一种是build-in方式,这种方式的模块和PHP,ZEND引擎一起编译作为PHP内核的一部分。另一种就是动态模块,这种模块更灵活,需要在php.ini中配置加载此模块,然后导出get_module函数,此函数向内核提供模块的具体信息,然后其它的实现,跟build-in方式就差不多了。
现在就说动态模块吧,动态模块肯定是动态链接库,即是windows上的dll或linux上的so文件,这个库肯定需要导出get_module函数,get_module函数由PHP内核加载模块时调用,你需要返回一个zend_module_entry结构体指针给内核,以让内核知道你的模块提供哪些函数给PHP,可以设置其它属性什么的,还可以设置一些钩子函数。比如phpinfo的钩子,页面启动,结束的钩子,服务器启动结束的钩子等。本来文档里写得很详细,但这个结构体还是挺重要的,写出来分析一下吧:
struc zend_module_entry ;模块结构,应为88字节 .size resw 1 ;结构体大小88字节 resw 1 ;这个是C的规定,结构体的类型对齐,很重要的! .zend_api resd 1;api版本,一般要与内核版本一样 .zend_debug resb 1 ;一些标记 .zts resb 1 resb 2 .ini_entry resd 1 ._zend_module_dep resd 1 .name resd 1;文件名 .functions resd 1;函数列表,是zend_function_entry数组,包含你的模块提供的所有函数 .startup_func resd 1 ;一些钩子函数 .shutdown_func resd 1 .active_func resd 1 .deactive_func resd 1 .info_func resd 1 ;这个是phpinfo函数回调函数,可以输出些好玩的 .version resd 1;版本和一些其它变量,最后几个是内核用的,内核用的我们保留0 .globals_size resd 1 .globals_ptr resd 1 .globals_ctor resd 1 .globals_dtor resd 1 .post_deactivate_func resd 1 .module_started resd 1 .type resb 1 resb 3 .handle resd 1 .module_number resd 1 endstruc
上面的functions指向模块提供的函数表,我们以前使用过很多php模块,他们都会按照这个标准,把函数表填在这儿。如果你感趣,可以在windows系统上动态加载php_mysql库,调用它的get_module函数,便可以取得模块详细信息和函数表信息。
函数表是由一个个的function_entry结构组成,最后由三个DWORD的0结束。Function_entry结构也是很重要的,这儿也列出来看看:
struc zend_function_entry .fname resd 1 ;PHP能直接调用的函数名 .handler resd 1 ;模块里的函数实际地址 .arg_info resd 1 ;参数信息,有了它文档都可以不写了,但是一般的懒人都会让它是0的。 .num_args resd 1 ;参数个数,一般用-1不限制参数 .flags resd 1 ;一般为0 Endstruc
有了上面的一些概念,我们可以先来构造一个模块对象,就叫它hoverlees吧,然后在后面一步一步的去实现它。
module_name db ‘hoverlees’,0 hoverlees_version db ‘1.0’,0 funcName1 db ‘hoverlees_add’,0 funcName2 db ‘hoverlees_get_name’,0 hoverlees_functions istruc zend_function_entry at zend_function_entry.fname, dd funcName1 at zend_function_entry.handler, dd Hov_add at zend_function_entry.arg_info, dd 0 at zend_function_entry.num_args, dd -1 at zend_function_entry.flags, dd 0 iend istruc zend_function_entry at zend_function_entry.fname, dd funcName2 at zend_function_entry.handler, dd Hov_getname at zend_function_entry.arg_info, dd 0 at zend_function_entry.num_args, dd -1 at zend_function_entry.flags, dd 0 iend dd 0,0,0,0,0,0 hoverlees_module istruc zend_module_entry at zend_module_entry.size, dw 88 at zend_module_entry.zend_api, dd 20060613 at zend_module_entry.zend_debug, db 0 at zend_module_entry.zts, db 0 at zend_module_entry.ini_entry, dd 0 at zend_module_entry._zend_module_dep, dd 0 at zend_module_entry.name, dd module_name at zend_module_entry.functions, dd hoverlees_functions at zend_module_entry.startup_func, dd hoverlees_startup at zend_module_entry.shutdown_func, dd hoverlees_shutdown at zend_module_entry.active_func, dd hoverlees_active at zend_module_entry.deactive_func, dd hoverlees_deactive at zend_module_entry.info_func, dd hoverlees_phpinfo at zend_module_entry.version, dd hoverlees_version at zend_module_entry.globals_size, dd 0 at zend_module_entry.globals_ptr, dd 0 at zend_module_entry.globals_ctor, dd 0 at zend_module_entry.globals_dtor, dd 0 at zend_module_entry.post_deactivate_func, dd 0 at zend_module_entry.module_started, dd 0 at zend_module_entry.type, db 0 at zend_module_entry.handle, dd 0 at zend_module_entry.module_number, dd 0 iend
上面的表中构造了一个名为hoverlees的模块,这个模块提供2个函数供PHP脚本调用,这两个函数是hoverlees_add,hoverlees_get_name,打算把它们做成PHP的函数声明如下:
function hoverlees_add($a,$b) 返回$a+$b
function hoverlees_get_name() 返回一个字符串。
20060613是API版本,一般来说这个版本要与PHP的内核版本相同。hoverlees_startup,,hoverlees_shutdown,hoverlees_active,hoverlees_deactive是PHP状态变化时的钩子。你可以在服务器或PHP状态发生变化时处理些事情。hoverlees_phpinfo是phpinfo的钩子,当用户执行phpinfo()时,每个模块都有机会输出些信息出来。Version嘛就随便你写了。
对象有了,接下来要完成对象的传递处理,你的库肯定要导出get_module函数。现在我们就来实现导出它吧,上面已经实例化了一个zend_module_entry结构,所以这个函数就很简单,只需要返回我们实例的对象即可。
global get_module get_module: enter 0,0 mov eax,hoverlees_module leave ret
现在我们来实现一些函数,首先就是PHP的phpinfo钩子,当脚本执行函数phpinfo时,会遍历每一个扩展提供的钩子函数,这个函数可以调用php内部提供的一些打表函数出表格。这些打表函数例如:
php_info_print_table_start //开始表格
php_info_print_table_header //输出表头
php_info_print_table_row //输出行
php_info_print_table_end //结束表格
在phpinfo里执行上面的函数,就可以在phpinfo中输出一个表格了,表格的样子如下图所示,其实大家都看多了。
不过当然啦,你也可以使用php内部提供的php_printf函数输出一些其它的信息,如图像标签什么的,就可以在phpinfo页面里输出图片了。下面这个图就是输出图片后的样子。输出的时候注意这一页img标签本身是向右浮动的属性,设置成不浮动后就可以居中了。
但是事实上,phpinfo是给开发人员看的,有没有图片谁会在意?就算有图片,人们也不会因为图片而对这个模块高度评价,而肯定会因为模块提供的优秀功能而高度评价。
接下来是提供函数,模块不提供函数那干嘛呢。。。首先要讨论变量,在PHP的内部,变量是用一个zval结构表示的,zval结构存有变量的类型和数据,它跟Windows COM编程里的VARIANT结构基本上差不多的意思,变量的类型包括null(空),long(整数),string(字符串), object(对象),array(数组),double(浮点)等类型。Zval结构声明如下:
struc zval .value resd 2 ;真正的数据存在这里 .refcount resd 1 .type resb 1 ;数据类型 .is_ref resb 1 resb 2 endstruc
上面的结构体,value其实是zend_value联合体,长度是double长度(32位机器是8个字节),如果要表示字符串,即是前4个字节是字符串内容,后4个字节是字符串长度。其它类型数据均使用前4个字节。(NASM好像是没有联合体的,汗。。)
那么,
对于一个整数100,它的zval表示就是 type是LONG(1),value的前四个字节是100.
对于一个字符串“100”,它的zval表示就是type是STRING(6),value前四个字节是字符串指针,后四个字节是字符串长度(4)
对于数组,type就是ARRAY(4),value前四个字节就是数组的hash表指针。
其它的类似。
所以,A+B其实就是把A的zval里的值跟B的zval里的值相加。再返回一个新的zval就行了。
模块提供的函数必须声明成如下格式:
funcname: %define ht ebp+8 ;哈希表,没用到过 %define return_value ebp+12 ;指向返回值zval,有数据返回就填写这个结构体。 %define return_value_ptr ebp+16 ;返回值指针,没用到过 %define this_ptr ebp+20 ;对象指针,应该是在$obj->func时才会有用 %define return_value_used ebp+24 ;这个没用到过 取得函数参数的方法,就是使用zend_get_parameters_ex函数,这个函数调用例子如下: zend_get_parameters_ex(1,¶m1); zend_get_parameters_ex(2,¶m1,¶m2); zend_get_parameters_ex(3,¶m1,¶m2,¶m3); …
就是说要几个参数,第一个参数就是几,后面就是接受这些参数的指针,函数成功后它们各指向一个zval结构,代表PHP传进来的参数。
所以像上面的A+B函数,首先调用zend_get_parameters_ex取得两个参数,然后将两个zval相加结果放到return_value中返回。因为PHP是个弱类型的脚本语言,所以最好可以先判断一下两个参数的类型是不是字符串类型,如果是,相加时要用转换成整型的值,不然结果肯定是不正确的。
模块中当然也可以调用外部函数,对象的函数,和直接修改外部变量的值。比如我们提供的模块里调用了用户提供的PHP函数,修改了用户的全局变量都是可以的,这里要用到一系列的zend_hash_*函数操作hash表。全局变量的hash表和全局函数的hash是由PHP内核导出的。调用函数就用call_user_function_ex函数调用,call_user_function_ex函数不仅可以调用全局的函数,还可以调用对象里的函数,这个,就看参数怎么传了。
executor_globals这个符号就是内核导出的一个有用信息的结构指针,这个指针指向zend_executor_globals结构体,它提供全局的函数hash表和变量hash表,可以供我们随便操作全局的变量和函数,在汇编里只要用extern executor_globals 指令后,即可取得这个结构体里的哈希表成员。C语言中直接用EG()宏操作,这儿肯定是要比汇编语言方便多了。
最后是编译Linux上编译模块:
nasm –f elf hover.asm
链接模块:
ld -shared -o hover.so hover.o –lc
Windows编译链接方式差不多,只是windows必须得有一个DEF文件,而且要链接上php5ts.lib这个静态链接库。
安装模块的话,装过PHP的人都知道,把库拷到php.ini所配置的extension_dir下,再增加相应的extension行就行了。