Obtenir un dump des opcodes d'un script PHP

18 février 2013php, opcode
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

Lors de l’exécution d’un script PHP, l’interpréteur PHP passe par deux étapes :

  • Tout d’abord, le script écrit en langage PHP est compilé en opcodes,
  • Puis ces opcodes sont exécutés.

Basiquement, les opcodes sont des instructions simples ; nettement plus, en tout cas, que le langage PHP que nous écrivons.

Dans notre vie de tous les jours, savoir que ce processus PHP -> opcodes -> exécution existe est utile : il nous permet par exemple de comprendre l’utilité d’un cache d’opcodes comme APC. Mais, heureusement, il est rare que nous ayons à nous pencher sur les opcodes en eux-même.

Cela dit, pour les plus curieux d’entre nous1, il existe des extensions PHP permettant d’obtenir la liste des opcodes correspondant à un script PHP.

Installation de VLD

L’extension VLD - Vulcan Logic Dumper est généralement celle à laquelle je pense à premier, lorsque je veux obtenir un dump des opcodes correspondant à un script PHP.

Il s’agit d’une extension PECL, qui s’installe comme n’importe quelle autre extension du genre, via la commande pecl install :

~/bin/php-5.3/bin/pecl install vld
downloading vld-0.11.2.tgz ...
Starting to download vld-0.11.2.tgz (16,403 bytes)

Cette extension étant en bêta2, si vous n’avez sélectionné le channel par défaut, vous aurez peut-être à utiliser :

~/bin/php-5.3/bin/pecl install vld-beta
downloading vld-0.11.2.tgz ...
Starting to download vld-0.11.2.tgz (16,403 bytes)

Et pour vérifier que l’extension se charge, elle doit apparaitre en sortir d’un php -m, à partir du moment où extension=vld.so est spécifié comme option :

$ ~/bin/php-5.3/bin/php -dextension=vld.so -m
[PHP Modules]
bcmath
bz2
calendar
...
tokenizer
vld
xml
...

L’autre solution serait d’ajouter extension=vld.so à votre fichier php.ini ou à un des fichiers interprétés par PHP — mais je n’ai pas tendance à le faire pour cette extension, puisque je ne souhaite la charger que très rarement, et pas “par défaut”.

Obtenir un premier dump d’opcodes

Une fois l’extension VLD installée, il va nous être possible d’obtenir un dump d’opcodes pour un script PHP.

Par exemple, considérons le script PHP suivant — qui est des plus simples, vous l’admettrez :

<?php
echo "Hello, World";

Pour obtenir la liste des opcodes correspondant à ce script, PHP doit être invoqué en ligne de commandes, en chargeant l’extension VLD (-dextension=vld.so), et en lui spécifiant qu’elle doit être active (-dvld.active=1) :

$ ~/bin/php-5.3/bin/php -dextension=vld.so -dvld.active=1 exemples/001-hello-world.php
Finding entry points
Branch analysis from position: 0
Return found
filename:       /.../exemples/001-hello-world.php
function name:  (null)
number of ops:  2
compiled vars:  none
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ECHO                                                     'Hello%2C+World'
   3     1    > RETURN                                                   1

branch: #  0; line:     2-    3; sop:     0; eop:     1
path #1: 0,
Hello, World

Il est possible de jouer avec la verbosité pour obtenir une sortie plus ou moins riche, en utilisant l’option vld.verbosity, qui vaut 1 par défaut. Il est possible de la baisser à 0, ou l’augmenter — une valeur de 3, par exemple, ajoute des informations portant sur les types de données manipulées.

L’option vld.execute est dans certains cas intéressante, puisqu’elle permet d’éviter l’exécution du code PHP — afin de n’obtenir en sortie que les opcodes, et pas les affichages provoqués par le script.

En jouant avec les options vld.verbosity et vld.execute, voici un exemple de sortie que nous pourrions obtenir :

$ ~/bin/php-5.3/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/001-hello-world.php
filename:       /.../exemples/001-hello-world.php
function name:  (null)
number of ops:  2
compiled vars:  none
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ECHO                                                     'Hello%2C+World'
   3     1    > RETURN                                                   1

