Assembleur LLVM

L'assembleur LLVM est la représentation textuelle du code intermédiaire du compilateur LLVM. Vous trouverez les spécifications complètes du langage sur le site du projet : http://llvm.org/docs/LangRef.html.

1. Compilation

Les fichiers contenants du code assembleur LLVM doivent porter l'extension ".ll"

Exemple :

hello_world.ll:
@str = internal constant [14 x i8] c"Hello, world!\00"
declare i32 @puts(i8*)
define i32 @main() {
  %adr = getelementptr [14 x i8]* @str, i32 0,i32 0
  call i32 @puts(i8* %adr)
  ret i32 0
}

L'utilitaire llc permet de transformer du code assembleur LLVM en code natif (assembleur x86 sur les machines intel).

Par exemple pour transformer hello_world.ll en assembleur natif :
$ llc hello_world.ll

Le code généré est écrit dans le fichier hello_world.s que vous pouvez ensuite compiler avec le compilateur de votre choix (gcc par exemple).

2. Structure de l'assembleur LLVM

Les fonctions

L'assembleur LLVM permet la définition de fonctions avec un type de retour et une liste de paramètres.

Syntaxe basique des déclarations de fonctions :
define <ResultType> @<FunctionName> ([argument list]) {...}

<ResultType> est le type de retour de la fonction et <argument list> est la liste des paramètres de la fonction. Ces paramètres sont exprimés de la même manière qu'en C.

Exemple :
define i32 @add(i32 %a, i32 %b) {
    %1 = add i32 %b, %a
    ret %1
}
Ce code définit la fonction add qui retourne la somme des deux entiers passés en paramètres.

Types et identifiants

Les types :

Les types de base en assembleur LLVM sont :

Des types plus complexe :

Les identifiants :

En LLVM, les identifiants commencent toujours par % ou @ en fonction de leur visibilité. Les identifiants dont la visibilité est globale (comme les fonctions ou les variables globales) commencent par un @, alors que les identifiants locaux commencent par le signe %. Après ce préfixe les identifiants peuvent être composés de n'importe quel caractère parmi les lettres (minuscules ou majuscules), les chiffres, le blanc souligné, le point, et le signe dollar.

3. Les principales instructions

