Assembleur et C

Voici quelques bases pour embarquer de l'assembleur dans un programme C en utilisant gcc. Cet article est dédié à l'architecture x86_64.

  1. Écrire le code d'assemblage en ligne : Dans votre fichier C, utilisez l'instruction asm pour insérer du code d'assemblage. Par exemple, pour ajouter deux nombres :
#include <stdio.h>

int main() {
    int a = 10, b = 20, result;

    asm("addl %%ebx, %%eax"
        : "=a"(result)   // output
        : "a"(a), "b"(b) // input
    );

    printf("Result: %d\n", result);
    return 0;
}
  1. Utiliser du code d'assemblage externe : Vous pouvez également écrire du code d'assemblage dans un fichier séparé avec une extension .asm ou .s, puis le compiler et lier avec votre programme C.

Exemple d'un fichier code.asm :

section .text
    global add_two_numbers
add_two_numbers:
    mov eax, [esp+4]
    add eax, [esp+8]
    ret

Fichier C (main.c) qui utilise cette fonction d'assemblage :

#include <stdio.h>

extern int add_two_numbers(int a, int b);

int main() {
    int a = 10, b = 20;
    int result = add_two_numbers(a, b);
    printf("Result: %d\n", result);
    return 0;
}

Remarque importante

__asm__ est souvent préféré à asm dans les programmes C pour inclure du code assembleur. Bien que les deux fonctionnent de manière similaire, il y a des raisons spécifiques pour lesquelles __asm__ est souvent recommandé :

  1. Conformité aux Normes : __asm__ est conforme aux normes de C et C++ en tant que mot-clé réservé pour l'assembleur inline. Cela garantit une meilleure portabilité et compatibilité avec différents compilateurs et versions du langage.

  2. Précision : En utilisant __asm__, tu indiques clairement au compilateur que tu utilises une extension spécifique du compilateur pour l'assembleur inline. Cela peut aider à éviter des ambiguïtés dans le code.

  3. Compatibilité avec GCC : GCC (GNU Compiler Collection) recommande l'utilisation de __asm__ pour l'assembleur inline. Certaines versions de GCC peuvent émettre des avertissements ou des erreurs lorsque asm est utilisé au lieu de __asm__.

Exemple de Code utilisant __asm__:

Voici comment écrire une fonction utilisant __asm__ pour insérer du code d'assemblage inline :

#include <stdio.h>

int main() {
    int a = 10, b = 20, result;

    __asm__("addl %%ebx, %%eax"
        : "=a"(result)   // output
        : "a"(a), "b"(b) // input
    );

    printf("Result: %d\n", result);
    return 0;
}

L'utilisation de __asm__ garantit que le code est conforme aux normes et peut être plus facilement accepté par différents compilateurs.

  1. Compiler et lier le programme : Utilisez gcc pour compiler et lier les fichiers ensemble :
nasm -f elf32 code.asm -o code.o
gcc -m32 main.c code.o -o main

FPU x87

Le FPU x87 représente un environnement d'exécution séparé, composé des 8 types de registres décrits ci-après:

classDiagram class FPU { + ST0: 80 bits + ST1: 80 bits + ST2: 80 bits + ST3: 80 bits + ST4: 80 bits + ST5: 80 bits + ST6: 80 bits + ST7: 80 bits + Status Register: 16 bits + Control Register: 16 bits + Tag Word Register: 16 bits + Last Instruction Pointer: 16 bits + Last Data Pointer: 16 bits + Opcode Register: 16 bits }

1. x87 FPU Data Registers

Ces registres sont au cœur du FPU x87 et sont utilisés pour stocker les valeurs en virgule flottante pendant les calculs. Il y a 8 registres de ce type, nommés de ST(0) à ST(7).

Les registres de données du FPU x87 (ST(0) à ST(7)) ont une taille de 80 bits. Ils sont capables de stocker des valeurs en précision étendue, ce qui permet des calculs en virgule flottante avec une précision plus élevée que les types de données standard en simple ou double précision.

Voici une répartition des 80 bits dans un registre de la FPU x87 :

  • 1 bit pour le signe (0 pour positif, 1 pour négatif)
  • 15 bits pour l'exposant (avec un biais de 16383)
  • 1 bit caché qui est toujours 1 pour les valeurs normales
  • 63 bits pour la mantisse