branch: #  0; line:     2-    3; sop:     0; eop:     1
path #1: 0,

Nous n’avons pas, cette fois-ci, obtenu l’affichage du "Hello, World" ; et moins d’informations sont affichées autour de la liste des opcodes.

Des exemples avec des scripts PHP plus longs ?

Nous avons vu juste au-dessus que l’extension VLD nous permettait d’obtenir un dump des opcodes correspondant à un script PHP.

Cela dit, un “Hello, World” n’est pas forcément l’exemple le plus intéressant… Passons donc à quelque chose d’un peu plus complet !

Avec une déclaration et un appel de fonction

Tout d’abord, prenons un exemple où nous déclarons une fonction, et l’appelons en lui passant un paramètre — pour ensuite afficher la valeur retournée :

<?php
function hello($who) {
    return sprintf("Hello, %s!", $who);
}

echo hello('World');

La sortie renvoyée par VLD pour ce script est la suivante :

$ ~/bin/php-5.3/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/003-function.php
filename:       /.../exemples/003-function.php
function name:  (null)
number of ops:  5
compiled vars:  none
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   NOP                                                      
   6     1      SEND_VAL                                                 'World'
         2      DO_FCALL                                      1  $0      'hello'
         3      ECHO                                                     $0
   7     4    > RETURN                                                   1

branch: #  0; line:     2-    7; sop:     0; eop:     4
path #1: 0,
Function hello:
filename:       /.../exemples/003-function.php
function name:  hello
number of ops:  6
compiled vars:  !0 = $who
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   RECV                                                     1
   3     1      SEND_VAL                                                 'Hello%2C+%25s%21'
         2      SEND_VAR                                                 !0
         3      DO_FCALL                                      2  $0      'sprintf'
         4    > RETURN                                                   $0
   4     5*   > RETURN                                                   null

branch: #  0; line:     2-    4; sop:     0; eop:     5
path #1: 0,
End of function hello.

Cette sortie se découpe en deux portions :

  • L’appel de notre fonction,
  • Et ensuite, sa déclaration.

En essayant de formuler en français ce que la portion correspondant à l’appel de la fonction fait, nous aurions :

  • Tout d’abord, la valeur 'Hello' est envoyée,
  • Puis, la fonction 'hello' est appelée ; sa valeur de retour étant stockée dans $0,
  • Cette valeur $0 est alors affichée,
  • Et enfin, le script se termine avec un retour à 1.

Et pour la déclaration de la fonction hello(), maintenant :

  • Tout d’abord, notre fonction reçoit un paramètre,
  • Puis elle empile la chaine "Hello, %s!" — celle que nous passons en premier paramètre à sprintf(),
  • Après quoi elle empile le second paramètre passé à la fonction sprintf() — qui est le premier paramètre reçu en argument par notre fonction : !0 que nous avions nommé $who dans le code PHP (Cette correspondance entre !0 et $who est indiquée au-dessus de la liste d’opcodes de la fonction, dans la section compiled vars),
  • Pour ensuite appeler fonction sprintf(), en stockant sa valeur de retour dans $0,
  • Et cette valeur de retour de sprintf(), $0, sera elle-même retournée par notre fonction.
  • On notera que la liste d’opcodes de cette fonction se termine par deux opcodes RETURN ; le second, qui aurait retourné null, ne sera jamais exécuté (puisqu’il est précédé du RETURN $0), et est donc marqué d’une * par VLD.

Et une classe, alors ?

Une question, qu’on aura parfois tendance à se poser après avoir vu l’exemple précédent, tourne autour des classes : “mais comment est-ce que ça marche pour une classe et des méthodes ?”. Je serais tenté de répondre que la meilleure façon de savoir est d’essayer ; avec une portion de code telle que celle que je reproduis ci-dessous :

<?php
class MaClasse {
    protected $prop;
    public function __construct($valeur) {
        $this->prop = $valeur;
    }
    public function add($valeur) {
        $this->prop += $valeur;
    }
    public function getValeur() {
        return $this->prop;
    }
}

$obj = new MaClasse(32);
$obj->add(10);
$result = $obj->getValeur();
echo $result;

