您的位置:首页 > 编程语言 > PHP开发

108 内核探索 --php扩展、zend引擎、sapi

2015-10-31 15:14 1046 查看

php内核探索系列文章 :http://www.nowamagic.net/librarys/veda/detail/1285

1.php请求流程

先看一个php请求的运行流程:

浏览器用户--->web服务器(apache,nginx)--->Zend引擎从文件系统读取php代码文件--->Zend解释器工作

--->执行解释后的代码-->Zend引擎注册的函数接口-->内置模块或者各个需要的外部模块扩展-->数据库memcache等后端资源

2.zend引擎

Zend引擎是PHP实现的核心,提供了语言实现上的基础设施。

例如:PHP的语法实现,脚本的编译运行环境, 扩展机制以及内存管理等,当然这里的PHP指的是官方的PHP实现(除了官方的实现, 目前比较知名的有facebook的hiphop实现,不过到目前为止,PHP还没有一个标准的语言规范)。

而PHP则提供了请求处理和其他Web服务器的接口(SAPI)。

PHP3 采用的是边解释边执行的运行方式,运行效率很受其影响。其次,代码整体耦合度比较高,可扩展性也不够好,不利于应付各种各样需求。因此,此时在 PHP 界里已经有点中流砥柱作用的 Zeev Suraski 和 Andi Gutmans 决定重写代码以解决这两个问题。最终他们俩把该项技术的核心引擎命名为 Zend Engine,Zend 的意思即为 Zeev + Andi 。

3.php扩展

Zend引擎注册的函数接口 就是php工程师经常接触的各种php函数.

外部模块扩展 就是php编译的各个so文件(linux)或者dll文件(windwos).

执行解释后的代码 浏览器的内容就是从这里返回的.

内置模块 也就是php每次启动的时候会携带启动的模块.

从上面的流程图可以知道php可以从3个点进行扩展.1 外部模块扩展 2 Zend引擎 3 内置模块,下面我将一一讨论.

外部模块扩展.

如果你使用过dl()你就接触过这些外部的扩展模块.外部的扩展模块文件就放在你的硬盘里,他在php脚本运行时被加载到内存中,而且只有需要的时候才被加载.

当此次的脚本运行完之后他就会被内存释放掉,总的来说它运行的慢但是不占资源.不需要你重新编译一个php.

内置模块

虽然也是Zend引擎之外的模块,但是与外部模块扩展有些不同,他已经在php里边了.他会使得你编译的php体积变大,如果有改变,必须重新编译php才行.内置模块会使得

php内存变大,但是调用起来也会更加的快速.在我们的测试中一些模块运行在内置模式会有30%以上的速度提升.

Zend引擎

首先,我绝对不建议你去修改Zend引擎.一些php语言的特性只要在Zend引擎中才能够实现.比如你要修改数组关键字的名字,你可以在这里实现.

在php源代码里,以zend开头的都是zend引擎的相关代码.

一般php源代码目录结构类似下面:

main php的主要源代码,

ext php的扩展

sapi 与不同服务器的api交互层代码

zend zend引擎部分

TSRM 线程安全相关模块代码

/* 扩展的标准头 */
#include "php.h"
/* 声明这个so被导出的函数 */
ZEND_FUNCTION(helloworld_module);
/* Zend引擎注册的函数接口 */
zend_function_entry helloworldmod_interfaces[] =
{
ZEND_FE(helloworld_module, NULL)
{NULL, NULL, NULL}
};
/* 这是这个模块的声明实体,它的值对模块编译的时候起实际作用 */
zend_module_entry helloworldmod_module_entry =
{
STANDARD_MODULE_HEADER,
"Hello world",
helloworldmod_interfaces,
NULL,
NULL,
NULL,
NULL,
NULL,
NO_VERSION_YET,
STANDARD_MODULE_PROPERTIES
};

/* 向zend引擎声明一个备案,可以说明 helloworldmod_module_entry属于helloworldmod.so这个动态库*/
#if COMPILE_DL_helloworld_module
ZEND_GET_MODULE(helloworldmod)
#endif

/* 这就是我们新增的函数的真正代码 */
ZEND_FUNCTION(helloworld_module)
{
return "Hello,world";
}


我们可以根据其他扩展的config.m4文件来修改成我们的必要编译配置信息。这里这个模块几乎是一个空的config.m4文件就行,

然后利用phpize来生成configure文件然后是 ./configure && make && make install执行就能编译一份我们的动态库

test.php

<?php
echo helloworld_module();

?>

输出:

"Hello,world"

完成了PHP扩展,我们已经深入了php的c代码内部,但是在一些情况下,这还不够。我们需要深入到c语言调用c库的过程当中,在linux下面一个很给力的工具是LD_PRELOAD环境变量

LD_PRELOAD环境变量是编译器找到程序中所引用的函数或全局变量所存在的位置的一个过滤器,比如在php的c代码里调用一个开始网络连接的方法connect,事实上就是通过动态链接

去寻找linux的c库的函数connect,这些链接文件一般放在lib下面,这也就为我们影响php的代码执行提供了一个切入点。因为php程序在动态载入lib下面的函数connect之前会检查LD_PRELOAD

提供的动态库里有没有这个connect函数,我们可以在这里对php的行为进行干涉。

下面以一个简单的过滤网络访问的例子说明如何实现:

先是一个准备作为LD_PRELOAD环境变量的值的so文件的代码。

lp_demo.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <dlfcn.h>

//定义我们自己的connect函数
int  connect(int  sockfd,  const  struct sockaddr *serv_addr, socklen_t
addrlen){
static int (*connect_linuxc)(int, const  struct sockaddr*, socklen_t)=NULL;
unsigned char *ip_char;

//利用 lsym的RTLD_NEXT选项绕过LD_PRELOAD环境变量的connect方法找到c库的函数
if (!connect_linuxc) connect_linuxc=dlsym(RTLD_NEXT,"connect");

ip_char=serv_addr->sa_data;
ip_char+=2;

//192.168.2.3 找到了
if ((*ip_char==192)&&(*(ip_char+1)==168)&&(*(ip_char+2)==2)&&(*(ip_char+3)==3)) {

//简单返回一个权限错误的代码
return EACCES;
}

// 调用真正的connect方法
return connect_linuxc(sockfd,serv_addr,addrlen);

}


编译成so文件

$ gcc -o lp_demo.so -shared lp_demo.c -ldl

测试文件 test.php

<?php

file_get_contents("http://192.168.2.3/");

?>

使用方法

LD_PRELOAD=lp_demo.so php test.php

这样他将不可能访问的到192.168.2.3这种我们内部的网址。起到一个很好的沙盒作用。

除此之外我们还可以利用fwrite fopen等函数将php对文件系统的读写操作转移到mencache,nosql之类的后端资源当中。

最后,即使我们已经深入了c库的内部,也不意味着我们走到了最底层,在c库下面,还有一堆sys_开头的函数,他们才是内核空间里的真正函数,在此就不在探讨了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: