Per un semplice esempio diamo un'occhiata a Zend Engine di PHP. Questa è una VM relativamente semplice che funziona esclusivamente in memoria, quindi non deve riguardare la serializzazione ecc.
In PHP il bytecode è composto da 167 diversi codici opcode che possono essere trovati in un'intestazione: link
Tutti questi opcode sono memorizzati in una struttura dati insieme ad alcune meta informazioni in forma di un array (rappresentato come puntatore al primo elemento)
struct _zend_op_array {
/* ... */
zend_op *opcodes;
/* ... */
}
Ogni operazione ha quindi la sua struttura dati memorizzata in tale posizione:
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;
};
Qui possiamo vedere più cose:
- Il numero di opcode dall'elenco lungo di opcode è memorizzato nel campo
opcode
.
- Ogni operazione richiede due operandi (
op1
e op2
) e ha un valore di ritorno ( result
)
- Gli operandi e il valore restituito hanno tipi, che indicano se rappresentano variabili, costanti o valori temporanei. Nell'implementazione di PHP questi sono memorizzati in luoghi diversi e sono accessibili in modo diverso.
- Esiste un
extended_value
che viene utilizzato, ad esempio, in foreach loop per memorizzare ulteriori informazioni
- Il numero di riga del file di origine per riportare errori (il nome del file è memorizzato nella strucuture zend_op_array sopra)
- Il
handler
è una piccola ottimizzazione, che punta direttamente alla funzione che implementa questo.
Durante la compilazione dello script il compilatore creerà queste strutture dati e assicurerà che gli operandi e il valore restituito vengano visualizzati nello stesso posto, quindi con una chiamata come foo(bar())
il valore di ritorno della chiamata a bar
sarà lo stesso temporaneo come operando dell'opcode che spinge un argomento sullo stack di argomenti delle funzioni. Aspetta cosa - stack argomento !? potresti chiedere, a destra: poiché ogni codice operativo accetta solo due operandi, non possiamo passare direttamente i parametri di funzione, ma le prime operazioni di ZEND_SEND_*
riempiono uno stack, quindi un'operazione ZEND_DO_FCALL
/ ZEND_DO_FCALL_BY_NAME
verrà eseguita utilizzando tale stack.
Usando lo strumento vld possiamo scaricare questo modulo compilato:
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,
Il prossimo passo è l'esecuzione. Questo è essenzialmente questo ciclo (abbreviato):
while (1) {
if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
switch (ret) {
case 1:
return;
/* ... */
default:
break;
}
}
}
OPLINE
rappresenta l'elemento corrente della nostra matrice di strutture zend_op. Ogni op ha un gestore che viene eseguito. (per vedere il ciclo corretto: è in zend_vm_execute.h generato da zend_vm_gen.php da zend_vm_execute.skl e zend_vm_def.h) Se il gestore restituisce uno script / funzione / ... stiamo attualmente eseguendo dei ritorni, come il ciclo finisce.
Un semplice gestore di codice operativo può essere definito in questo modo (vedi 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();
}
Questo codice è simile a C ma è inserito nello script gen menzionato in precedenza. Vediamo che questo è l'opcode numero 1, che va sotto il nome di ZEND_ADD
che rappresenta un'aggiunta. Richiede entrambi gli operandi, che possono essere di entrambi i tipi (costanti, temporanee, variabili arbitrarie, variabili memorizzate nella cache del compilatore). Al termine dell'operazione il puntatore viene impostato sul prossimo codice operativo dell'array.