Voici une liste des principales instructions du langage LLVM (vous pouvez trouver la liste complète sur : http://llvm.org/docs/LangRef.html#instruction-reference. Les types réels et entiers peuvent être également des vecteurs d'entiers ou de réels.

Instructions arithmétique

Instruction Syntaxe Exemple Description
add <result> = add <ty> <op1>, <op2> %0 = add i32 %1, %2 Somme entière. <ty> doit être un type entier.
sub <result> = sub <ty> <op1>, <op2> %0 = sub i32 %1, %2 Soustraction entière <ty> doit être un type entier.
fadd <result> = fadd <ty> <op1>, <op2> %0 = fadd float %1, %2 Somme flottante. <ty> doit être un type réel.
fsub <result> = fsub <ty> <op1>, <op2> %0 = fsub i32 %1, %2 Soustraction flottante <ty> doit être un type réel.
fdiv <result> = fdiv <ty> <op1>, <op2> %0 = fsub float %1, %2 Division flottante <ty> doit être un type réel.

Instructions de branchement (et logique)

Instruction Syntaxe Exemple Description
icmp <result> = icmp <cond> <ty> <op1>, <op2> %0 = icmp eq i32 %1, %2 Comparaison entière. <cond> donne le type de comparaison à faire. <cond> peut être :
  • eq : égalité
  • ne : non égalité
  • ugt : plus grand (non signé)
  • uge : plus grand ou égal (non signé)
  • ult : plus petit (non signé)
  • ule : plus petit ou égal (non signé)
  • sgt : plus grand (signé)
  • sge : plus grand ou égal (signé)
  • slt : plus petit (signé)
  • use : plus petit ou égal (signé)
<op1> et <op2> doivent être de type entier ou pointeur. Le type retourné est un booléen (i1).
fcmp <result> = fcmp <cond> <ty> <op1>, <op2> %0 = fcmp oeq float %1, 1.5 Comparaison flottante. <cond> donne le type de comparaison à faire. <cond> peut être :
  • oeq : égalité
  • ogt : supériorité
  • oge : supériorité ou égalité
  • olt : infériorité
  • ole : infériorité ou égalité
  • one : différent
<op1> et <op2> doivent être d'un type réel et ne doivent pas contenir de valeurs non définies (nan). Le type retourné est un booléen (i1).
call <result> = <ty> call <func> (<args>) %0 = call i32 @add(i32 %1, i32 %2) Appel de fonction. Les paramètres précédés de leur type.
br br i1 <cond>, label <iftrue>, label <iffalse> cf code fonction max. Branchement conditionnel
ret ret <ty> <value>
ret void
ret i32 %a Retour de fonction. <ty> est le type de retour de la fonction et <value> la valeur retournée.

Instructions mémoire :

Instruction Syntaxe Exemple Description
alloca <ptr> = alloca <ty> %ptr = alloca i32 Alloue de la mémoire dans la pile de la fonction courante pour une variable de type <ty>. <ptr> est donc de type <ty>* (un pointeur sur le type alloué).
load <result> = load <ty>* <pointer> %a = load i32* %a_ptr Charge la valeur à l'adresse pointée par <pointer> dans la variable <result>.
store store <ty> <value>, <ty>* <pointer> store i32 %a, i32* %a_ptr Sauvegarde la valeur <value> à l'adresse pointée par <pointer>.
getelementptr <result> = getelementptr <pty>* <ptrval>{, <ty> <idx>}* cf exemple accès aux éléments d'un tableau Permet de calculer l'adresse d'une variable (comem l'opérateur & en C). Si ty et idx sont spécifié getelementptr retourne ptrval + idx*sizeof(pty). Cette instruction permet donc de calculer l'adresse des différents éléments d'un tableau.

Fonction max :
define i32 @max(i32 %a, i32 %b) {
  %cond = icmp sgt i32 %a, %b
  br i1 %cond, label %ifGt, label %ifLt
  ifGt:
  ret i32 %a
  ifLt:
  ret i32 %b
}

4. SSA et accès aux tableaux

Accès aux scalaires ;

L'assembleur LLVM est en forme SSA (single static assignment) c'est à dire que les variables du langage ne peuvent être affectées qu'une seule fois. Pour générer le code LLVM, vous allouerez donc les variables définies dans le code d'entrée dans la pile des fonctions (avec l'instruction alloca). Et vous accéderez à ces variables via les instructions load et store. Ainsi dans le langage LLVM que vous générerez, les variables que vous manipulerez seront des pointeurs que vous n'aurez pas besoin de modifier. Ce code sera donc conforme à la forme SSA.

Accès aux éléments des tableaux :

Bien que le type tableau existe en assembleur LLVM l'accès à leurs éléments ne peut se faire avec l'opérateur crochet comme en C. Pour accéder à un élément d'un tableau il faut calculer sont adresse et y accéder comme vous feriez pour un scalaire.

Par exemple:

Code C:
int elt(int n, int *tab) {
     return tab[n];
}

Code LLVM équivalent:
define i32 @elt(i32 %n, i32* %t) {
  %addr = getelementptr i32* %t, i32 %n
  %ret = load i32* %addr
  ret i32 %ret
}

5. Explications sur l'exemple hello_world

En LLVM les variables globales sont toujours des pointeurs. Aussi dans la déclaration suivant :

Variable globale :
@str = internal constant [14 x i8] c"Hello, world!\00"

le type de @str est [14 x i8]* (et non juste un tableau de caractères : [14 x i8]). C'est la raison pour laquelle on utilise deux indices dans l'instruction getelementptr. Pour calculer l'adresse du premier caractère de la chaîne. En C on aurait calculé sont adresse comme suit :

char *str[14];
char *adr = &str[0][0];