Cette structure permet une plus grande précision et une plus grande plage de valeurs pour les calculs en virgule flottante.

2. The Status Register

Le registre d'état contient des informations sur l'état actuel du FPU, telles que les indicateurs de débordement, de division par zéro, de précision, etc. Ce registre aide à déterminer si les opérations se sont déroulées correctement ou si des exceptions se sont produites.

classDiagram class StatusRegister { + Bit 0: IE + Bit 1: DE + Bit 2: ZE + Bit 3: OE + Bit 4: UE + Bit 5: PE + Bit 6: SF + Bit 7: ES + Bit 8: C0 + Bit 9: C1 + Bit 10: C2 + Bit 11: TOP + Bit 12: C3 + Bit 13: B + Bit 14: Reserved + Bit 15: Reserved }

Le status register du FPU x87 est un registre de 16 bits qui fournit des informations sur l'état actuel du FPU et les résultats des opérations en virgule flottante. Voici une description de chacun des bits dans ce registre :

  • Bit 0 (IE - Invalid Operation Exception) : Indicateur d'exception pour les opérations invalides.
  • Bit 1 (DE - Denormalized Operand Exception) : Indicateur d'exception pour les opérandes dénormalisés.
  • Bit 2 (ZE - Zero Divide Exception) : Indicateur d'exception pour la division par zéro.
  • Bit 3 (OE - Overflow Exception) : Indicateur d'exception pour le débordement.
  • Bit 4 (UE - Underflow Exception) : Indicateur d'exception pour le sous-débordement.
  • Bit 5 (PE - Precision Exception) : Indicateur d'exception pour la perte de précision.
  • Bit 6 (SF - Stack Fault) : Indicateur de faute de pile (débordement ou sous-débordement de la pile FPU).
  • Bit 7 (ES - Error Summary Status) : Indicateur de résumé des erreurs (un de bits 0 à 5 est 1).
  • Bit 8 (C0) : Bit de condition 0, utilisé pour les résultats d'opérations spécifiques.
  • Bit 9 (C1) : Bit de condition 1, utilisé pour les résultats d'opérations spécifiques.
  • Bit 10 (C2) : Bit de condition 2, utilisé pour les résultats d'opérations spécifiques.
  • Bit 11 (TOP) : Indicateur du sommet de la pile FPU, montrant quel registre est actuellement en haut de la pile.
  • Bit 12 (C3) : Bit de condition 3, utilisé pour les résultats d'opérations spécifiques.
  • Bit 13 (B) : Bit occupé, indiquant que le FPU est actuellement occupé à effectuer une opération.

Exemple de Fonction pour Lire le Status Register

Pour lire les valeurs du status register, nous pouvons utiliser une fonction en C avec de l'assembleur inline. Le programme ci-dessous lit les valeurs du status register, puis décompose et affiche chacun des bits pour fournir des informations détaillées sur l'état du FPU.

#include <stdio.h>

// Lire la valeur du status register
unsigned short get_fpu_status() {
    unsigned short status;
    asm("fstsw %0" : "=m"(status));
    return status;
}

int main() {
    unsigned short status = get_fpu_status();
    printf("FPU Status Register: 0x%04x\n", status);

    // Décomposition des bits du status register
    printf("Invalid Operation Exception (IE): %d\n", (status & 0x0001) != 0);
    printf("Denormalized Operand Exception (DE): %d\n", (status & 0x0002) != 0);
    printf("Zero Divide Exception (ZE): %d\n", (status & 0x0004) != 0);
    printf("Overflow Exception (OE): %d\n", (status & 0x0008) != 0);
    printf("Underflow Exception (UE): %d\n", (status & 0x0010) != 0);
    printf("Precision Exception (PE): %d\n", (status & 0x0020) != 0);
    printf("Stack Fault (SF): %d\n", (status & 0x0040) != 0);
    printf("Error Summary Status (ES): %d\n", (status & 0x0080) != 0);
    printf("Condition Code 0 (C0): %d\n", (status & 0x0100) != 0);
    printf("Condition Code 1 (C1): %d\n", (status & 0x0200) != 0);
    printf("Condition Code 2 (C2): %d\n", (status & 0x0400) != 0);
    printf("Top of Stack Pointer (TOP): %d\n", (status >> 11) & 0x07);
    printf("Condition Code 3 (C3): %d\n", (status & 0x1000) != 0);
    printf("Busy (B): %d\n", (status & 0x8000) != 0);

    return 0;
}

