标签 - PHP内核

PHP内核    2019-04-25 16:36:26    23    0    0

在5.2及更早版本的PHP中,没有专门的垃圾回收器GC(Garbage Collection),引擎在判断一个变量空间是否能够被释放的时候是依据这个变量的zval的refcount的值,如果refcount为0,那么变量的空间可以被释放,否则就不释放,这是一种非常简单的GC实现。然而在这种简单的GC实现方案中,出现了意想不到的变量内存泄漏情况(Bug:http://bugs.php.net/

PHP内核    2019-04-25 16:35:39    14    0    0

变量的内部引用和计数

在引擎内部,一个PHP的变量是保存在“zval”结构中,此结构包含了变量的类型和值信息,这个在之前的文章 变量的内部存储:值和类型 中已经介绍了,此结构还有另外两个字段信息,一个是"is_ref"(此字段在5.3.2版本中是is_ref__gc),此字段是一个布尔值,用来标识变量是否是一个引用,通过这个字段,PHP引擎能够区分一般的变量和引用变量。PHP代码中可以通过 & 操作符号来建立一个引用变量,建立的引用变量内部的zval的is_ref字段就为1。zval中还有另外一个字段refcount(此字段在5.3.2版本中是refcount__gc),这个字段是一个计数器,表示有多少个变量名指向这个zval容器,当此字段为0时,表示没有任何变量指向这个zval,那么zval就可以被释放,这是引擎内部对内存的一种优化。考虑如下代码:

1<!--?php 
2$a "Hello NowaMagic"
3$b $a
4?--> 

代码中有两个变变量$a和$b,通过普通赋值方式将$a赋给$b,这样$b的值和$a相等,对$b的修改不会对$a造成任何影响,那么在这段代码中,如果$a和$b对应两个不同的zval,那么显然是对内存的一种浪费,PHP的开发者也不会让这样的事情发生。所以实际上$a和$b是指向同一个zval。这个zval的类型是STRING,值是"Hello world",有$a和$b两个变量指向它,所以它的refcount=2, 由于是一个普通赋值,所以is_ref字段为0。 这样就节省了内存开销。

当执行$a = "Hello world"之后,$a对应的zval的信息为:a: (refcount=1, is_ref=0)="Hello world"

但执行$b=$a之后,$a对应的zval的信息为:a: (refcount=2, is_ref=0)="Hello world"

下面将之前的代码修改一下:

1<?php 
2$a "Hello world"
3$b = &$a
4?> 

这样就通过引用赋值方式将$a赋给$b。

当执行$a = "Hello world"之后,$a对应的zval的信息为:a: (refcount=1, is_ref=0)="Hello world"

但执行$b=&$a之后,$a对应的zval的信息为:a: (refcou

PHP内核    2019-04-25 16:35:13    21    0    0

解释器引擎最终执行op的函数是zend_execute,实际上zend_execute是一个函数指针,在引擎初始化的时候zend_execute默认指向了execute,这个execute定义在{PHPSRC}/Zend/zend_vm_execute.h:

01ZEND_API void execute(zend_op_array *op_array TSRMLS_DC) 
02
03    zend_execute_data *execute_data; 
04    zend_bool nested = 0; 
05    zend_bool original_in_execution = EG(in_execution); 
06   
07   
08    if (EG(exception)) { 
09        return
10    
11   
12    EG(in_execution) = 1; 
13   
14zend_vm_enter: 
15    /* Initialize execute_data */ 
16    execute_data = (zend_execute_data *)zend_vm_stack_alloc( 
17        ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + 
18        ZEND_MM_ALIGNED_SIZE(sizeof(zval**) * op_array->last_var * (EG(active_symbol_table) ? 1 : 2)) + 
19        ZEND_MM_ALIGNED_SIZE(sizeof(temp_variable)) * op_array->T TSRMLS_CC); 
20   
21    EX(CVs) = (zval***)((char*)execute_data + ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data))); 
22    memset(EX(CVs), 0, sizeof(zval**) * op_array->last_var); 
23    EX(Ts) = (temp_variable *)(((char*)EX(CVs)) + ZE
PHP内核    2019-04-25 16:34:41    13    0    0

PHP语言作为脚本语言的一种,由于不需要进行编译,所以通常PHP程序的分发都是直接发布源代码。 对于一些开源软件来说,这并没有什么问题,因为它本来就希望有更多的人阅读代码,希望有更多的人参与进来, 而对于商业代码来说,这却是一个不太好的消息,不管是从商业秘密,还是从对公司产权的保护来说却是一个问题, 基于此,从而引出了对PHP代码的加密和解密的议题。 例如国内的Discuz论坛程序在开源之前要运行是必须安装Zend Optimizer的, Zend官方的代码加密软件是Zend Guard, 可以用来加密和混淆PHP代码,这样分发出去的代码就可以避免直接分发源代码, 不过加密后的代码是无法直接运行的,在运行时还需要一个解密的模块来运行加密后的程序, 要运行Zend Guard加密后的代码需要安装Zend Optimizer(PHP5.2之前的版本), 或者安装Zend Guard Loader(PHP5.3版本)扩展才能运行。

加密的本质

本质上程序在运行时都是在执行机器码,而基于虚拟机的语言的加密通常也是加密到这个级别, 也就是说PHP加密后的程序在执行之前都会解密成opcode来执行。

PHP在执行之前有一个编译的环节,编译的结果是opcode,然后由Zend虚拟机执行, 从这里看如果只要将源代码加密,然后在执行之前将代码解密即可。

从这里看,只要代码能被解密为opcode,那么总有可能反编译出来源代码, 其他的语言中也是类似,比如objdump程序能将二进制程序反汇编出来, .NET、Java的程序也是一样,都有一些反编译的程序,不过通常这些厂商同时还会 附带代码混淆的工具,经过混淆的代码可读性极差,很多人都留意过Gmail等网站 经过混淆的JS代码吧,他们阅读起来非常困难,经过混淆的代码即使反编译出来, 读者也很难通过代码分析出代码中的逻辑,这样也就极大的增加了应用的安全性。

简单的代码加密解密实战

根据前文的介绍,作为实例,本文将编写一个简单的代码加密扩展用于对PHP代码的加密, 我们只需要能把源码加密,简单通过浏览源代码的方法无法获取到源代码那我们的目标就达到了, 为了能正确执行加密后的代码,我们还需要另一个模块:解密模块。

简单的思路是把所有的PHP文件代码进行加密,同时另存为同名的PHP文件, 这是一种很简单的做法,只是为了防止源代码赤裸裸的暴露在代码中。

加密也有很

PHP内核    2019-04-25 16:34:11    38    0    0

假如我们现在使用的是CLI模式,直接在SAPI/cli/php_cli.c文件中找到main函数, 默认情况下PHP的CLI模式的行为模式为PHP_MODE_STANDARD。 此行为模式中PHP内核会调用php_execute_script(&file_handle TSRMLS_CC);来执行PHP文件。 顺着这条执行的线路,可以看到一个PHP文件在经过词法分析,语法分析,编译后生成中间代

PHP内核    2019-04-25 16:33:30    19    0    0

Bison是一种通用目的的分析器生成器。它将LALR(1)上下文无关文法的描述转化成分析该文法的C程序。 使用它可以生成解释器,编译器,协议实现等多种程序。 Bison向上兼容Yacc,所有书写正确的Yacc语法都应该可以不加修改地在Bison下工作。 它不但与Yacc兼容还具有许多Yacc不具备的特性。

Bison分析器文件是定义了名为yyparse并且实现了某个语法的函数的C代码。 这个函数并不是一个可以完成所有的语法分析任务的C程序。 除此这外我们还必须提供额外的一些函数: 如词法分析器、分析器报告错误时调用的错误报告函数等等。 我们知道一个完整的C程序必须以名为main的函数开头,如果我们要生成一个可执行文件,并且要运行语法解析器, 那么我们就需要有main函数,并且在某个地方直接或间接调用yyparse,否则语法分析器永远都不会运行。

先看下bison的示例:逆波兰记号计算器

01%{
02#define YYSTYPE double
03#include <stdio.h>
04#include <math.h>
05#include <ctype.h>
06int yylex (void);
07void yyerror (char const *);
08%}
09  
10%token NUM
11  
12%%
13input:    /* empty */
14     | input line
15    ;
16  
17line:     '\n'
18    exp '\n'      printf ("\t%.10g\n", $1); }
19;
20  
21exp:      NUM           { $$ = $1;           }
22   exp exp '+'   { $$ = $1 + $2;      }
23    exp exp '-'   { $$ = $1 - $2;      }
24    exp exp '*'   { $$ = $1 * $2;      }
25    exp exp '/'   { $$ = $1 / $2;      }
26     /* Exponentiation */
27    exp exp '^'   { $$ = pow($1, $2); }
28    /* Unary minus
PHP内核    2019-04-25 16:32:36    11    0    0

通过前面的学习,我们了解到一个PHP文件在服务器端的执行过程包括以下两个大的过程:

  1. 递给php程序需要执行的文件, php程序完成基本的准备工作后启动PHP及Zend引擎, 加载注册的扩展模块。
  2. 初始化完成后读取脚本文件,Zend引擎对脚本文件进行词法分析,语法分析。然后编译成opcode执行。 如过安装了apc之类的opcode缓存, 编译环节可能会被跳过而直接从缓存中读取opcode
PHP内核    2019-04-25 16:31:29    8    0    0

讲述之前,先描述下{资源}类型在内核中的结构:

1//每一个资源都是通过它来实现的。
2typedef struct _zend_rsrc_list_entry
3{
4    void *ptr;
5    int type;
6    int refcount;
7}zend_rsrc_list_entry;

在真实世界中,我们经常需要操作一些不好用标量值表现的数据,比如某个文件的句柄,而对于C来说,它也仅仅是个指针而已。

1#include <stdio.h>
2int main(void)
3{
4    FILE *fd;
5    fd = fopen("/home/jdoe/.plan""r");
6    fclose(fd);
7    return 0;
8}

C语言中stdio的文件描述符(file descriptor)是与每个打开的文件相匹配的一个变量,它实际上十一个FILE类型的指针,它将在程序与硬件交互通讯时使用。我们可以使用fopen函数来打开一个文件获取句柄,之后只需把这个句柄传递给feof()、fread()、fwrite()、fclose()之类的函数,便可以对这个文件进行后续操作了。既然这个数据在C语言中就无法直接用标量数据来表示,那我们如何对其进行封装才能保证用户在PHP语言中也能使用到它呢?这便是PHP中资源类型变量的作用了!所以它也是通过一个zval结构来进行封装的。

资源类型的实现并不复杂,它的值其实仅仅是一个整数,内核将根据这个整数值去一个类似资源池的地方寻找最终需要的数据。

资源类型变量的使用

资源类型的变量在实现中也是有类型区分的!为了区分不同类型的资源,比如一个是文件句柄,一个是mysql链接,我们需要为其赋予不同的分类名称。首先,我们需要先把这个分类添加到程序中去。这一步的操作可以在MINIT中来做:

01#define PHP_SAMPLE_DESCRIPTOR_RES_NAME "山寨文件描述符"
02static int le_sample_descriptor;
03ZEND_MINIT_FUNCTION(sample)
04{
05    le_sample_descriptor = zend_register_list_destructors_ex(NULL, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME,modu
PHP内核    2019-04-25 16:31:09    18    0    0

在定义一个类时往往会使其继承某个父类或者实现某个接口,在扩展中实现这个功能非常方便。下面我先给出PHP语言中的代码。

01<?php
02interface i_myinterface
03{
04    public function hello();
05}
06 
07class parent_class implements i_myinterface
08{
09    public function hello()
10    {
11        echo "Good Morning!\n";
12    }
13}
14 
15final class myclass extends parent_class
16{
17    public function call_hello()
18    {
19        $this->hello();
20    }
21}
22?>

上面的代码我们已经非常熟悉了,它们在PHP扩展中的实现应该是这样的:

01//三个zend_class_entry
02zend_class_entry *i_myinterface_ce,*parent_class_ce,*myclass_ce;
03 
04//parent_class的hello方法
05ZEND_METHOD(parent_class,hello)
06{
07    php_printf("hello world!\n");
08}
09 
10//myclass的call_hello方法
11ZEND_METHOD(myclass,call_hello)
12{
13    //这里涉及到如何调用对象的方法,详细内容下一章叙述
14    zval *this_zval;
15    this_zval = getThis();
16    zend_call_method_with_0_params(&this_zval,myclass_ce,NULL,"hello",NULL);
17}
18 
19//各自的zend_function_entry
20static zend_function_entry i_myinterface_method[]={
21    ZEND_ABSTRACT_ME(i_myinterface, hello,  NULL)
22    {NULL,NULL,NULL}
23};
24 
25sta
PHP内核    2019-04-25 16:30:38    14    0    0

定义一个接口还是很方便的,我先给出一个PHP语言中的形式。

1<?php
2interface i_myinterface
3{
4    public function hello();
5}
6?>

那它在扩展中的实现是这样的。

01zend_class_entry *i_myinterface_ce;
02 
03static zend_function_entry i_myinterface_method[]={
04    ZEND_ABSTRACT_ME(i_myinterface, hello, NULL) //注意这里的null指的是arginfo
05    {NULL,NULL,NULL}
06};
07 
08ZEND_MINIT_FUNCTION(test)
09{  
10    zend_class_entry ce;
11    INIT_CLASS_ENTRY(ce, "i_myinterface", i_myinterface_method);
12 
13    i_myinterface_ce = zend_register_internal_interface(&ce TSRMLS_CC);
14    return SUCCESS;
15}

我们使用ZEND_ABSTRACT_ME()宏函数来为这个接口添加函数,它的作用是声明一个类似虚函数的东西,不用实现。也就是说我们不用为其添加ZEND_METHOD(i_myinterface,hello){...}的实现。但是这个宏函数只能为我们实现public类型的函数声明,如果有其它特殊需要,需要使用ZEND_FENTRY()宏函数来实现,因为ZEND_ABSTRACT_ME也不过是后者的一种封装。

下面我们在PHP语言中使用这个接口。

01<?php
02class sample implements i_myinterface
03{
04    public $name "hello world!";
05     
06    public function hello()
07    {
08        echo $this->name."\n";
09    }
10}
11 
12$obj new sample();
13$obj->hello();
14?>
1/9