Main content of this article:
Introduction to symbol tables and zval reference principles Back to the original questionI wrote an article about citation a long time ago, but it was written in a cursory way and many principles were not explained clearly. Recently I was reading Derick Rethans (home: http://derickrethans.nl/ Github: https://github.com/derickr) When Daniel made a report before, he found an article explaining the PHP reference mechanism, which is this PDF. The article explains reference counting and reference passing from the perspective of zval and symbol tables. , reference return, global parameters, etc., the principles are eloquent, illustrated and written, and it is very exciting. It is recommended that children who have time read the original version. I believe there will be a lot of gains.
Without further ado, let’s talk about today’s topic.
We know that many languages provide a reference mechanism, which allows us to access the same content using different names (or symbols). The definition of a reference in the PHP manual is: "A reference in PHP means accessing the contents of the same variable with a different name. This is not like a pointer in C. Instead, a reference is a symbol table alias." In other words, a reference Some form of "binding" is implemented. For example, this type of interview question we often encounter is a typical example:
$a = array(1,2,3,4); foreach($a as &$v){ $v *= $v; } foreach($a as $v){ echo $v; }
Leaving aside the output of this question, today we will follow the footsteps of senior Derick Rethans and uncover the mystery of citation step by step.
Before starting to quote the principles, we need to make a brief explanation of the terms that appear repeatedly in the article. The most important and important ones are: 1. Symbol table 2. zval.
1. Symbol table
Computer language is a tool for communication between humans and machines, but unfortunately, the high-level languages we rely on and are proud of cannot be executed directly on computers because computers can only understand some form of machine language. This means that high-level languages must go through a compilation (or interpretation) process before they can be understood and executed by computers. During this process, many complex processes such as lexical analysis, syntax analysis, semantic analysis, intermediate code generation and optimization are required. During these processes, the compiler may repeatedly use information such as identifiers (such as variables) that appear in the source program. Type checking, semantic checking in the semantic analysis stage), this information is stored in different symbol tables. The symbol table stores the name and attribute information of identifiers in the source program. This information may include: type, storage type, scope, storage allocation information and other additional information. In order to efficiently insert and query symbol table entries, many compiler symbol tables are implemented using Hashtable. We can simply understand it as: the symbol table is a hashtable or map that saves the symbol name and various attributes of the symbol. For example, for program:
$str = 'this is a test'; function foo( $a, $b ){ $tmp = 12; return $tmp + $a + $b; } function to(){ }
A possible symbol table (not the actual symbol table) is a structure like this:
We don’t pay attention to the specific structure of the symbol table. We only need to know that each function, class, namespace, etc. has its own independent symbol table (separate from the global symbol table). Speaking of this, I suddenly remembered something. When I first started programming in PHP, when I was reading the manual of the extract() function, I saw the sentence "Import variables from the array into the current symbol table" I couldn't understand the meaning of the words, and I was extremely troubled by the suggestion made by my predecessors that "It is not recommended to use extract($_POST) and extract($_GET) to extract variables". In fact, the abuse of extract will not only cause serious security problems, but also pollute the current symbol table ( active symbol table).
So what is an active symbol table?
We know that during the execution of PHP code, it almost always starts from the global scope, scans sequentially, and executes sequentially. If a function call is encountered, the internal execution of the function will be entered. After the function is executed, it will return to the calling program to continue execution. This means that there must be some mechanism to distinguish the symbol tables used in different stages, otherwise it will cause confusion in compilation and execution. The active symbol table is the symbol table used to mark the current activity (at this time there should be at least a global global symbol table and an active active symbol table. Normally, active symbol table refers to global symbol table). The symbol table is not established at the beginning, but is continuously added and updated as the compiler scans it. When entering a function call, zend (PHP's language interpretation engine) creates a symbol table for the function and points the active symbol table to the symbol table. In other words, the symbol table used at any time should be the current active symbol table.
The above is the entire content of the symbol table. Let’s briefly extract the key contents:
符号表记录了程序中符号的name-attribute对,这些信息对于编译和执行是至关重要的。符号表类似一个map或者hashtable符号表不是一开始就建立好的,而是不断添加和更新的过程。活动符号表是一个指针,指向的是当前活动的符号表。更多的资料可以查看:
1. http://www.scs.stanford.edu/11wi-cs140/pintos/specs/sysv-abi-update.html/ch4.symtab.html
2. http://arantxa.ii.uam.es/~modonnel/Compilers/04_SymbolTablesI.pdf
2. Zval
在上一篇博客(PHP内核探索之变量(1)Zval)中,我们已经对zval的结构和基本原理有了一些了解。对zval不了解的童鞋可以先看看。为了方便阅读,我们再次贴出zval的结构:
struct _zval_struct { zvalue_value value; /* value */ zend_uint refcount__gc; /* variable ref count */ zend_uchar type; /* active type */ zend_uchar is_ref__gc; /* if it is a ref variable */ }; typedef struct _zval_struct zval;
1. 引用计数
正如上节所言,zval是PHP变量底层的真正容器,为了节省空间,并不是每个变量都有自己独立的zval容器,例如对于赋值(assign-by-value)操作:$a = $b(假设$b,$a都不是引用型变量),Zend并不会为$b变量开辟新的空间,而是将符号表中a符号和b符号指向同一个zval。只有在其中一个变量发生变化时,才会执行zval分离的操作。这被称为COW(Copy-on-write)的机制,可以在一定程度上节省内存和提高效率。
为了实现上述机制,需要对zval的引用状态做标记,zval的结构中,refcount__gc便是用于计数的,这个值记录了有多少个变量指向该zval, 在上述赋值操作中,$a=$b ,会增加原始的$b的zval的refcount值。关于这一点,上次(PHP内核探索之变量(1)Zval)已经做了详细的解释,这里不再赘述。
2. 函数传参
在脚本执行的过程中,全局的符号表几乎是一直存在的,但除了这个全局的global symbol table,实际上还会生成其他的symbol table:例如函数调用的过程中,Zend会创建该函数的内部symbol table,用于存放函数内部变量的信息,而在函数调用结束后,会删除该symbol table。我们接下来以一个简单的函数调用为例,介绍一下在传参的过程中,变量和zval的状态变化,我们使用的测试脚本是:
function do_zval_test($s){ $s = "change "; return $s; } $a = "before"; $b = do_zval_test($a);
我们来逐步分析:
(1). $a = "before";
这会为$a变量开辟一个新的zval(refcount=1,is_ref=0),如下所示:
(2). 函数调用do_zval_test($a)
由于函数的调用,Zend会为do_zval_test这个函数创建单独的符号表(其中包含该函数内部的符号s),同时,由于$s实际上是函数的形参,因此并不会为$s创建新的zval,而是指向$a的zval。这时,$a指向的zval的refcount应该为3(分别是$a,$s和函数调用堆栈):
a: (refcount=3, is_ref=0)='before func'
如下图所示:
(3).函数内部执行$s = "change "
由于$s的值发生了改变,因此会执行zval分离,为s专门copy生成一个新的zval:
(4).函数返回 return $s ; $b = do_zval_test($a).
$b与$s共享zval(暂时),准备销毁函数中的符号表:
(5). 销毁函数中的符号表,回到Global环境中:<喎?"http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPjwvcD4KPHA+IDxpbWcgc3JjPQ=="http://www.2cto.com/uploadfile/Collfiles/20141129/20141129083533169.jpg" alt="PHP Kernel Exploration Variables (2) - Understanding References_PHP Tutorial">
这里我们顺便说一句,在你使用debug_zval_dump()等函数查看zval的refcount时,会令zval本身的refcount值加1,所以实际的refcount的值应该是打印出的refcount减1,如下所示:
$src = "string"; debug_zval_dump($src);
结果是:
string(6) "string" refcount(2)
3. 引用初探
同上,我们还是直接上代码,然后一步步分析(这个例子比较简单,为了完整性,我们还是稍微分析一下):
$a = "simple test"; $b = &a; $c = &a; $b = 42; unset($c); unset($b);
则变量与zval的对应关系如下图所示:(由此可见,unset的作用仅仅是将变量从符号表中删除,并减少对应zval的refcount值)
上图中值得注意的最后一步,在unset($b)之后,zval的is_ref值又变成了0。
那如果是混合了引用(assign-by-reference)和普通赋值(assign-by-value)的脚本,又是什么情况呢?
我们的测试脚本:
(1). 先普通赋值后引用赋值
$a = "src"; $b = $a; $c = &$b;
具体的过程见下图:
(2). 先引用赋值后普通赋值
$a = "src"; $b = &$a; $c = $a;
具体过程见下图:
4. 传递引用
同样,向函数传递的参数也可以以引用的形式传递,这样可以在函数内部修改变量的值。作为实例,我们仍使用2(函数传参)中的脚本,只是参数改为引用的形式:
function do_zval_test(&$s){ $s = "after"; return $s; } $a = "before"; $b = do_zval_test($a);
这与上述函数传参过程基本一致,不同的是,引用的传递使得$a的值发生了变化。而且,在函数调用结束之后 $a的is_ref恢复成0:
可以看出,与普通的值传递相比,引用传递的不同在于:
(1) 第3步 $s = "change";时,并没有为$s新建一个zval,而是与$a指向同一个zval,这个zval的is_ref=1。
(2) 还是第3步。$s = "change";执行后,由于zval的is_ref=1,因此,间接的改变了$a的值
5. 引用返回
PHP支持的另一个特性是引用返回。我们知道,在C/C++中,函数返回值时,实际上会生成一个值的副本,而在引用返回时,并不会生成副本,这种引用返回的方式可以在一定程度上节省内存和提高效率。而在PHP中,情况并不完全是这样。那么,究竟什么是引用返回呢?PHP手册上是这么说的:"引用返回用在当想用函数找到引用应该被绑定在哪一个变量上面时",是不是一头雾水,完全不知所云?其实,英文手册上是这样描述的"Returning by reference is useful when you want to use a function to find to which variable a reference should be bound"。提取文中的主干和关键点,我们可以得到这样的信息:
(1). 引用返回是将引用绑定在一个变量上。
(2). 这个变量不是确定的,而是通过函数得到的(否者我们就可以使用普通的引用了)。
这其实也说明了引用返回的局限性:函数必须返回一个变量,而不能是一个表达式,否者就会出现类似下面的问题:
PHP Notice: Only variable references should be returned by reference in xxx(参看PHP手册中的Note).
那么,引用返回时如何工作的呢?例如,对于如下的例子:
function &find_node($key,&$tree){ $item = &$tree[$key]; return $item; } $tree = array(1=>'one',2=>'two',3=>'three'); $node =& find_node(3,$tree); $node ='new';
Zend都做了哪些工作呢?我们一步步来看。
(1). $tree = array(1=>'one',2=>'two',3=>'three')
同之前一样,这会在Global symbol table中添加tree这个symbol,并生成该变量的zval。同时,为数组$tree的每个元素都生成相应的zval:
tree: (refcount=1, is_ref=0)=array ( 1 => (refcount=1, is_ref=0)='one', 2 => (refcount=1, is_ref=0)='two', 3 => (refcount=1, is_ref=0)='three' )
如下图所示:
(2). find_node(3,&$tree)
由于函数调用,Zend会进入函数的内部,创建该函数的内部symbol table,同时,由于传递的参数是引用参数,因此zval的is_ref被标志为1,而refcount的值增加为3(分别是全局tree,内部tree和函数堆栈):
(3)$item = &$tree[$key];
由于item是$tree[$key]的引用(在本例的调用中,$key是3),因而更新$tree[$key]指向zval的is_ref和refcount值:
(4)return $item,并执行引用绑定:
(5)函数返回,销毁局部符号表。
tree对应的zval的is_ref恢复了0,refcount=1,$tree[3]被绑定在了$node变量上,对该变量的任何改变都会间接更改$tree[3]:
(6) 更改$node的值,会反射到$tree的节点上,$node ="new':
Note:为了使用引用返回,必须在函数定义和函数调用的地方都显式的使用&符号。
6. Global关键字
PHP中允许我们在函数内部使用Global关键字引用全局变量(不加global关键字时引用的是函数的局部变量),例如:
$var = "outside"; function inside() { $var = "inside"; echo $var; global $var; echo $var; } inside();
输出为insideoutside
我们只知道global关键字建立了一个局部变量和全局变量的绑定,那么具体机制是什么呢?
使用如下的脚本测试:
$var = "one"; function update_var($value){ global $var; unset($var); global $var; $var = $value; } update_var('four'); echo $var;
具体的分析过程为:
(1).$var = 'one';
同之前一样,这会在全局的symbol table中添加var符号,并创建相应的zval:
(2).update_var("four')
由于直接传递的是string而不是变量,因而会创建一个zval,该zval的is_ref=0,ref_count=2(分别是形参$value和函数的堆栈),如下所示:
(3)global $var
global $var这句话,实际上会执行两件事情:
(1).在函数内部的符号表中插入局部的var符号
(2).建立局部$var与全局变量$var之间的引用.
(4)unset($var);
这里要注意的是,unset只是删除函数内部符号表中var符号,而不是删除全局的。同时,更新原zval的refcount值和is_ref引用标志(引用解绑):
(5).global $var
同3,再次建立局部$var与全局的$var的引用:
(6)$var = $value;
更改$var对应的zval的值,由于引用的存在,全局的$var的值也随之改变:
(7)函数返回,销毁局部符号表(又回到最初的起点,但,一切已经大不一样了):
据此,我们可以总结出global关键字的过程和特性:
函数中声明global,会在函数内部生成一个局部的变量,并与全局的变量建立引用。函数中对global变量的任何更改操作都会间接更改全局变量的值。函数unset局部变量不会影响global,而只是解除与全局变量的绑定。现在,我们对引用已经有了一个基本的认识。让我们回到最初的问题:
$a = array(1,2,3); foreach($a as &$v){ $v *= $v; } foreach($a as $v){ echo $v; }
这之中,究竟发生了什么事情呢?
(1).$a = array(1,2,3);
这会在全局的symbol table中生成$a的zval并且为每个元素也生成相应的zval:
(2). foreach($a as &$v) {$v *= $v;}
这里由于是引用绑定,所以相当于对数组中的元素执行:
$v = &$a[0]; $v = &$a[1]; $v = &$a[2];
执行过程如下:
我们发现,在这次的foreach执行完毕之后,$v = &$a[2].
(3)第二次foreach循环
foreach($a as $v){ echo $v; }
这次因为是普通的assign-by-value的赋值形式,因此,类似与执行:
$v = $a[0]; $v = $a[1]; $v = $a[2];
别忘了$v现在是$a[2]的引用,因此,赋值的过程会间接更改$a[2]的值。
过程如下:
因此,输出结果应该为144.
附:本文中的zval的调试方法。
如果要查看某一过程中zval的变化,最好的办法是在该过程的前后均加上调试代码。例如
$a = 123; xdebug_debug_zval('a'); $b=&$a; xdebug_debug_zval('a');
配合画图,可以得到一个直观的zval更新过程。
参考文献:
http://en.wikipedia.org/wiki/Symbol_tablehttp://arantxa.ii.uam.es/~modonnel/Compilers/04_SymbolTablesI.pdfhttp://web.cs.wpi.edu/~kal/courses/cs4533/module5/myst.htmlhttp://www.cs.dartmouth.edu/~mckeeman/cs48/mxcom/doc/TypeInference.pdfhttp://www.cs.cornell.edu/courses/cs412/2008sp/lectures/lec12.pdfhttp://php.net/manual/zh/language.references.return.phphttp://stackoverflow.com/questions/10057671/how-foreach-actually-works由于写作匆忙,文中难免会有错误之处,欢迎指出探讨。