3. The Control Register

Le registre de contrôle permet de configurer le comportement du FPU, comme les modes d'arrondi, les masques d'exception, et la précision. Ce registre influence directement la manière dont les opérations en virgule flottante sont effectuées.

classDiagram class ControlRegister { + Bit 0-5: Exception Masks + Bit 8-9: Precision Control + Bit 10-11: Rounding Control + Bit 12: Infinity Control + Bit 6-7: Reserved + Bit 13-15: Reserved }

Le registre de contrôle du FPU x87 est particulièrement intéressant car il permet de modifier le comportement des calculs en virgule flottante. Il a également une taille de 16 bits et contient plusieurs champs qui contrôlent diverses fonctionnalités du FPU.

Structure du registre de contrôle

Voici la décomposition des bits du registre de contrôle :

  • Bit 0-5 : Masques d'exception

    • Bit 0 (IM - Invalid Operation Mask) : Masque pour l'exception d'opération invalide.
    • Bit 1 (DM - Denormalized Operand Mask) : Masque pour l'exception d'opérande dénormalisé.
    • Bit 2 (ZM - Zero Divide Mask) : Masque pour l'exception de division par zéro.
    • Bit 3 (OM - Overflow Mask) : Masque pour l'exception de débordement.
    • Bit 4 (UM - Underflow Mask) : Masque pour l'exception de sous-débordement.
    • Bit 5 (PM - Precision Mask) : Masque pour l'exception de perte de précision.
  • Bit 6-7 : Réservé

  • Bit 8-9 : Précision de calcul

    • Bit 8-9 (PC - Precision Control) :
      • 00 : Simple précision (24 bits)
      • 01 : Réservé
      • 10 : Double précision (53 bits)
      • 11 : Précision étendue (64 bits)
  • Bit 10-11 : Mode d'arrondi

    • Bit 10-11 (RC - Rounding Control) :
      • 00 : Arrondi vers l'infini positif
      • 01 : Arrondi vers l'infini négatif
      • 10 : Arrondi vers zéro
      • 11 : Arrondi vers le plus proche
  • Bit 12 : Mode d'arrondi de précision

    • Bit 12 (X - Infinity Control) : (Utilisé uniquement pour les processeurs 80287 et inférieurs)
      • 0 : Arrondi vers l'infini positif
      • 1 : Arrondi vers l'infini négatif

Exemple de Fonction pour Lire et Modifier le Registre de Contrôle

Nous pouvons lire et modifier les valeurs du registre de contrôle en utilisant des fonctions en C avec de l'assembleur inline. Voici quelques exemples :

Lire la valeur du registre de contrôle

#include <stdio.h>

// Lire la valeur du registre de contrôle
unsigned short get_fpu_control() {
    unsigned short control;
    asm("fstcw %0" : "=m"(control));
    return control;
}

int main() {
    unsigned short control = get_fpu_control();
    printf("FPU Control Register: 0x%04x\n", control);

    // Décomposition des bits du registre de contrôle
    printf("Invalid Operation Mask (IM): %d\n", (control & 0x0001) != 0);
    printf("Denormalized Operand Mask (DM): %d\n", (control & 0x0002) != 0);
    printf("Zero Divide Mask (ZM): %d\n", (control & 0x0004) != 0);
    printf("Overflow Mask (OM): %d\n", (control & 0x0008) != 0);
    printf("Underflow Mask (UM): %d\n", (control & 0x0010) != 0);
    printf("Precision Mask (PM): %d\n", (control & 0x0020) != 0);
    printf("Precision Control (PC): 0x%02x\n", (control >> 8) & 0x03);
    printf("Rounding Control (RC): 0x%02x\n", (control >> 10) & 0x03);
    printf("Infinity Control (X): %d\n", (control & 0x1000) != 0);

    return 0;
}

Modifier la valeur du registre de contrôle

#include <stdio.h>