La sortie obtenue via l’extension VLD est relativement longue ; et je ne vais pas la reproduire d’un seul trait ; voici ce que nous obtenons pour le script — pour le code en dehors de la classe, les quelques lignes en bas de l’exemple de code PHP reproduit ci-dessus :

$ ~/bin/php-5.3/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/003-classe.php
filename:       /.../exemples/003-classe.php
function name:  (null)
number of ops:  14
compiled vars:  !0 = $obj, !1 = $result
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   NOP                                                      
  15     1      ZEND_FETCH_CLASS                              4  :1      'MaClasse'
         2      NEW                                              $2      :1
         3      SEND_VAL                                                 32
         4      DO_FCALL_BY_NAME                              1          
         5      ASSIGN                                                   !0, $2
  16     6      ZEND_INIT_METHOD_CALL                                    !0, 'add'
         7      SEND_VAL                                                 10
         8      DO_FCALL_BY_NAME                              1          
  17     9      ZEND_INIT_METHOD_CALL                                    !0, 'getValeur'
        10      DO_FCALL_BY_NAME                              0  $8      
        11      ASSIGN                                                   !1, $8
  18    12      ECHO                                                     !1
  19    13    > RETURN                                                   1

Je vous laisse parcourir ces 14 opcodes pour essayer de les comprendre ; dans les grandes lignes, une classe est chargée via ZEND_FETCH_CLASS, puis instanciée à l’aide de NEW, après quoi son constructeur est appelé (instruction 4, avec DO_FCALL_BY_NAME). Les méthodes add() et getValeur() sont ensuite invoquées, en stockant la valeur de retour de la seconde dans $result, qui finit par être affichée.

Viennent ensuite les listes d’opcodes de chacune des méthodes de la classe, qui constituent autant de sous-ensembles que l’on pourrait considérer comme indépendant.
Commençons par le constructeur :

Class MaClasse:
Function __construct:
filename:       /.../exemples/003-classe.php
function name:  __construct
number of ops:  4
compiled vars:  !0 = $valeur
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   4     0  >   RECV                                                     1
   5     1      ZEND_ASSIGN_OBJ                                          'prop'
         2      ZEND_OP_DATA                                             !0
   6     3    > RETURN                                                   null

branch: #  0; line:     4-    6; sop:     0; eop:     3
path #1: 0,
End of function __construct.

Simplement, la valeur reçue en paramètre, via la variable $valeur ici désignée par !0, est assignée à la propriété prop.

Suit la définition de la méthode add(), qui ajoute à prop la valeur reçue via le paramètre $valeur, ici aussi désigné par !0 :

Function add:
filename:       /.../exemples/003-classe.php
function name:  add
number of ops:  4
compiled vars:  !0 = $valeur
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   7     0  >   RECV                                                     1
   8     1      ASSIGN_ADD                                   88          'prop'
         2      ZEND_OP_DATA                                             !0
   9     3    > RETURN                                                   null

branch: #  0; line:     7-    9; sop:     0; eop:     3
path #1: 0,
End of function add.

Et, finalement, la dernière méthode de notre classe, getValeur(), qui charge la valeur de la propriété prop et la retourne à l’appelant :

Function getvaleur:
filename:       /.../exemples/003-classe.php
function name:  getValeur
number of ops:  3
compiled vars:  none
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
  11     0  >   FETCH_OBJ_R                                      $0      'prop'
         1    > RETURN                                                   $0
  12     2*   > RETURN                                                   null

branch: #  0; line:    11-   12; sop:     0; eop:     2
path #1: 0,
End of function getvaleur.

End of class MaClasse.

Pour une quinzaine de lignes de code PHP, ça représente un bon paquet de lignes en sortant de VLD, n’est-ce pas ? Mais, malgrés tout, à condition de ne pas prendre peur devant tous ces opcodes, cette sortie reste globalement compréhensible, non ?

Un peu de structures conditionnelles / boucles

Pour illustrer le type d’enchainements d’opcodes correspondant à des boucles et conditions, prenons l’exemple de la portion de code suivante (qui pourrait être écrite de façon un peu plus concise, j’en conviens) :

<?php
for ($i=0 ; $i<10 ; $i++) {
    if ($i % 2 === 0) {
        echo "Pair : ";
    }
    else {
        echo "Impair : ";
    }
    echo $i, "\n";
}

