PHP模块by Nasm

用了这么多年的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,&param1);
	zend_get_parameters_ex(2,&param1,&param2);
	zend_get_parameters_ex(3,&param1,&param2,&param3);
	…

就是说要几个参数,第一个参数就是几,后面就是接受这些参数的指针,函数成功后它们各指向一个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行就行了。

Leave a comment

Your email address will not be published. Required fields are marked *