// Modifier la valeur du registre de contrôle
void set_fpu_control(unsigned short control) {
    asm("fldcw %0" : : "m"(control));
}

int main() {
    unsigned short control = get_fpu_control();
    printf("Old FPU Control Register: 0x%04x\n", control);

    // Modifier pour utiliser la précision étendue et l'arrondi vers le plus proche
    control &= ~0x0300;      // Effacer les bits de contrôle de la précision
    control |= 0x0300;       // Définir la précision étendue (64 bits)
    control &= ~0x0C00;      // Effacer les bits de contrôle de l'arrondi
    control |= 0x0C00;       // Définir l'arrondi vers le plus proche

    set_fpu_control(control);

    unsigned short new_control = get_fpu_control();
    printf("New FPU Control Register: 0x%04x\n", new_control);

    return 0;
}

Ces deux exemples montrent comment lire et modifier les valeurs du registre de contrôle pour configurer le comportement du FPU x87 en fonction des exigences induites par le contexte du calcul à réaliser.

4. Tag Word Register

Le registre des étiquettes (tag word) identifie l'état des registres de données FPU (ST(0) à ST(7)). Il indique si chaque registre contient une valeur valide, une valeur spéciale comme NaN (Not a Number), une valeur infinie, ou s'il est vide.

Le tag word register est un registre essentiel pour surveiller l'état des registres de données du FPU x87. Il indique si les registres de la pile FPU contiennent des valeurs valides ou s'ils sont vides. Chaque registre de données ST(0) à ST(7) est associé à deux bits dans le tag word register, pour un total de 16 bits.

Structure du Tag Word Register

Chaque paire de bits dans le tag word register correspond à l'état d'un des registres de données de la FPU, selon la disposition suivante : - Bits 0-1 : État de ST(0) - Bits 2-3 : État de ST(1) - Bits 4-5 : État de ST(2) - Bits 6-7 : État de ST(3) - Bits 8-9 : État de ST(4) - Bits 10-11 : État de ST(5) - Bits 12-13 : État de ST(6) - Bits 14-15 : État de ST(7)

Les valeurs possibles pour chaque paire de bits sont les suivantes : - 00 : Registre valide (Non vide) - 01 : Zéro (Valide et représente zéro) - 10 : Spécial (NaN, infini, ou dénormalisé) - 11 : Vide

Exemple de Fonction pour Lire le Tag Word Register

Voyons comment lire et interpréter le tag word register en utilisant une fonction en C avec de l'assembleur inline :

#include <stdio.h>

// Lire la valeur du tag word register
unsigned short get_fpu_tag() {
    unsigned short tag;
    asm("fstcw %0" : "=m"(tag));
    return tag;
}

int main() {
    unsigned short tag = get_fpu_tag();
    printf("FPU Tag Word Register: 0x%04x\n", tag);

    // Décomposition des bits du tag word register
    for (int i = 0; i < 8; i++) {
        unsigned short tag_state = (tag >> (i * 2)) & 0x03;
        printf("ST(%d): ", i);
        switch (tag_state) {
            case 0x00:
                printf("Valid (Non vide)\n");
                break;
            case 0x01:
                printf("Zero\n");
                break;
            case 0x02:
                printf("Special (NaN, infini, dénormalisé)\n");
                break;
            case 0x03:
                printf("Empty (Vide)\n");
                break;
            default:
                printf("Unknown state\n");
                break;
        }
    }

    return 0;
}

Modification du Tag Word Register

Le tag word register est principalement utilisé pour indiquer l'état des registres de données du FPU, et il est mis à jour automatiquement par le FPU en fonction des opérations effectuées. En général, il n'est pas nécessaire ni recommandé de modifier manuellement ce registre, car cela peut entraîner des incohérences dans l'état du FPU.

En lisant et interprétant correctement le tag word register, tu peux surveiller efficacement l'état des registres de données du FPU x87 et t'assurer que les opérations en virgule flottante se déroulent comme prévu.

5. Last Instruction Pointer Register

Ce registre contient l'adresse de la dernière instruction FPU exécutée. Il est utile pour le débogage et la gestion des exceptions.

Tu as raison. Le "Last Instruction Pointer Register" contient l'adresse de la dernière instruction exécutée par le FPU x87. Ce registre est particulièrement utile pour le débogage et la gestion des exceptions, car il te permet de savoir exactement quelle instruction a été exécutée en dernier.

