3.扩展实现
比如模块提供一个叫做get_var_name()的函数来获取变量名字. 如果大家有写过PHP扩展的经验的话,应该看过类似如下的函数实现(取自php json扩展$PHP_SRC/ext/json/json.c):
/* {{{ proto string json_encode(mixed data [, int options])
Returns the JSON representation of a value */
static PHP_FUNCTION(json_encode)
{
zval *parameter;
smart_str buf = {0};
long options = 0;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", ¶meter, &options) == FAILURE) { // 这里将传入的参数取出来. 参考文档 http://www.php.net/manual/en/internals2.funcs.php
return;
}
php_json_encode(&buf, parameter, options TSRMLS_CC);
ZVAL_STRINGL(return_value, buf.c, buf.len, 1);
smart_str_free(&buf);
}
/* }}} */
这中实现在函数体内可以通过zend_parse_parameter的方式来获取传递进来的变量, 但这样只能获取到变量的值. 却无法得到其他更多的信息.,我们往低层看看在PHP中函数是怎么调用,参数是怎么传递的.
3.1 PHP中函数的调用
在研究函数怎么调用之前, 我们需要看看PHP代码是怎么执行的.
大致可以分为2个步骤:
- 词法分析,语法分析然后编译成opcode
- 执行opcode
PHP函数的执行也只能在opcode执行阶段执行.
这里之前要介绍一个查看OPCODE的绝佳工具 vld(http://pecl.php.net/package/vld)
装好这扩展。可以在命令行下查看php脚本编译后的opcode
我们看看下面这个php脚本被编译后opcode是什么样的.
$str = "http://reeze.cn";
$len = strlen($str);
$len2 = strlen($str2=10);
echo $len;
echo $len2;
?>
$ php -dvld.active=1 func_call.php

也可以再增加一个参数 -dvld.verbosity=3, 这样将会显示更多的信息.

它被编译为上面的10条opcode命令.
op的名称一看也能看出什么意思. .. 其中以 “!”开头的数字表示编译后的变量,, 以”~”开头的变量表示零时变量.
上面可可以看出如果函数调用存在参数的话,在DO_FCALL之前会执行SEND_VAR 或者 SEND_VAR_NO_REF指令。并且这些指令后面操作的是编译过变量或者一个临时变量.
在PHP中调用时我们是可以访问到DO_FCALL这个操作的opcode信息 。可以通过 EG(active_opline_ptr) 获取到当前指令
PHP中存在一系列*G宏, EG 则为在执行opcode时的全局变量。
见文件: $PHP_SRC/Zend/zend_globals.h
struct _zend_executor_globals {
// ...
zend_op **opline_ptr; // 指向当前正在执行的zend_op对象
HashTable *active_symbol_table;
HashTable symbol_table; /* main symbol table */
HashTable included_files; /* files already included */
jmp_buf *bailout;
int error_reporting;
int orig_error_reporting;
int exit_status;
zend_op_array *active_op_array;
// ...
};
struct _zend_op_array {
/* Common elements */
zend_uchar type;
char *function_name;
zend_class_entry *scope;
zend_uint fn_flags;
union _zend_function *prototype;
zend_uint num_args;
zend_uint required_num_args;
zend_arg_info *arg_info;
zend_bool pass_rest_by_reference;
unsigned char return_reference;
/* END of common elements */
zend_bool done_pass_two;
zend_uint *refcount;
zend_op *opcodes; // zend_op数组.
zend_uint last, size;
zend_compiled_variable *vars; // 所有编译后的变量信息Since PHP5.1 这是一个数组
int last_var, size_var; // last_var 最后一个编译变量的索引
// ...
};
当前执行的op_array中保存所有编译变量的信息, 再看看zend_compiled_variable的结构吧。
typedef struct _zend_compiled_variable {
char *name;
int name_len;
ulong hash_value;
} zend_compiled_variable;
这正是我想获取的变量名称.
我们可以通过全局变量EG(opline_ptr)指针获取到当前执行的zend_op, zend_op的结构如下:
struct _zend_op {
opcode_handler_t handler; // 处理该OPCODE的处理函数
znode result; // 该opcode执行的结果
znode op1; // 有的opcode需要1个,有的需要两个操作数。
znode op2;
ulong extended_value;
uint lineno;
zend_uchar opcode; // 该opcode的值 见$PHP_SRC/Zend/zend_vm_opcodes.h
};
这也就是我们函数调用时执行的opcode.我们现在可以获取到DO_FCALL时的opcode, 通过VLD察看opcode工具很容易就知道函数调用之前,如果函数有参数的话,在DO_FCALL之前一定有SEND_VAR或者SEND_VAR_NO_REF指令, 指针后退一个则一定是指向SEND_VAR或SEND_VAR_NO_REF指令的。 这样的话我们根据DO_FCALL获取到的zend_op指令后退不久可以获取SEND_VAR指令了么. SEND_VAR指令会操作compiled_var,这样我们就能得到变量的信息了..
看看znode都有哪些信息:
typedef struct _znode {
int op_type;
union {
zval constant;
zend_uint var; // 这个var就是当前变量在zend_op_array.vars 中的compiled_variable数组中的索引.不过这个索要并不是字面上的. 详情请看最后的代码实现.
zend_uint opline_num; /* Needs to be signed */
zend_op_array *op_array;
zend_op *jmp_addr;
struct {
zend_uint var; /* dummy */
zend_uint type;
} EA;
} u;
} znode;
如在在上面的注释. 通过获取znode.u.var的值就可以获取到变量的信息了.
这样的话.程序的实现也就简单了.
下面是实现:
/* {{{ get_var_name
*
* 这个扩展要求PHP >= 5.1
* 因为依赖PHP 5.1引入的compiled variable
*
* 在PHP空间导出一个get_var_name函数.
* echo get_var_name($var_name); // expect: var_name
* echo get_var_name($lineno=100); // expect: lineno
*/
PHP_FUNCTION(get_var_name)
{
int len;
char *strg = "";
if(ZEND_NUM_ARGS() < 1) {
return;
}
/* 显示所有的编译变量
int i;
zend_compiled_variable *vars = EG(active_op_array)->vars;
for(i=0; i < EG(active_op_array)->last_var; ++i) { // last_var 最后一个编译变量的索引
spprintf(&strg, 0, "%snVar:%sn", strg, EG(active_op_array)->vars[i].name);
++vars;
}
*/
zend_op *pre_opline_ptr = *EG(opline_ptr);
pre_opline_ptr--;
// 支持这类的调用: get_var_name($a="VALUE"); // expect: a
// 这里增加在赋值的情况下也能正确返回变量的名字的处理方法, 如果方法参数是赋值的的话, 编译的OPCODE 中SEND_VAR之前将会
// 有一个ZEND_ASSIGN 操作, 并且ZEND_ASSIGN操作的返回值被使用.比如: $c = $d + 1; $d + 1的返回值就被使用了. 就可以确认
// 是前面的调用方式
zend_op *pre_pre_online_ptr = pre_opline_ptr - 1;
if(pre_pre_online_ptr && pre_pre_online_ptr->opcode == ZEND_ASSIGN && !(pre_pre_online_ptr->result.u.EA.type & EXT_TYPE_UNUSED)) {
// 通过赋值之前的zend_op来获取变量信息
pre_opline_ptr = pre_pre_online_ptr;
}
int index;
// 比如get_var_name($name); 这时SEND_VAR OPCODE的op1操作数类型就是IS_CV 也就是IS Compiled Variable
// 只有compiled variable才是直接存储索引的. PHP >= 5.1
if(pre_opline_ptr->op1.op_type == IS_CV) {
index = pre_opline_ptr->op1.u.var;
}
else {
// 请参考VLD的源代码 $VLD_SRC/srm_oparray.c LINE:320 vld_dump_znode函数
index = pre_opline_ptr->op1.u.var / sizeof(temp_variable);
}
zend_compiled_variable var = EG(active_op_array)->vars[index];
len = spprintf(&strg, 0, "%s", strg, var.name);
RETURN_STRINGL(strg, len, 0);
}
/* }}} */
点击这里下载代码