2 votes

Génération des fichiers de couverture .gcda via QEMU/GDB

Résumé exécutif : Je veux utiliser GDB pour extraire les compteurs d'exécution de la couverture stockés en mémoire dans ma cible embarquée, et les utiliser pour créer des fichiers .gcda (pour alimenter gcov/lcov).

Configuration :

  • Je peux compiler en cross avec succès mon binaire, ciblant ma cible embarquée spécifique - puis l'exécuter sous QEMU.
  • Je peux également utiliser le support GDB de QEMU pour déboguer le binaire (c'est-à-dire utiliser tar extended-remote localhost:... pour se connecter au serveur GDB QEMU en cours d'exécution, et contrôler entièrement l'exécution de mon binaire).

Couverture : Maintenant, pour effectuer une analyse de couverture "sur cible", je compile en cross avec -fprofile-arcs -ftest-coverage. GCC émet alors des compteurs sur 64 bits pour suivre les comptages d'exécution de blocs de code spécifiques.

En cas d'exécution normale (c'est-à-dire sur l'hôte, non compilé en cross), lorsque l'application se termine, __gcov_exit est appelé - et rassemble tous ces comptages d'exécution dans des fichiers .gcda (que gcov utilise ensuite pour rapporter les détails de la couverture).

Cependant, dans ma cible embarquée, il n'y a pas vraiment de système de fichiers - et libgcov contient essentiellement des ébauches vides pour toutes les fonctions __gcov_....

Solution de contournement via QEMU/GDB : Pour résoudre cela, et le faire de manière indépendante de la version de GCC, je pourrais lister les symboles liés à la couverture dans mon binaire via MYPLATFORM-readelf, et extraire ceux pertinents avec grep (par exemple __gcov0.Task1_EntryPoint, __gcov0.worker, etc) :

$ MYPLATFORM-readelf -s binaire | grep __gcov
...
46: 40021498  48 OBJECT  LOCAL  DEFAULT 4 __gcov0.Task1_EntryPoint
...

Je pourrais ensuite utiliser les décalages/tailles rapportés pour créer automatiquement un script GDB - un script qui extrait les données des compteurs via de simples vidages de mémoire (à partir du décalage, vider longueur octets vers un fichier local).

Ce que je ne sais pas (et n'ai pas réussi à trouver d'informations/outil pertinent), c'est comment convertir les paires résultantes de (décalage mémoire, données mémoire) en fichiers .gcda. Si un tel outil/script existe, j'aurais un moyen portable (indépendant de la plateforme) pour effectuer une couverture sur n'importe quelle plateforme prise en charge par QEMU.

Existe-t-il un tel outil/script ?

Toute suggestion/indication serait grandement appréciée.

MISE À JOUR : J'ai résolu cela moi-même, comme vous pouvez le lire ci-dessous - et j'ai écrit un article de blog à ce sujet.

1voto

ttsiodras Points 2355

Il s'est avéré qu'il y avait une (beaucoup) meilleure façon de faire ce que je voulais.

Le noyau Linux inclut une fonctionnalité portable liée à GCOV, qui abstrait les détails spécifiques à la version de GCC en fournissant ce point de terminaison :

size_t convert_to_gcda(char *buffer, struct gcov_info *info)

En gros, j'ai pu effectuer la couverture sur cible via les étapes suivantes :

Étape 1

J'ai ajouté trois versions légèrement modifiées des fichiers gcov linux à mon projet : base.c, gcc_4_7.c et gcov.h. J'ai dû remplacer certaines particularités de Linux à l'intérieur - comme vmalloc, kfree, etc. - pour rendre le code portable (et ainsi, compilable sur ma plateforme embarquée, qui n'a rien à voir avec Linux).

Étape 2

J'ai ensuite fourni ma propre __gcov_init...

typedef struct tagGcovInfo {
    struct gcov_info *info;
    struct tagGcovInfo *next;
} GcovInfo;
GcovInfo *headGcov = NULL;

void __gcov_init(struct gcov_info *info)
{
    printf(
        "__gcov_init appelé pour %s!\n",
        gcov_info_filename(info));
    fflush(stdout);
    GcovInfo *newHead = malloc(sizeof(GcovInfo));
    if (!newHead) {
        puts("Mémoire insuffisante!");
        exit(1);
    }
    newHead->info = info;
    newHead->next = headGcov;
    headGcov = newHead;
}

...et __gcov_exit :

void __gcov_exit()
{
    GcovInfo *tmp = headGcov;
    while(tmp) {
        char *buffer;
        int bytesNeeded = convert_to_gcda(NULL, tmp->info);
        buffer = malloc(bytesNeeded);
        if (!buffer) {
            puts("Mémoire insuffisante!");
            exit(1);
        }
        convert_to_gcda(buffer, tmp->info);
        printf("Émission de %6d octets pour %s\n", bytesNeeded, gcov_info_filename(tmp->info));
        free(buffer);
        tmp = tmp->next;
    }
}

Étape 3

Enfin, j'ai scripté mon GDB (pilotant QEMU à distance) via ceci :

$ cat coverage.gdb
tar extended-remote :9976
file bin.debug/fputest
b base.c:88  <================= Cela se casse sur le printf "Émission" dans __gcov_exit
commands 1
    silent
    set $filename = tmp->info->filename
    set $dataBegin = buffer
    set $dataEnd = buffer + bytesNeeded
    eval "dump binary memory %s 0x%lx 0x%lx", $filename, $dataBegin, $dataEnd
    c
end
c
quit

Et enfin, j'ai exécuté à la fois QEMU et GDB - comme ceci :

$ # Dans le terminal 1 :
qemu-system-MYPLATFORM ... -kernel bin.debug/fputest  -gdb tcp::9976 -S

$ # Dans le terminal 2 :
MYPLATFORM-gdb -x coverage.gdb

...et c'est tout - j'ai pu générer les fichiers .gcda dans mon système de fichiers local, pour ensuite voir les résultats de la couverture avec gcov et lcov.

MISE À JOUR : J'ai écrit un article de blog montrant le processus en détail.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X