Accéder au "Last Instruction Pointer Register"

Pour accéder au "Last Instruction Pointer Register", nous pouvons utiliser les instructions fstenv et fldenv qui stockent et chargent l'état de l'environnement du FPU, y compris ce registre.

Voici un exemple de code en C utilisant de l'assembleur inline pour lire l'adresse de la dernière instruction exécutée :

#include <stdio.h>

// Structure pour stocker l'état de l'environnement du FPU
struct fpu_env {
    unsigned short control_word;
    unsigned short status_word;
    unsigned short tag_word;
    unsigned short instruction_pointer_offset;
    unsigned short instruction_pointer_selector;
    unsigned short opcode;
    unsigned int data_pointer_offset;
    unsigned short data_pointer_selector;
    unsigned int mxcsr;
    unsigned int reserved;
};

// Lire l'adresse de la dernière instruction exécutée
void get_last_instruction_pointer() {
    struct fpu_env env;
    asm("fstenv %0" : "=m"(env));  // Stocker l'état de l'environnement du FPU

    // Afficher l'adresse de la dernière instruction exécutée
    printf("Last Instruction Pointer: 0x%04x:0x%04x\n",
           env.instruction_pointer_selector, env.instruction_pointer_offset);
}

int main() {
    get_last_instruction_pointer();
    return 0;
}

Explication de l'Exemple

  1. Structure fpu_env : Cette structure est utilisée pour stocker l'état de l'environnement du FPU, y compris le registre du pointeur de la dernière instruction.
  2. Instruction fstenv : Cette instruction stocke l'état de l'environnement du FPU dans une structure spécifiée.
  3. Affichage de l'Adresse : Une fois que l'état de l'environnement est stocké, nous affichons l'adresse de la dernière instruction exécutée en utilisant les champs instruction_pointer_selector et instruction_pointer_offset.

En utilisant cette méthode, tu peux surveiller quelle instruction a été exécutée en dernier par le FPU, ce qui est extrêmement utile pour le débogage.

6. Last Data (Operand) Pointer Register

Ce registre contient l'adresse du dernier opérande utilisé par le FPU. Il est également utile pour le débogage et la gestion des exceptions.

Exemple de Fonction pour Lire le Last Data (Operand) Pointer Register

Voici un exemple de code en C utilisant de l'assembleur inline pour lire l'adresse de la dernière opérande utilisée :

#include <stdio.h>

// Structure pour stocker l'état de l'environnement du FPU
struct fpu_env {
    unsigned short control_word;
    unsigned short status_word;
    unsigned short tag_word;
    unsigned short instruction_pointer_offset;
    unsigned short instruction_pointer_selector;
    unsigned short opcode;
    unsigned int data_pointer_offset;
    unsigned short data_pointer_selector;
    unsigned int mxcsr;
    unsigned int reserved;
};

// Lire l'adresse de la dernière opérande utilisée
void get_last_data_pointer() {
    struct fpu_env env;
    asm("fstenv %0" : "=m"(env));  // Stocker l'état de l'environnement du FPU

    // Afficher l'adresse de la dernière opérande utilisée
    printf("Last Data (Operand) Pointer: 0x%04x:0x%08x\n",
           env.data_pointer_selector, env.data_pointer_offset);
}

int main() {
    float a = 3.0, b = 4.0, result;

    // Effectuer une addition en virgule flottante
    asm("fld %1\n"  // Charger a dans ST(0)
        "fld %2\n"  // Charger b dans ST(1)
        "faddp\n"   // Ajouter ST(0) et ST(1) et stocker le résultat dans ST(1)
        "fstp %0"   // Stocker le résultat dans result
        : "=m"(result) : "m"(a), "m"(b));

    printf("Result: %f\n", result);

    get_last_data_pointer();
    return 0;
}

Explication de l'Exemple

  1. Structure fpu_env : Utilisée pour stocker l'état de l'environnement du FPU, y compris le "Last Data (Operand) Pointer Register".
  2. Instruction fstenv : Stocke l'état de l'environnement du FPU dans une structure spécifiée.
  3. Affichage de l'Adresse : L'adresse de la dernière opérande utilisée est affichée en utilisant les champs data_pointer_selector et data_pointer_offset.