Avec un niveau de verbosité à 0, la sortie obtenue avec VLD est la suivante :

$ ~/bin/php-5.3/bin/php -dextension=vld.so -dvld.active=1 -dvld.verbosity=0 -dvld.execute=0 exemples/003-control.php
filename:       /.../exemples/003-control.php
function name:  (null)
number of ops:  16
compiled vars:  !0 = $i
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ASSIGN                                                   !0, 0
         1  >   IS_SMALLER                                       ~1      !0, 10
         2    > JMPZNZ                                        6          ~1, ->15
         3  >   POST_INC                                         ~2      !0
         4      FREE                                                     ~2
         5    > JMP                                                      ->1
   3     6  >   MOD                                              ~3      !0, 2
         7      IS_IDENTICAL                                     ~4      ~3, 0
         8    > JMPZ                                                     ~4, ->11
   4     9  >   ECHO                                                     'Pair+%3A+'
   5    10    > JMP                                                      ->12
   7    11  >   ECHO                                                     'Impair+%3A+'
   9    12  >   ECHO                                                     !0
        13      ECHO                                                     '%0A'
  10    14    > JMP                                                      ->3
  11    15  > > RETURN                                                   1

branch: #  0; line:     2-    2; sop:     0; eop:     0; out1:   1
branch: #  1; line:     2-    2; sop:     1; eop:     2; out1:  15; out2:   6
branch: #  3; line:     2-    2; sop:     3; eop:     5; out1:   1
branch: #  6; line:     3-    3; sop:     6; eop:     8; out1:   9; out2:  11
branch: #  9; line:     4-    5; sop:     9; eop:    10; out1:  12
branch: # 11; line:     7-    9; sop:    11; eop:    11; out1:  12
branch: # 12; line:     9-   10; sop:    12; eop:    14; out1:   3
branch: # 15; line:    11-   11; sop:    15; eop:    15
path #1: 0, 1, 15,
path #2: 0, 1, 6, 9, 12, 3, 1, 15,
path #3: 0, 1, 6, 11, 12, 3, 1, 15,

Cela peut sembler être un peu cryptique si on regarde de loin, avec des sauts dans tous les sens3, mais, finalement, c’est “juste” la transcription de la portion de code que nous avons écrite — celle-ci pourrait être lu de la manière suivante :

  • Instruction 0 : On assigne 0 à la variable $i ; qui est représentée par !0, comme indiqué dans la section compiled vars.
  • Instruction 1 : “Est-ce que $i est inférieur à 10” est stocké dans ~1.
  • Instruction 2 :
    • Si ~1 est faux, on saute vers l’instruction 15 (cf seconde colonne, celle titrée “#”), marquant ainsi la fin de la boucle.
    • Sinon, on saute vers l’instruction 6 (on entre dans le corps de la boucle for, autrement dit — en passant après les opcodes 3 à 5 qui correspondent à l’operation du for, et au retour au début, au test de la condition).
  • Instruction 3 : on post-incrémente $i, et on stocke le résultat de cette post-incrémentation dans ~2 ; c’est l’opération effectuée en troisième partie de la boucle for.
  • Instruction 4 : on libère ~2, qui correspond au résultat de la post-incrémentation de $i ; dans la pratique, stocker ce résultat lors de l’instruction 3 a été complètement inutile, puisque nous le libérons immédiatement après.
  • Instruction 5 : on saute vers l’instruction 1 ; en somme, après avoir incrémenté $i, nous revenons au test de la condition ; nous bouclons.
  • Instruction 6 : on calcule le modulo 2 de $i, et stocke le résultat dans ~3 ; nous sommes à l’intérieur du corps de la structure for.
  • Instruction 7 : on teste si ~3, le résultat de ce modulo, est identique (égalité stricte avec l’opérateur ===) à 0, et stocke le résultat de la comparaison dans ~4.
  • Instruction 8 : si ~4 est faux (le résultat du modulo n’est pas égal à 0 ; $i est donc un nombre impair), on saute vers l’instruction 11.
  • Instruction 9 : on affiche "Pair : " ; on est dans le corps du if.
  • Instruction 10 : on saute vers l’instruction 12 ; nous sommes arrivés à la fin du corps du if, et sautons après le corps du else, pour ne pas l’exécuter.
  • Instruction 11 : on affiche "Impair : " ; on est dans le corps du else.
  • Instruction 12 : on affiche la valeur de $i ; on est après l’intégralité de la construction if/else.
  • Instruction 13 : on affiche un retour à la ligne.
  • Instruction 14 : on saute vers l’instruction 3 ; vers l’opération de post-incrémentation de $i, effectuée en troisième partie de la construction for.
  • Instruction 15 : c’est la fin du script.

Cela devrait vous aider à comprendre l’enchainement logique des quelques lignes d’opcodes reproduites ci-dessus ;-)

