• Aucun résultat trouvé

Arbres de syntaxes

La représentation sous forme de séquence de lexèmes présente des limitations lorsqu’il s’agit d’éviter l’obtention de correspondances chevauchant plusieurs unités syntaxiques. L’arbre de syntaxe d’une unité de compilation est une représentation plus riche représentant sous forme hiérarchique la structure de l’unité telle qu’elle est obtenue par l’application de la grammaire du

3.3. Arbres de syntaxes 48

langage. Ceci nous permet donc de rechercher des correspondances sur des unités syntaxiques entières : à cet effet nous pouvons utiliser des méthodes d’indexation de sous-arbres basées sur la production de valeur de hachage sur ceux-ci (voir chapitre 9). Il est également possible de quantifier la proximité entre unités syntaxiques de l’arbre afin d’utiliser des méthodes de consolidation et d’extension de correspondances que nous présentons au chapitre 11. La consolidation de correspondances basées sur une représentation par séquences de lexèmes est également réalisable, cependant la proximité de deux correspondances au sein de la séquence de lexèmes ne garantit pas nécessairement la pertinence de leur proximité syntaxique.

3.3.1 Obtention de l’arbre de syntaxe

Arbres de syntaxes concrets et grammaires non-contextuelles L’arbre de syntaxe concret d’une unité de compilation correspond au résultat de l’analyse syntaxique de cette unité en utilisant la grammaire du langage. Les analyses lexicale et syntaxique sur le code source brut peuvent être réalisées en une unique étape (cette approche est préférée par ANTLR [112]) ou plus fréquemment en deux étapes distinctes, le résultat de la lexémisation (la séquence de lexèmes) étant fourni à l’analyseur syntaxique. La plupart des langages de programmation sont conçus pour être syntaxiquement décrits par une grammaire non contex- tuelle. Une telle grammaire G = (Σ, Γ, R, S) est décrite par un ensemble de symboles termi- naux (Σ) qui représentent les lexèmes du langages, un ensemble de symboles non-terminaux (Γ) qui représentent les différents types de nœuds de l’arbre de syntaxe concret, un ensemble de règles de production R de la forme U ∈ Γ −→ V ∈ (Σ ∪ Γ)∗ et un symbole non-terminal

de départ S de Γ.

Méthodes d’analyse syntaxique L’ensemble des arbres de syntaxe concrets pouvant être obtenus à partir de l’analyse d’une chaîne de n lexèmes peut être calculé avec une complexité temporelle en O(n3) pour des grammaires non-ambiguës par les méthodes d’analyse de Cocke

Younger Kasami [23] et de Earley [7]. Dans la pratique, les langages de programmation peuvent être définis par des grammaires non-contextuelles non-ambiguës afin que chaque séquence de lexèmes ne puisse être interprétée que par un unique arbre de syntaxe concret. Des méthodes d’analyse de complexité temporelle moindre mais ne reconnaissant qu’un sous-ensemble des grammaires non-contextuelles non-ambiguës et nécessitant un pré-traitement par la génération de tables spécifiques sont le plus souvent utilisées pour l’analyse de langages de programma- tion. Parmi elles, la méthode d’analyse LL(k) permet une analyse descendante (du symbole de départ S aux feuilles terminales de Σ) avec dérivation à gauche avec k lexèmes d’anticipation pour le choix de la règle à développer. La méthode LR(k) et ses variantes (LALR(k)) réalise quant à elle une analyse syntaxique ascendante (des feuilles vers le symbole de départ) avec k lexèmes d’anticipation : elle permet l’analyse des langages non contextuels déterministes. L’analyse LR(k) est généralement privilégiée car reconnaissant une classe plus étendue de lan- gages que l’analyse LL(k) et autorisant une récupération sur erreur plus souple pour les codes sources présentant des erreurs syntaxiques. Une généralisation de l’analyse LR présentant un temps d’exécution plus bas que l’analyse de Earley a été proposée par Tomita [22] pour des grammaires ambiguës.

Quelques générateurs d’analyseurs syntaxiques Si générer manuellement un analyseur syntaxique LL(k ≤ 1) est possible, la complexité des tables d’analyse générées préalablement à une analyse LL(k) et LR(k) pour des valeurs de k élevées nécessite l’utilisation de générateurs

d’analyseurs syntaxiques. Parmi ces générateurs, nous pouvons citer ANTLR [112] créant des analyseurs LL(k) dans de multiples langages et JavaCC [105] générant des analyseurs en Java. Pour l’analyse LALR(1), SableCC [119] (multi-langages), CUP [116] (Java), Tatoo [106] (Java) ou Bison [114] (C) peuvent être utilisés. Ces générateurs proposent chacun leur propre format de définition de grammaire : les règles définies peuvent alors soit être associées à des actions dans le langage cible (Bison, Cup, ANTLR, ...), soit être utilisées pour construire directement un arbre de syntaxe concret qui pourra être ultérieurement manipulé (SableCC, ANTLR, ...). 3.3.2 Abstraction de l’arbre de syntaxe