Cette méthode te permet de surveiller l'adresse de la dernière opérande utilisée par le FPU, ce qui peut être utile pour le débogage et la compréhension des opérations.

7. Opcode Register

Le registre d'opcode contient le code opération de la dernière instruction FPU exécutée. Il aide à identifier quelle instruction a été exécutée en cas d'exception.

Au premier abord ce registre peut sembler redondant, mais le opcode register et le last instruction pointer register ont des rôles distincts et complémentaires.

Last Instruction Pointer Register

  • Fonction : Contient l'adresse de la dernière instruction exécutée par le FPU x87.
  • Usage : Utilisé pour déterminer l'emplacement mémoire de la dernière instruction FPU exécutée. Ceci est particulièrement utile pour le débogage, car il permet de savoir où l'instruction a été exécutée.

Opcode Register

  • Fonction : Contient l'opcode de la dernière instruction exécutée par le FPU x87.
  • Usage : Utilisé pour identifier quelle instruction a été exécutée. Cela peut être crucial pour comprendre le contexte des erreurs ou des exceptions, car il donne un aperçu direct de l'opération effectuée.

Illustration avec un Extrait de Code

Voyons comment ces deux registres travaillent ensemble pour fournir des informations complètes sur la dernière instruction exécutée :

#include <stdio.h>

// Structure pour stocker l'état de l'environnement du FPU
struct fpu_env {
    unsigned short control_word;
    unsigned short status_word;
    unsigned short tag_word;
    unsigned short instruction_pointer_offset;
    unsigned short instruction_pointer_selector;
    unsigned short opcode;
    unsigned int data_pointer_offset;
    unsigned short data_pointer_selector;
    unsigned int mxcsr;
    unsigned int reserved;
};

// Lire l'adresse et l'opcode de la dernière instruction exécutée
void get_last_instruction_info() {
    struct fpu_env env;
    asm("fstenv %0" : "=m"(env));  // Stocker l'état de l'environnement du FPU

    // Afficher l'adresse et l'opcode de la dernière instruction exécutée
    printf("Last Instruction Pointer: 0x%04x:0x%04x\n",
           env.instruction_pointer_selector, env.instruction_pointer_offset);
    printf("Last Opcode: 0x%04x\n", env.opcode);
}

int main() {
    // Exemple d'instruction FPU
    float a = 1.0;
    asm("fld %0" : : "m"(a)); // Charger a dans ST(0)

    get_last_instruction_info();
    return 0;
}

Dans cet exemple : - Last Instruction Pointer Register : Contient l'adresse de la dernière instruction exécutée, permettant de localiser l'instruction en mémoire. - Opcode Register : Contient l'opcode de la dernière instruction exécutée, indiquant exactement quelle instruction a été exécutée.

Ensemble, ces registres fournissent une vue complète de la dernière instruction FPU exécutée, ce qui est essentiel pour le débogage et l'analyse des opérations en virgule flottante.

Exemple de Fonctions pour Lire les Valeurs des Registres Spéciaux

Maintenant que nous comprenons ces registres, créons des fonctions pour lire leurs valeurs. Voici un exemple en C utilisant de l'assembleur inline :

#include <stdio.h>

// Lire la valeur du registre de statut
unsigned short get_fpu_status() {
    unsigned short status;
    asm("fstsw %0" : "=m"(status));
    return status;
}

// Lire la valeur du registre de contrôle
unsigned short get_fpu_control() {
    unsigned short control;
    asm("fstcw %0" : "=m"(control));
    return control;
}

// Lire la valeur du registre des étiquettes
unsigned short get_fpu_tag() {
    unsigned short tag;
    asm("fstsw %0" : "=m"(tag));
    return tag;
}

int main() {
    unsigned short status = get_fpu_status();
    unsigned short control = get_fpu_control();
    unsigned short tag = get_fpu_tag();

    printf("FPU Status Register: 0x%04x\n", status);
    printf("FPU Control Register: 0x%04x\n", control);
    printf("FPU Tag Word Register: 0x%04x\n", tag);

    return 0;
}

Ces fonctions permettent de lire les valeurs des registres spéciaux du FPU x87.