- URL: https://www.laruence.com/en/2008/08/11/147.html
- Please include attribution when republishing.
For a long time now, looking at the state of PHP here in China, there have been very few books devoted specifically to PHP's internals. Heh, so I'll be jotting down what I learn as I go, and if the chance comes along, I'll collect it all into a book. 🙂
This article today, in my mind, is meant to serve as an introduction:
PHP is a widely used scripting language. Because of its success, most of the time when we build something with PHP we don't need to think about how it is actually implemented underneath at all. I believe most PHP programmers never give that a thought. It has been three years since I first touched PHP, and for the first two of those years I was simply "using" PHP. Every time I finished a script, I would think, "Eh, no need to worry - the PHP interpreter knows what I want to do." That went on until last year, when I joined Yahoo and took on a job building a PHP extension. From that point on, I grew curious about a whole pile of new and unfamiliar things: zend, TSRM, zval, hashtable, op_array...
So I went digging for material everywhere, and every time I got hold of a good article, or even just a good passage, I would treasure it like a rare find - printing it out, filing it away, poring over it. What I discovered is that material on PHP internals is pitifully scarce in China. I don't know whether it is because there are plenty of people who understand it but are unwilling to share, or because there are simply very few who understand it at all. Either way, this road has been a hard one for me to walk. And that is how this article came to be.
In this article I will start from the whole execution lifecycle of PHP and give a rough overview of the various stages - lexical analysis, syntax analysis, op code, and so on. In later articles I will go into each stage in detail. (Of course, if you are too impatient to wait for the details - heh - you can just contact me directly.)
From the PHP script we first write -> to that script finally being executed -> to getting the execution result, this process can actually be divided into the following stages (sneer: CSDN won't let you upload images):
First, the Zend Engine (ZE) invokes the lexer (generated by Lex; the source file is Zend/zend_language_sanner.l) to strip whitespace and comments from the PHP source file we are about to execute, splitting it into tokens one by one.
Then ZE forwards the resulting tokens to the parser (generated by yacc; the source file is Zend/zend_language_parser.y), which produces op codes one by one. Opcodes are usually kept in the form of an op array, which is the intermediate language that PHP executes.
Finally, ZE calls zend_executor to execute the op array and print the result.

ZE is a virtual machine, and it is precisely because it exists that we can write PHP scripts without ever having to care what type of operating system we are on. ZE is a CISC (Complex Instruction Set Computer) processor, supporting 150 instructions (the specific instructions live in Zend/zend_vm_opcodes.h), ranging from the simplest ZEND_ECHO (echo) to the complex ZEND_INCLUDE_OR_EVAL (include, require). Everything we write in PHP is ultimately turned into a sequence of these 150 instructions (op codes), and then executed.
So is there any way to see what our PHP script finally gets "translated" into? In other words, what do op codes actually look like? Heh, to do that by hand we would need to recompile PHP and modify its compile_file and zend_execute functions. Luckily, there is already a PECL extension that lets us do this directly: VLD (Vulcan Logic Dissassembler), developed by Derick Rethans. Just download it, load it into PHP, and with a simple setting you can get the translation result of your script. As for how to use the extension - Yahoo it, and you will know ^_^.
Next, let us try using VLD to look at the intermediate language of a simple PHP script.
The original code:
$i = "This is a string";
//I am comments
echo $i.‘ that has been echoed to screen‘;
?>
The op codes produced by VLD:
function name: (null)
number of ops: 7
line # op fetch ext operands
-------------------------------------------------------------------------------------------------------------------------------
2 0 FETCH_W local $0, 'i'
1 ASSIGN $0, 'This+is+a+string'
4 2 FETCH_R local $2, 'i'
3 CONCAT ~3, $2,'+that+has+been+echoed+to+screen'
4 ECHO ~3
6 5 RETURN 1
6 ZEND_HANDLE_EXCEPTION
As you can see, the comments in the source file are already gone from the op codes, so there is no need to worry that having too many comments will affect your script's execution time (in practice, it only affects how long ZE's lexical processing stage takes).
Now let us analyze this op codes one by one. Every op code, also called an op_line, consists of the following 7 parts. In zend_compile.h we can see the definition below:
opcode_handler_t handler;
znode result;
znode op1;
znode op2;
ulong extended_value;
uint lineno;
zend_uchar opcode;
};
Among these, the opcode field indicates the type of operation, handler indicates the handler, and then there are two operands and one result.
- FETCH_W fetches a variable in write mode; here it fetches the variable named "i" into $0 (*zval).
- Assign (ASSIGN) the string "this+is+a+string" to $0
- String concatenation
- Output
As you can see, this is very similar to the three-address code many of you studied in your compiler course at university. The difference is that these intermediate codes are executed directly by the Zend VM (Zend virtual machine).
The function truly responsible for execution is zend_execute. Look at zend_execute.h:
ZEND_API extern void (*zend_execute)(zend_op_array *op_array TSRMLS_DC);
As you can see, zend_execute accepts a zend_op_array* as its argument.
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_uint *refcount;
zend_op *opcodes;
zend_uint last, size;
zend_compiled_variable *vars;
int last_var, size_var;
zend_uint T;
zend_brk_cont_element *brk_cont_array;
zend_uint last_brk_cont;
zend_uint current_brk_cont;
zend_try_catch_element *try_catch_array;
int last_try_catch;
/* static variables support */
HashTable *static_variables;
zend_op *start_op;
int backpatch_count;
zend_bool done_pass_two;
zend_bool uses_this;
char *filename;
zend_uint line_start;
zend_uint line_end;
char *doc_comment;
zend_uint doc_comment_len;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};
As you can see, the structure of zend_op_array looks a lot like that of zend_function (see my other articles). For code in the global scope - that is, an op_array not contained in any function - its function_name is NULL. The opcodes field in the structure holds the array of op codes belonging to this op_array. Starting from start_op, zend_execute interprets and executes each op code passed in, one by one, thereby producing the result our PHP script intends.
Next time I will introduce the soul of PHP variables - zval. You will see how PHP implements variable passing, type juggling, and so on.
Be First to Comment