L’arbre de syntaxe concret issu directement de l’analyse syntaxique par la grammaire du langage présente des nœuds sans réel intérêt sémantique. Par exemple, une liste d’arguments peut être représentée dans l’arbre concret par un arbre binaire de type peigne, certains lexèmes ou symboles non-terminaux utilisés pour la désambiguisation de la grammaire peuvent être présents avec des nœuds d’arité sortante unitaire. Par ailleurs, au-delà de cet aspect purement technique, il peut être utile d’introduire une abstraction au niveau de certains nœuds tels que les identificateurs, les commentaires ou certains modificateurs sans intérêt réel pour la recherche de similitudes. L’étape d’abstraction peut être menée directement durant l’analyse en définissant des actions adéquates associées aux règles de la grammaire. Elle peut aussi être réalisée en transformant l’arbre de syntaxe concret par l’utilisation de visiteurs ou la spécifi- cation de règles de réécriture dans un langage spécifique (tel que Stratego [121], Tom [122] ou TXL [123]).

Un des objectifs de l’étape d’abstraction pourrait être de définir pour chaque classe de lan- gage de programmation un jeu de nœuds internes et feuilles communs afin de pouvoir exprimer un code source sous une forme abstraite commune. Une telle représentation permettrait de traiter des cas d’obfuscation impliquant une traduction inter-langage du code source.

La figure 3.5 illustre l’analyse lexicale et syntaxique d’une instruction Java simple avec des exemples d’arbre de syntaxe concret et abstrait. On notera notamment la suppression de symboles terminaux et de nœuds d’arité sortante unaire.

3.3.3 Normalisation de l’arbre de syntaxe

L’arbre de syntaxe abstrait obtenu par l’analyse syntaxique du code source peut ensuite subir des opérations de transformations afin d’obtenir une forme normalisée de celui-ci. Une compréhension sémantique plus ou moins avancée du code est alors nécessaire afin de réaliser certaines opérations telles que :

1. La détermination et la suppression des sous-arbres inaccessibles à l’exécution (représen- tant du code mort).

2. La normalisation des expressions.

3. Le découpage de structures composées en structures élémentaires équivalentes.

Par exemple pour l’instruction Java for (int i=0 ; i < tab.length ; i++) somme += tab[i] ;, nous pouvons réaliser des opérations de normalisation afin d’obtenir l’arbre de syn- taxe abstrait suivant (exprimé sous forme de pseudo-code développé) : Nous notons qu’une instruction analogue utilisant une boucle de type tant que (while) serait normalisée sous la même forme.

3.3. A rbre s de sy ntax es 50 constant=0 expression int ) expression affectation identificateur=i < instruction incr´ementation postfixe expression += valeur de gauche identificateur=i instruction identificateur=i operateur type ( op´eration binaire expression expression identificateur=somme expression declaration identificateur=i expression [ boucle for identificateur=tab operateur identificateur=tab expression length ] instruction indice valeur droite instruction variable post-instruction pr´e-instruction condition valeur gauche identificateur identificateur operateur (+=) constante boucle for op´eration binaire identificateur declaration identificateur identificateur operateur (<) length incr´ementation postfixe identificateur affectation acc`es tableau identificateur type=int

Fig.3.5 – Un exemple d’arbre de syntaxe concret et abstrait de l’instruction Java for (int i=0 ; i < tab.length ; i++) somme += tab[i] ;

declaration(entier, i); affectation(i, 0); boucle(condition = (i < tab.length)) {

affectation(somme, somme + tab[i]); affectation(i, i+1); }

Fig.3.6 – Pseudo-code développé 3.3.4 Parcours de l’arbre de syntaxe

Le parcours en profondeur de l’arbre de syntaxe concret ou abstrait permet d’obtenir une séquence de lexèmes associée à une structure syntaxique. Le résultat obtenu diffère d’une ana- lyse lexicale directe du code source en fonction du degré d’abstraction et de normalisation préalablement appliqué à l’arbre de syntaxe concret. Ce procédé permet d’intégrer aux sé- quences de lexèmes des informations de limite syntaxique afin de borner les correspondances. Il devient également possible de limiter la profondeur de parcours de l’arbre de syntaxe afin d’obtenir des séquences de lexèmes dont l’alphabet intègre des symboles non-terminaux abs- traits. Ainsi, par exemple, l’instruction Java dont les arbres de syntaxes sont présentés en figure 3.5 pourrait être lexemisée, en se limitant à une profondeur 2 de l’arbre abstrait, en la séquence suivante exprimée en figure 3.7. Certaines modifications telles que la réécriture de l’expression conditionnelle ou de l’affectation sont neutres pour cette représentation.

début_for

declaration opération_binaire incrémentation_postfixe affectation fin_for

Fig. 3.7 – Résultat de la lexémisation par parcours en profondeur de l’arbre de syntaxe

3.4 Graphe d’appels