D’autres extensions ?

Pour cet article, j’ai choisi de parler quasi-exclusivement de l’extension VLD.

Cela dit, il existe d’autres extensions qui permettent d’accéder aux opcodes d’un script PHP.
Je pense notamment aux extensions Bytekit et Parsekit.

La première, Bytekit, dont on peut trouver les sources sur github, est connue en partie gràce à l’outil bytekit-cli ; cet utilitaire en ligne de commande permet d’utiliser l’extension Bytekit pour effectuer certains types d’analyses de code, comme la détection d’utilisation de certains opcodes.

La seconde, Parsekit, est disponible sur PECL, et exporte au code PHP quelques fonctions qui permettent d’obtenir des listes d’opcodes correspondant à du code PHP, depuis un script PHP.

Cela dit, ces deux extensions ne me semblent plus tellement maintenues :

  • Pour Bytekit, les sources que l’on trouve sur github semblent correspondre à une copie de la dernière version “officiellement publiée” des sources : elles ont été importées vers github en mai 2011, et jamais modiées depuis — le site officiel n’existe plus, d’ailleurs.
  • Et pour Parsekit, la dernière version a été diffusée en 2009 ; la précédente remontant à 2006.

Voila pourquoi j’ai préféré parler de VLD tout au long de cet article, cette extension ayant été mise à jour pour la dernière fois suite à la sortie de PHP 5.4.

Liens :


Au cours de cet article, nous avons vu comment installer l’extension VLD pour obtenir un dump des opcodes correspondant à un script PHP. Nous avons aussi étudié les dumps correspondant à des exemples relativement simples, afin de comprendre leur structure et de nous familiariser avec les opcodes, leur syntaxe, et la logique de leur enchainement.

Dans quelques jours, je publierai un second article sur le sujet des opcodes, où je montrerai comment utiliser VLD pour obtenir un graphe des chemins correspondant à un dump d’opcodes — et donc, au code PHP initialement écrit.

Et je profiterai de l’occasion pour montrer que, des fois, mettre à jour PHP est une bonne idée ;-)



  1. Et peut-être ceux qui ont eu la chance de faire un peu d’Assembleur dans leur jeune temps ? Et qui regretteraient cette bonne vieille époque ? ↩︎

  2. L’extension VLD n’a jamais été marquée comme “stable”, je serais presque tenté de supposer que c’est fait exprès pour décourager ceux qui voudraient l’installer en production (chose que, bien évidemment, vous n’avez pas à faire ^^)↩︎

  3. A chaque fois que je vois un truc qui ressemble à ça, je ne peux m’empêcher de l’époque où j’ai eu l’occasion d’écrire un peu d’Assembleur Motorola 68000 — et, surtout, de lire celui que GCC générait lorsque je compilais du code C à destination de ma TI-92+, et que je voulais comprendre quelle écriture C donnait le code assembleur le plus efficace (Sur 10 MHz, pour arriver à avoir un FPS raisonable, il fallait optimiser ; pas d’autre solution ^^).
    Je n’en referais pas dans “la vie de tous les jours”, mais le petit sentiment de nostalgie est bien présent : après, qu’est-ce que c’était fun… ↩︎

Vous avez apprécié cet article ? Faites-le savoir !

Ce blog a récemment été migré vers un générateur de sites statiques et je n'ai pas encore eu le temps de remettre un mécanisme de commentaires en place.

Avec un peu de chance, je parviendrai à m'en occuper d'ici quelques semaines ;-)