For a simple example let's take a look at PHP's Zend Engine. This is a relatively simple VM working purely in memory, thus not having to are about serialisation etc.
In PHP the bytecode consists of 167 different opcodes which can be found in one header: http://lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_opcodes.h
All those opcodes are stored in a data structure along with some meta information in form of an array (represented as pointer to the first element)
struct _zend_op_array {
/* ... */
zend_op *opcodes;
/* ... */
}
Each operation then has its own data structure stored in there:
struct _zend_op {
opcode_handler_t handler;
znode_op op1;
znode_op op2;
znode_op result;
ulong extended_value;
uint lineno;
zend_uchar opcode;
zend_uchar op1_type;
zend_uchar op2_type;
zend_uchar result_type;
};
In there we can see multiple things:
- The opcode number from the long list of opcodes is stored in the
opcode
field.
- Each operation takes two operands (
op1
and op2
) and has a return value (result
)
- Operands and return value have types, these indicate whether they represent variables, constants or temporary values. In PHP's implementation these are stored in different places and are accessed differently.
- There is an
extended_value
which is used i.e. in foreach loops to store some more info
- The line number from the source file for reporting errors (filename is stored in the zend_op_array strucuture above)
- The
handler
is a small optimization, directly pointing to the function implementing this.
While compiling the script the compiler will create these data structures and ensure that the operands and return value show to the same place, so with a call like foo(bar())
the return value of the call to bar
will be the same temporary as the operand of the opcode pushing an argument on the functions argument stack. Wait what - argument stack!? you might ask, right: As each opcode only accepts two operands we can't directly pass the function parameters, but first ZEND_SEND_*
operations fill a stack, then a ZEND_DO_FCALL
/ZEND_DO_FCALL_BY_NAME
operation will be executed using that stack.
Using the tool vld we can dump this compiled form:
php -dextension=modules/vld.so -dvld.active -r 'foo(bar());'
Finding entry points
Branch analysis from position: 0
Return found
filename: Command line code
function name: (null)
number of ops: 6
compiled vars: none
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > INIT_FCALL_BY_NAME 'foo', 'foo'
1 INIT_FCALL_BY_NAME 'bar', 'bar'
2 DO_FCALL_BY_NAME 0
3 SEND_VAR_NO_REF 4 $0
4 DO_FCALL_BY_NAME 1
5 > RETURN null
branch: # 0; line: 1- 1; sop: 0; eop: 5
path #1: 0,
Next step is execution. This essentially is this loop (shortened):
while (1) {
if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
switch (ret) {
case 1:
return;
/* ... */
default:
break;
}
}
}
OPLINE
represents the current element of our array of zend_op structures. Each op has a handler which is executed. (for seeing the correct loop: it is in zend_vm_execute.h generated by zend_vm_gen.php out of zend_vm_execute.skl and zend_vm_def.h) If the handler returns one the script/function/... we are currently executing returns, such the loop ends.
A simple opcode handler can be defined like this (see zend_vm_def.h):
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
USE_OPLINE
zend_free_op free_op1, free_op2;
SAVE_OPLINE();
fast_add_function(&EX_T(opline->result.var).tmp_var,
GET_OP1_ZVAL_PTR(BP_VAR_R),
GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC);
FREE_OP1();
FREE_OP2();
CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}
This code looks similar to C but is filled into the gen script mentioned ahead. We see that this is the opcode number 1, going by the name of ZEND_ADD
which represents an addition. It requires both operands, which both can be of any kind (constants, temporaries, arbitrary variables, compiler cached variables). At the end of the operation the pointer is set to the next opcode in the array.