Analýza zdrojového kódu nástrojem gcov

18. 4. 2023
Doba čtení: 32 minut

Sdílet

 Autor: Depositphotos
V tomto článku se seznámíme s velmi užitečným nástrojem nazvaným gcov. Jedná se o jeden z nástrojů z ekosystému GCC, jenž slouží pro zjištění, které řádky v programovém kódu jsou skutečně volány (například z testů) a které nikoli.

Obsah

1. Analýza zdrojového kódu nástrojem gcov

2. První seznámení s nástrojem gcov

3. Zavolání nástroje gcov z příkazové řádky

4. Jednoduchý program napsaný v C, který budeme zkoumat nástrojem gcov

5. Zjištění živého a mrtvého kódu nástrojem gcov

6. Vygenerování čitelného protokolu s informací o živém a mrtvém kódu

7. Úprava testovacího příkladu: vložení uzlu do binárního stromu

8. Opětovné vygenerování protokolu

9. Úprava testovacího příkladu tak, aby se v runtime zavolaly všechny programové řádky

10. Vygenerování výsledného protokolu

11. Analýza, kolikrát jsou jednotlivé řádky programového kódu volány

12. Vizualizace frekvence volání jednotlivých programových řádků

13. Jak se vlastně interně zjišťuje, které řádky byly zavolány?

14. Překlad několika funkcí z C do assembleru

15. Překlad s použitím přepínačů -fprofile-arcs a -ftest-coverage

16. Podrobnější porovnání obou vygenerovaných strojových kódů

17. Nástroj gcov a Clang

18. Repositář s demonstračními příklady

19. Předchozí články s tématem testování

20. Odkazy na Internetu

1. Analýza zdrojového kódu nástrojem gcov

V dnešním článku se seznámíme s velmi užitečným a poměrně často používaným nástrojem nazvaným gcov. Tento nástroj je součástí ekosystému GCC a slouží pro zjištění, které řádky v programovém kódu jsou skutečně volány a které naopak nikoli. Navíc je u volaných řádků možné zjistit, kolikrát byly volány. K čemu se však tato informace používá? V první řadě nám umožňuje detekovat mrtvé části kódu, které se v praxi nikdy nevolají, takže je možné se zamyslet nad tím, jestli tyto části zcela neodstranit. A ve druhé řadě lze snadno zjistit, které části kódu jsou pokryty jednotkovými testy (unit tests), což je problematika, které jsme se věnovali v článcích uvedených v devatenácté kapitole.

V předchozím odstavci jsme si řekli, že nástroj gcov je součástí ekosystému GCC, což znamená, že je ho možné použít společně s překladači, jenž jsou do GCC zahrnuty. Kromě klasického překladače jazyka C se jedná o C++, ale například i o Objective-C, Objective-C++, Fortran, Adu, jazyk D a taktéž jazyk Go.

Poznámka: jak uvidíme dále, konkrétně v sedmnácté kapitole, lze gcov do určité míry využít i mimo ekosystém GCC, konkrétně společně s Clangem, jenž gcov taktéž podporuje.

2. První seznámení s nástrojem gcov

Ve skutečnosti samotný gcov nedokáže zjistit, které části kódu jsou skutečně volány. V případě gcov se jedná „pouze“ o nástroj analyzující data získaná ze dvou zdrojů a produkující analýzu v čitelném (textovém) formátu. Mezi oba zmíněné zdroje patří:

  1. První množina dat je vygenerována samotným překladačem. Jedná se o soubor (soubory) obsahující – stručně řečeno – mapování mezi řádky ve zdrojovém kódu a adresami v paměťové oblasti alokované pro běžící program. Tyto soubory jsou binární a jsou přímo určeny pro zpracování nástrojemgcov (tedy neměly by se používat jiným nástrojem, protože jejich struktura se může měnit). Jejich koncovka je typicky .gcno, kde „no“ znamená „notes“.
  2. Druhá množina dat je vygenerována v čase běhu testovaného programu. Jedná se o soubor (soubory) s čítači přístupů k jednotlivým řádkům. I tyto soubory jsou binární a mají koncovku .gcda, kde „da“ znamená „data“.

Teprve na základě obsahu těchto dvou množin dat dokáže gcov vytvořit čitelný výstup obsahující informace o tom, které řádky kódu byly volány a které nikoli. Výsledek provedené analýzy je přitom možné získat v čistě textové podobě, v podobě obarveného výstupu určeného pro terminál, nebo je možné si nechat výsledek analýzy uložit v podobě vhodné pro další strojové zpracování (například v integrovaném vývojovém prostředí atd.).

3. Zavolání nástroje gcov z příkazové řádky

Nástroj gcov se volá z příkazové řádky příkazem gcov a je mu možné předat poměrně velké množství přepínačů, z nichž některé jsou určeny pro konkrétní programovací jazyk (již tradičně je problematický C++ s jeho komolením jmen funkcí a metod – name mangling).

Všechny přepínače je možné si nechat vypsat:

$ gcov

S výsledkem:

Usage: gcov [OPTION...] SOURCE|OBJ...
 
Print code coverage information.
 
  -a, --all-blocks                Show information for every basic block
  -b, --branch-probabilities      Include branch probabilities in output
  -c, --branch-counts             Output counts of branches taken
                                    rather than percentages
  -d, --display-progress          Display progress information
  -f, --function-summaries        Output summaries for each function
  -h, --help                      Print this help, then exit
  -i, --json-format               Output JSON intermediate format into .gcov.json.gz file
  -j, --human-readable            Output human readable numbers
  -k, --use-colors                Emit colored output
  -l, --long-file-names           Use long output file names for included
                                    source files
  -m, --demangled-names           Output demangled function names
  -n, --no-output                 Do not create an output file
  -o, --object-directory DIR|FILE Search for object files in DIR or called FILE
  -p, --preserve-paths            Preserve all pathname components
  -q, --use-hotness-colors        Emit perf-like colored output for hot lines
  -r, --relative-only             Only show data for relative sources
  -s, --source-prefix DIR         Source prefix to elide
  -t, --stdout                    Output to stdout instead of a file
  -u, --unconditional-branches    Show unconditional branch counts too
  -v, --version                   Print version number, then exit
  -w, --verbose                   Print verbose informations
  -x, --hash-filenames            Hash long pathnames
 
For bug reporting instructions, please see:
<file:///usr/share/doc/gcc-9/README.Bugs>.

4. Jednoduchý program napsaný v C, který budeme zkoumat nástrojem gcov

Základní vlastnosti nástroje gcov si postupně otestujeme na několika demonstračních příkladech. První příklad, který si dnes ukážeme, je psaný v programovacím jazyku C. Najdeme v něm triviální implementaci konstrukce binárního stromu určeného pro uložení řetězců společně s funkcí pro průchod (traverzaci) tímto stromem. Ovšem průchod stromem je (alespoň prozatím) realizován nad prázdným stromem, takže již dopředu můžeme odhadnout, že zdaleka ne všechny řádky programového kódu budou v čase běhu programu (tedy v runtime) využity (zavolány):

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
typedef struct Node
{
    struct Node *left;
    struct Node *right;
    char *value;
} Node;
 
void insert_new_node(Node **root, char *value)
{
    int cmp;
 
    if (*root == NULL)
    {
        *root = (Node *)malloc(sizeof(Node));
        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
        strcpy((*root)->value, value);
        (*root)->left = NULL;
        (*root)->right = NULL;
        return;
    }
    cmp = strcmp(value, (*root)->value);
    if (cmp < 0)
    {
        insert_new_node(&(*root)->left, value);
    }
    else
    {
        insert_new_node(&(*root)->right, value);
    }
}
 
void traverse_tree(Node *root, void (*callback_function)(char *))
{
    if (root == NULL)
    {
        return;
    }
    traverse_tree(root->left, callback_function);
    callback_function(root->value);
    traverse_tree(root->right, callback_function);
}
 
void callback_function(char *value)
{
    printf("%s\n", value);
}
 
int main(void)
{
    static Node *root = NULL;
 
    traverse_tree(root, callback_function);
 
    return 0;
}
Poznámka: úplný zdrojový kód tohoto příkladu naleznete na adrese https://github.com/tisnik/sli­des/blob/master/sources/tre­e1.c.

5. Zjištění živého a mrtvého kódu nástrojem gcov

Pro zjištění, které části kódu jsou živé (volané) a které nikoli, je zapotřebí provést překlad zdrojového kódu s využitím přepínačů -fprofile-arcs a -ftest-coverage tree1.c (navíc je více než vhodné nepoužívat optimalizace):

$ gcc -fprofile-arcs -ftest-coverage tree1.c

Výsledkem překladu bude (podle očekávání) soubor a.out a navíc i soubor tree1.gcno obsahující – jak jsme si již ve stručnosti řekli – mapování mezi řádky ve zdrojovém kódu a adresami v paměťové oblasti alokované pro běžící program

$ ls -l
 
total 108
-rwxrwxr-x 1 ptisnovs ptisnovs 28544 Apr 15 12:28 a.out
-rw-r--r-- 1 ptisnovs ptisnovs  1111 Apr 13 18:50 tree1.c
-rw-rw-r-- 1 ptisnovs ptisnovs  1488 Apr 15 12:28 tree1.gcno

Nyní spustíme testovaný program naprosto stejným způsobem, jako bychom ho spouštěli bez zjišťování volaných řádků:

$ ./a.out

Po ukončení běhu programu by měl být vytvořen nový soubor tree1.gcda s čítači přístupů k jednotlivým řádkům:

$ ls -l
 
total 120
-rwxrwxr-x 1 ptisnovs ptisnovs 28544 Apr 15 12:28 a.out
-rw-r--r-- 1 ptisnovs ptisnovs  1111 Apr 13 18:50 tree1.c
-rw-rw-r-- 1 ptisnovs ptisnovs   256 Apr 15 12:29 tree1.gcda
-rw-rw-r-- 1 ptisnovs ptisnovs  1488 Apr 15 12:28 tree1.gcno

Oba dva výše zmíněné soubory, tedy tree1.gcno a tree1.gcda je nyní nutné sloučit nástrojem gcov.

6. Vygenerování čitelného protokolu s informací o živém a mrtvém kódu

Nyní nastal čas na vygenerování čitelného protokolu, z něhož zjistíme, které řádky ve vstupním zdrojovém textu jsou skutečně volány a které naopak nikoli. Tento protokol si necháme vygenerovat příkazem:

$ gcov tree1.c

Nástroj gcov v průběhu své činnosti zobrazí, které vstupní soubory se zdrojovými texty jsou zpracovávány a mj. taktéž ukáže velmi důležitou informaci – jaké procento řádků se zdrojovým kódem obsahuje živý kód. V našem konkrétním případě se reálně využila jen čtvrtina zapsaného kódu, což je z výsledku patrné:

File 'tree1.c'
Lines executed:25.00% of 24
Creating 'tree1.c.gcov'

Navíc je možné zjistit podrobnější informace o jednotlivých funkcích (metodách):

$ gcov -f tree1.c
 
Function 'main'
Lines executed:100.00% of 3
 
Function 'callback_function'
Lines executed:0.00% of 3
 
Function 'traverse_tree'
Lines executed:50.00% of 6
 
Function 'insert_new_node'
Lines executed:0.00% of 12
 
File 'tree1.c'
Lines executed:25.00% of 24
Creating 'tree1.c.gcov'

Předchozí příkaz současně vytvořil nový soubor pojmenovaný „tree1.c.gcov“:

$ ls -l
total 132
-rwxrwxr-x 1 ptisnovs ptisnovs 28544 Apr 15 12:28 a.out
-rw-r--r-- 1 ptisnovs ptisnovs  1111 Apr 13 18:50 tree1.c
-rw-rw-r-- 1 ptisnovs ptisnovs  2190 Apr 15 12:30 tree1.c.gcov
-rw-rw-r-- 1 ptisnovs ptisnovs   256 Apr 15 12:29 tree1.gcda
-rw-rw-r-- 1 ptisnovs ptisnovs  1488 Apr 15 12:28 tree1.gcno

Protokol obsahuje původní zdrojový kód doplněný o další informace. V levém sloupci je zobrazen počet volání příslušného řádku popř. řada znaků „#####“ na těch řádcích kódu, které obsahují příkazy, ale nejsou volány. Naopak ty řádky kódu, které příkazy neobsahují, začínají znakem „-“. Za dvojtečkou je uvedeno číslo řádku popř. hodnota 0 pro ty řádky protokolu, které obsahují nějaké metainformace. A konečně ve třetím sloupci za další dvojtečkou je kopie zdrojového kódu popř. metainformace:

        -:    0:Source:tree1.c
        -:    0:Graph:tree1.gcno
        -:    0:Data:tree1.gcda
        -:    0:Runs:1
        -:    1:#include <stdlib.h>
        -:    2:#include <stdio.h>
        -:    3:#include <string.h>
        -:    4:
        -:    5:typedef struct Node
        -:    6:{
        -:    7:    struct Node *left;
        -:    8:    struct Node *right;
        -:    9:    char *value;
        -:   10:} Node;
        -:   11:
    #####:   12:void insert_new_node(Node **root, char *value)
        -:   13:{
        -:   14:    int cmp;
        -:   15:
    #####:   16:    if (*root == NULL)
        -:   17:    {
    #####:   18:        *root = (Node *)malloc(sizeof(Node));
    #####:   19:        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
    #####:   20:        strcpy((*root)->value, value);
    #####:   21:        (*root)->left = NULL;
    #####:   22:        (*root)->right = NULL;
    #####:   23:        return;
        -:   24:    }
    #####:   25:    cmp = strcmp(value, (*root)->value);
    #####:   26:    if (cmp < 0)
        -:   27:    {
    #####:   28:        insert_new_node(&(*root)->left, value);
        -:   29:    }
        -:   30:    else
        -:   31:    {
    #####:   32:        insert_new_node(&(*root)->right, value);
        -:   33:    }
        -:   34:}
        -:   35:
        1:   36:void traverse_tree(Node *root, void (*callback_function)(char *))
        -:   37:{
        1:   38:    if (root == NULL)
        -:   39:    {
        1:   40:        return;
        -:   41:    }
    #####:   42:    traverse_tree(root->left, callback_function);
    #####:   43:    callback_function(root->value);
    #####:   44:    traverse_tree(root->right, callback_function);
        -:   45:}
        -:   46:
    #####:   47:void callback_function(char *value)
        -:   48:{
    #####:   49:    printf("%s\n", value);
    #####:   50:}
        -:   51:
        1:   52:int main(void)
        -:   53:{
        -:   54:    static Node *root = NULL;
        -:   55:
        1:   56:    traverse_tree(root, callback_function);
        -:   57:
        1:   58:    return 0;
        -:   59:}
        -:   60:

Alternativně je možné nechat si vygenerovat výstupní soubor, který bude obsahovat původní zdrojový text, ovšem obarvený podle toho, které řádky (příkazy) byly zavolány a které nikoli. V takovém souboru nalezneme řídicí kódy dle ANSI, které by mělo být možné interpretovat na prakticky jakémkoli emulátoru terminálu:

$ gcov -k tree1.c

Výsledný soubor tree1.c.gcov si můžeme zobrazit na terminálu například standardním nástrojem cat:

$ cat tree1.c.gcov

S následujícím výsledkem:

Obrázek 1: Obarvený výstup z nástroje gcov.

7. Úprava testovacího příkladu: vložení uzlu do binárního stromu

Nyní nastal čas na úpravu původního testovacího příkladu takovým způsobem, aby se využila (resp. přesněji řečeno zavolala) větší část jeho kódu. Zejména do binárního stromu vložíme uzel, což (nepřímo) ovlivní i funkci pro procházení (traverzaci) binárním stromem. Upravený příklad vypadá následovně:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
typedef struct Node
{
    struct Node *left;
    struct Node *right;
    char *value;
} Node;
 
void insert_new_node(Node **root, char *value)
{
    int cmp;
 
    if (*root == NULL)
    {
        *root = (Node *)malloc(sizeof(Node));
        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
        strcpy((*root)->value, value);
        (*root)->left = NULL;
        (*root)->right = NULL;
        return;
    }
    cmp = strcmp(value, (*root)->value);
    if (cmp < 0)
    {
        insert_new_node(&(*root)->left, value);
    }
    else
    {
        insert_new_node(&(*root)->right, value);
    }
}
 
void traverse_tree(Node *root, void (*callback_function)(char *))
{
    if (root == NULL)
    {
        return;
    }
    traverse_tree(root->left, callback_function);
    callback_function(root->value);
    traverse_tree(root->right, callback_function);
}
 
void callback_function(char *value)
{
    printf("%s\n", value);
}
 
int main(void)
{
    static Node *root = NULL;
 
    insert_new_node(&root, "xxx");
 
    traverse_tree(root, callback_function);
 
    return 0;
}
Poznámka: povšimněte si, že jediný nový řádek vložený do funkce main může zásadním způsobem ovlivnit „živost“ velké části programového kódu.

8. Opětovné vygenerování protokolu

Opět si můžeme nechat vygenerovat protokol s analýzou zdrojového kódu:

$ gcov tree2.c

Průběh analýzy:

File 'tree2.c'
Lines executed:84.00% of 25
Creating 'tree2.c.gcov'

Hodnota s poměrnou „živostí“ kódu je nyní vyšší, takže se podívejme na podrobnější informace rozdělené po jednotlivých funkcích:

$ gcov -f tree2.c

S výsledkem:

Function 'main'
Lines executed:100.00% of 4
 
Function 'callback_function'
Lines executed:100.00% of 3
 
Function 'traverse_tree'
Lines executed:100.00% of 6
 
Function 'insert_new_node'
Lines executed:66.67% of 12
 
File 'tree2.c'
Lines executed:84.00% of 25
Creating 'tree2.c.gcov'

Vlastní protokol se změnil, protože ubyly programové řádky označené symbolem „#####“ a naopak je patrné, že některé řádky (resp. přesněji řečeno příkazy na těchto řádcích) jsou zavolány vícekrát, což se týká například rekurzivně volané funkce traverse_tree, jenž je přímo zavolána jednou a dvakrát zavolána v rekurzi:

        -:    0:Source:tree2.c
        -:    0:Graph:tree2.gcno
        -:    0:Data:tree2.gcda
        -:    0:Runs:1
        -:    1:#include <stdlib.h>
        -:    2:#include <stdio.h>
        -:    3:#include <string.h>
        -:    4:
        -:    5:typedef struct Node
        -:    6:{
        -:    7:    struct Node *left;
        -:    8:    struct Node *right;
        -:    9:    char *value;
        -:   10:} Node;
        -:   11:
        1:   12:void insert_new_node(Node **root, char *value)
        -:   13:{
        -:   14:    int cmp;
        -:   15:
        1:   16:    if (*root == NULL)
        -:   17:    {
        1:   18:        *root = (Node *)malloc(sizeof(Node));
        1:   19:        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
        1:   20:        strcpy((*root)->value, value);
        1:   21:        (*root)->left = NULL;
        1:   22:        (*root)->right = NULL;
        1:   23:        return;
        -:   24:    }
    #####:   25:    cmp = strcmp(value, (*root)->value);
    #####:   26:    if (cmp < 0)
        -:   27:    {
    #####:   28:        insert_new_node(&(*root)->left, value);
        -:   29:    }
        -:   30:    else
        -:   31:    {
    #####:   32:        insert_new_node(&(*root)->right, value);
        -:   33:    }
        -:   34:}
        -:   35:
        3:   36:void traverse_tree(Node *root, void (*callback_function)(char *))
        -:   37:{
        3:   38:    if (root == NULL)
        -:   39:    {
        2:   40:        return;
        -:   41:    }
        1:   42:    traverse_tree(root->left, callback_function);
        1:   43:    callback_function(root->value);
        1:   44:    traverse_tree(root->right, callback_function);
        -:   45:}
        -:   46:
        1:   47:void callback_function(char *value)
        -:   48:{
        1:   49:    printf("%s\n", value);
        1:   50:}
        -:   51:
        1:   52:int main(void)
        -:   53:{
        -:   54:    static Node *root = NULL;
        -:   55:
        1:   56:    insert_new_node(&root, "xxx");
        -:   57:
        1:   58:    traverse_tree(root, callback_function);
        -:   59:
        1:   60:    return 0;
        -:   61:}
        -:   62:

Obrázek 2: Obarvený výstup z nástroje gcov.

9. Úprava testovacího příkladu tak, aby se v runtime zavolaly všechny programové řádky

Další, již poslední úprava testovacího příkladu s implementací naivního binárního stromu spočívá v tom, že do stromu vložíme větší množství uzlů a to navíc v takovém pořadí, aby byly nové poduzly vytvořeny jak napravo od nadřazeného uzlu, tak i nalevo od něj. To vede ke konstrukci binárního stromu, při jehož průchodu se využijí všechny implementované možnosti:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
typedef struct Node
{
    struct Node *left;
    struct Node *right;
    char *value;
} Node;
 
void insert_new_node(Node **root, char *value)
{
    int cmp;
 
    if (*root == NULL)
    {
        *root = (Node *)malloc(sizeof(Node));
        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
        strcpy((*root)->value, value);
        (*root)->left = NULL;
        (*root)->right = NULL;
        return;
    }
    cmp = strcmp(value, (*root)->value);
    if (cmp < 0)
    {
        insert_new_node(&(*root)->left, value);
    }
    else
    {
        insert_new_node(&(*root)->right, value);
    }
}
 
void traverse_tree(Node *root, void (*callback_function)(char *))
{
    if (root == NULL)
    {
        return;
    }
    traverse_tree(root->left, callback_function);
    callback_function(root->value);
    traverse_tree(root->right, callback_function);
}
 
void callback_function(char *value)
{
    printf("%s\n", value);
}
 
int main(void)
{
    static Node *root = NULL;
 
    insert_new_node(&root, "xxx");
    insert_new_node(&root, "aaa");
    insert_new_node(&root, "bbb");
    insert_new_node(&root, "ccc");
    insert_new_node(&root, "yyy");
    insert_new_node(&root, "yyy");
 
    traverse_tree(root, callback_function);
 
    return 0;
}

10. Vygenerování výsledného protokolu

Vygenerování výsledného protokolu provedeme nám již známým postupem, a to buď bez zobrazení využití jednotlivých funkcí, nebo s podrobnějším výpisem:

$ gcov tree3.c

Průběh analýzy:

File 'tree3.c'
Lines executed:100.00% of 30
Creating 'tree3.c.gcov'

Analýza naznačila, že jsou pokryty všechny řádky, což si ještě ověříme tak, že zjistíme pokrytí jednotlivých funkcí:

$ gcov -f tree3.c
 
Function 'main'
Lines executed:100.00% of 9
 
Function 'callback_function'
Lines executed:100.00% of 3
 
Function 'traverse_tree'
Lines executed:100.00% of 6
 
Function 'insert_new_node'
Lines executed:100.00% of 12
 
File 'tree3.c'
Lines executed:100.00% of 30
Creating 'tree3.c.gcov'

A takto bude vypadat výsledek analýzy:

        -:    0:Source:tree3.c
        -:    0:Graph:tree3.gcno
        -:    0:Data:tree3.gcda
        -:    0:Runs:1
        -:    1:#include <stdlib.h>
        -:    2:#include <stdio.h>
        -:    3:#include <string.h>
        -:    4:
        -:    5:typedef struct Node
        -:    6:{
        -:    7:    struct Node *left;
        -:    8:    struct Node *right;
        -:    9:    char *value;
        -:   10:} Node;
        -:   11:
       15:   12:void insert_new_node(Node **root, char *value)
        -:   13:{
        -:   14:    int cmp;
        -:   15:
       15:   16:    if (*root == NULL)
        -:   17:    {
        6:   18:        *root = (Node *)malloc(sizeof(Node));
        6:   19:        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
        6:   20:        strcpy((*root)->value, value);
        6:   21:        (*root)->left = NULL;
        6:   22:        (*root)->right = NULL;
        6:   23:        return;
        -:   24:    }
        9:   25:    cmp = strcmp(value, (*root)->value);
        9:   26:    if (cmp < 0)
        -:   27:    {
        3:   28:        insert_new_node(&(*root)->left, value);
        -:   29:    }
        -:   30:    else
        -:   31:    {
        6:   32:        insert_new_node(&(*root)->right, value);
        -:   33:    }
        -:   34:}
        -:   35:
       13:   36:void traverse_tree(Node *root, void (*callback_function)(char *))
        -:   37:{
       13:   38:    if (root == NULL)
        -:   39:    {
        7:   40:        return;
        -:   41:    }
        6:   42:    traverse_tree(root->left, callback_function);
        6:   43:    callback_function(root->value);
        6:   44:    traverse_tree(root->right, callback_function);
        -:   45:}
        -:   46:
        6:   47:void callback_function(char *value)
        -:   48:{
        6:   49:    printf("%s\n", value);
        6:   50:}
        -:   51:
        1:   52:int main(void)
        -:   53:{
        -:   54:    static Node *root = NULL;
        -:   55:
        1:   56:    insert_new_node(&root, "xxx");
        1:   57:    insert_new_node(&root, "aaa");
        1:   58:    insert_new_node(&root, "bbb");
        1:   59:    insert_new_node(&root, "ccc");
        1:   60:    insert_new_node(&root, "yyy");
        1:   61:    insert_new_node(&root, "yyy");
        -:   62:
        1:   63:    traverse_tree(root, callback_function);
        -:   64:
        1:   65:    return 0;
        -:   66:}
        -:   67:

11. Analýza, kolikrát jsou jednotlivé řádky programového kódu volány

Mnohdy je zapotřebí zjistit nejenom to, které části kódu byly volány a které nikoli, ale i informaci o tom, kolikrát byla funkce, blok či jednotlivý programový řádek volán. To je sice primárně úloha jiných nástrojů (například nástroje gprof, pokud zůstaneme v ekosystému GCC), ale základní a především velmi přehledně podané informace nám dá i nástroj gcov (jenž ovšem nezjistí časy vykonání ani kumulativní časy!). Vše si otestujeme na tomto jednoduchém a naivním rekurzivním výpočtu faktoriálu:

#include <stdio.h>
 
long factorial(long n)
{
    if (n<=1) {
        return 1;
    }
    return n*factorial(n-1);
}
 
int main(int argc, char **argv)
{
    printf("%ld\n", factorial(20));
    return 0;
}

12. Vizualizace frekvence volání jednotlivých programových řádků

Po spuštění výpočtu faktoriálu si opět můžeme nechat vygenerovat protokol s informacemi o řádcích kódu, které byly volány a které nikoli. U volaných řádků se podle očekávání vypíše i celkový počet volání:

$ gcov factorial.c
 
File 'factorial.c'
Lines executed:100.00% of 7
Creating 'factorial.c.gcov'

Výsledek bude vypadat následovně:

        -:    0:Source:factorial.c
        -:    0:Graph:factorial.gcno
        -:    0:Data:factorial.gcda
        -:    0:Runs:1
        -:    1:#include <stdio.h>
        -:    2:
       20:    3:long factorial(long n)
        -:    4:{
       20:    5:    if (n<=1) {
        1:    6:        return 1;
        -:    7:    }
       19:    8:    return n*factorial(n-1);
        -:    9:}
        -:   10:
        1:   11:int main(int argc, char **argv)
        -:   12:{
        1:   13:    printf("%ld\n", factorial(20));
        1:   14:    return 0;
        -:   15:}
        -:   16:

Kromě tohoto výstupu však můžeme použít i přepínač -q, jenž zajistí určitou formu vizualizace – volané řádky jsou obarveny jednou ze tří barev; viz zobrazená legenda v pravém horním rohu:

$ gcov -q factorial.c
 
File 'factorial.c'
Lines executed:100.00% of 7
Creating 'factorial.c.gcov'

Tento výstup je možné v případě potřeby kombinovat s „barvovým“ výstupem, jenž již známe – s přepínačem -k:

$ gcov -k -q factorial.c
 
File 'factorial.c'
Lines executed:100.00% of 7
Creating 'factorial.c.gcov'

Výsledek může vypadat následovně:

Obrázek 3: Vizuální zvýraznění programových řádků podle frekvence jejich volání (získáno pro jinou vstupní hodnotu pro výpočet faktoriálu). Nyní jsou červeně označené řádky označují kód, který tvoří více než polovinu všech volání za celou dobu běhu aplikace (a profiler by nám navíc zobrazil, jak dlouhou dobu program strávil v těchto blocích).

13. Jak se vlastně interně zjišťuje, které řádky byly zavolány?

Na tomto místě se pravděpodobně čtenář může ptát, jakým způsobem se vlastně vytváří soubory .gcda. Připomeňme si, že se jedná o soubory obsahující informace o „zásazích“ řádků zdrojového kódu, které jsou získány v době běhu testovaného programu, tedy v runtime. Co to ale ve skutečnosti znamená? Překladač musel do výsledného strojového kódu vložit další instrukce, které nějakým způsobem zajišťují získání informací o oněch „zásazích“. V navazujících kapitolách si ukážeme, jak tyto instrukce reálně vypadají, resp. přesněji řečeno jak se liší původní strojový kód od strojového kódu, který zajišťuje vygenerování souborů .gcda.

Poznámka: opět platí, že se bude provádět překlad bez zapnutí optimalizací, protože optimalizující překladač dokáže detekovat a zcela odstranit některé části kódu popř. naopak některé části sloučit, což ovlivňuje výsledky měření v runtime.

14. Překlad několika funkcí z C do assembleru

Nejprve si ukažme zdrojový kód napsaný v jazyku C, který obsahuje deklaraci několika jednoduchých funkcí. Tyto funkce akceptují různý počet parametrů a všechny jsou volány z funkce main, čímž je zajištěno, že je překladač neodstraní jako nepoužitý kód:

int zero(void)
{
    return 0;
}
 
int inc(int x)
{
    return x+1;
}
 
int add(int x, int y)
{
    return x+y;
}
 
int foo(int x)
{
    int r = 0;
    while (x--) {
        r++;
    }
    return r;
}
 
int main(void)
{
    int a = zero();
    int b = inc(a);
    int c = add(a, b);
    int d = foo(c);
    return c;
}

Překlad tohoto programu provedeme s přepínačem -g, kterým je zajištěno vložení ladicích informací do výsledného binárního spustitelného kódu:

gcc -g test.c

Nyní zjistíme, jakým způsobem byly výše zmíněné funkce přeloženy do assembleru (a dále do strojového kódu). Pro zpětný překlad přitom využijeme nástroj objdump, kterému jsme se již na stránkách Roota věnovali:

objdump -drw -Mintel -S a.out > normal.asm
0000000000001129 <zero>:
int zero(void)
{
    1129:       f3 0f 1e fa             endbr64
    112d:       55                      push   rbp
    112e:       48 89 e5                mov    rbp,rsp
    return 0;
    1131:       b8 00 00 00 00          mov    eax,0x0
}
    1136:       5d                      pop    rbp
    1137:       c3                      ret
 
0000000000001138 <inc>:
 
int inc(int x)
{
    1138:       f3 0f 1e fa             endbr64
    113c:       55                      push   rbp
    113d:       48 89 e5                mov    rbp,rsp
    1140:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
    return x+1;
    1143:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
    1146:       83 c0 01                add    eax,0x1
}
    1149:       5d                      pop    rbp
    114a:       c3                      ret
 
000000000000114b <add>:
 
int add(int x, int y)
{
    114b:       f3 0f 1e fa             endbr64
    114f:       55                      push   rbp
    1150:       48 89 e5                mov    rbp,rsp
    1153:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
    1156:       89 75 f8                mov    DWORD PTR [rbp-0x8],esi
    return x+y;
    1159:       8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
    115c:       8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
    115f:       01 d0                   add    eax,edx
}
    1161:       5d                      pop    rbp
    1162:       c3                      ret
 
0000000000001163 <foo>:
 
int foo(int x)
{
    1163:       f3 0f 1e fa             endbr64
    1167:       55                      push   rbp
    1168:       48 89 e5                mov    rbp,rsp
    116b:       89 7d ec                mov    DWORD PTR [rbp-0x14],edi
    int r = 0;
    116e:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
    while (x--) {
    1175:       eb 04                   jmp    117b <foo+0x18>
        r++;
    1177:       83 45 fc 01             add    DWORD PTR [rbp-0x4],0x1
    while (x--) {
    117b:       8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
    117e:       8d 50 ff                lea    edx,[rax-0x1]
    1181:       89 55 ec                mov    DWORD PTR [rbp-0x14],edx
    1184:       85 c0                   test   eax,eax
    1186:       75 ef                   jne    1177 <foo+0x14>
    }
    return r;
    1188:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
}
    118b:       5d                      pop    rbp
    118c:       c3                      ret

15. Překlad s použitím přepínačů -fprofile-arcs a -ftest-coverage

Ve druhém kroku přeložíme stejný zdrojový kód, ovšem nyní při překladu použijeme i přepínače -fprofile-arcs a -ftest-coverage:

gcc -g -fprofile-arcs -ftest-coverage test.c

Opět se podíváme na způsob překladu všech výše uvedených funkcí do assembleru a strojového kódu:

objdump -drw -Mintel -S a.out > coverage.asm

Tentokrát ovšem bude sekvence instrukcí odlišná. Přidané instrukce jsou pro větší přehlednost zvýrazněny:

00000000000014b9 <zero>:
int zero(void)
{
    14b9:       f3 0f 1e fa             endbr64
    14bd:       55                      push   rbp
    14be:       48 89 e5                mov    rbp,rsp
    14c1:       48 8b 05 e8 3d 00 00    mov    rax,QWORD PTR [rip+0x3de8]        # 52b0 <__gcov0.zero>
    14c8:       48 83 c0 01             add    rax,0x1
    14cc:       48 89 05 dd 3d 00 00    mov    QWORD PTR [rip+0x3ddd],rax        # 52b0 <__gcov0.zero>
    return 0;
    14d3:       b8 00 00 00 00          mov    eax,0x0
}
    14d8:       5d                      pop    rbp
    14d9:       c3                      ret
 
00000000000014da <inc>:
 
int inc(int x)
{
    14da:       f3 0f 1e fa             endbr64
    14de:       55                      push   rbp
    14df:       48 89 e5                mov    rbp,rsp
    14e2:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
    14e5:       48 8b 05 bc 3d 00 00    mov    rax,QWORD PTR [rip+0x3dbc]        # 52a8 <__gcov0.inc>
    14ec:       48 83 c0 01             add    rax,0x1
    14f0:       48 89 05 b1 3d 00 00    mov    QWORD PTR [rip+0x3db1],rax        # 52a8 <__gcov0.inc>
    return x+1;
    14f7:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
    14fa:       83 c0 01                add    eax,0x1
}
    14fd:       5d                      pop    rbp
    14fe:       c3                      ret
 
00000000000014ff <add>:
 
int add(int x, int y)
{
    14ff:       f3 0f 1e fa             endbr64
    1503:       55                      push   rbp
    1504:       48 89 e5                mov    rbp,rsp
    1507:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi
    150a:       89 75 f8                mov    DWORD PTR [rbp-0x8],esi
    150d:       48 8b 05 8c 3d 00 00    mov    rax,QWORD PTR [rip+0x3d8c]        # 52a0 <__gcov0.add>
    1514:       48 83 c0 01             add    rax,0x1
    1518:       48 89 05 81 3d 00 00    mov    QWORD PTR [rip+0x3d81],rax        # 52a0 <__gcov0.add>
    return x+y;
    151f:       8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
    1522:       8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
    1525:       01 d0                   add    eax,edx
}
    1527:       5d                      pop    rbp
    1528:       c3                      ret
 
0000000000001529 <foo>:
 
int foo(int x)
{
    1529:       f3 0f 1e fa             endbr64
    152d:       55                      push   rbp
    152e:       48 89 e5                mov    rbp,rsp
    1531:       89 7d ec                mov    DWORD PTR [rbp-0x14],edi
    1534:       48 8b 05 55 3d 00 00    mov    rax,QWORD PTR [rip+0x3d55]        # 5290 <__gcov0.foo>
    153b:       48 83 c0 01             add    rax,0x1
    153f:       48 89 05 4a 3d 00 00    mov    QWORD PTR [rip+0x3d4a],rax        # 5290 <__gcov0.foo>
    int r = 0;
    1546:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
    while (x--) {
    154d:       eb 16                   jmp    1565 <foo+0x3c>
        r++;
    154f:       83 45 fc 01             add    DWORD PTR [rbp-0x4],0x1
    1553:       48 8b 05 3e 3d 00 00    mov    rax,QWORD PTR [rip+0x3d3e]        # 5298 <__gcov0.foo+0x8>
    155a:       48 83 c0 01             add    rax,0x1
    155e:       48 89 05 33 3d 00 00    mov    QWORD PTR [rip+0x3d33],rax        # 5298 <__gcov0.foo+0x8>
    while (x--) {
    1565:       8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
    1568:       8d 50 ff                lea    edx,[rax-0x1]
    156b:       89 55 ec                mov    DWORD PTR [rbp-0x14],edx
    156e:       85 c0                   test   eax,eax
    1570:       75 dd                   jne    154f <foo+0x26>
    }
    return r;
    1572:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
}
    1575:       5d                      pop    rbp
    1576:       c3                      ret

Ze zvýrazněných částí kódů je patrné, že byly přidány instrukce určené pro zvýšení obsahu čítačů uložených v operační paměti. A tyto instrukce nejsou přidány pouze jedenkrát do každé funkce, ale navíc i do každého bloku, tedy i do těla programové smyčky. Pro krátké funkce a bloky tedy kód může podstatným způsobem narůst.

Poznámka: povšimněte si, že na této úrovni není řešeno zamykání čítačů, takže sice nedojde k výraznému zpomalení běhu programu, na druhou stranu však nemusí být výsledky přesné ve chvíli, kdy proces běží v několika vláknech.

16. Podrobnější porovnání obou vygenerovaných strojových kódů

Abychom mohli obě varianty strojových kódů vzniklých kompilací a slinkováním porovnat, odstraníme všechny jeho „pohyblivé“ části, zejména tedy absolutní adresy jednotlivých instrukcí. Kód bez instrukcí přidaných pro gcov bude po ruční úpravě vypadat takto:

int zero(void)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    return 0;
    b8 00 00 00 00              mov    eax,0x0
}
    5d                          pop    rbp
    c3                          ret
 
 
int inc(int x)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    89 7d fc                    mov    DWORD PTR [rbp-0x4],edi
    return x+1;
    8b 45 fc                    mov    eax,DWORD PTR [rbp-0x4]
    83 c0 01                    add    eax,0x1
}
    5d                          pop    rbp
    c3                          ret
 
 
int add(int x, int y)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    89 7d fc                    mov    DWORD PTR [rbp-0x4],edi
    89 75 f8                    mov    DWORD PTR [rbp-0x8],esi
    return x+y;
    8b 55 fc                    mov    edx,DWORD PTR [rbp-0x4]
    8b 45 f8                    mov    eax,DWORD PTR [rbp-0x8]
    01 d0                       add    eax,edx
}
    5d                          pop    rbp
    c3                          ret
 
 
int foo(int x)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    89 7d ec                    mov    DWORD PTR [rbp-0x14],edi
    int r = 0;
    c7 45 fc 00 00 00 00        mov    DWORD PTR [rbp-0x4],0x0
    while (x--) {
    eb 04                       jmp    117b <foo+0x18>
        r++;
    83 45 fc 01                 add    DWORD PTR [rbp-0x4],0x1
    while (x--) {
    8b 45 ec                    mov    eax,DWORD PTR [rbp-0x14]
    8d 50 ff                    lea    edx,[rax-0x1]
    89 55 ec                    mov    DWORD PTR [rbp-0x14],edx
    85 c0                       test   eax,eax
    75 ef                       jne    1177 <foo+0x14>
    }
    return r;
    8b 45 fc                    mov    eax,DWORD PTR [rbp-0x4]
}
    5d                          pop    rbp
    c3                          ret

Kód s instrukcemi přidanými pro účely nástroje gcov:

int zero(void)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    48 8b 05 e8 3d 00 00        mov    rax,QWORD PTR [rip+0x3de8]        # 52b0 <__gcov0.zero>
    48 83 c0 01                 add    rax,0x1
    48 89 05 dd 3d 00 00        mov    QWORD PTR [rip+0x3ddd],rax        # 52b0 <__gcov0.zero>
    return 0;
    b8 00 00 00 00              mov    eax,0x0
}
    5d                          pop    rbp
    c3                          ret
 
 
int inc(int x)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    89 7d fc                    mov    DWORD PTR [rbp-0x4],edi
    48 8b 05 bc 3d 00 00        mov    rax,QWORD PTR [rip+0x3dbc]        # 52a8 <__gcov0.inc>
    48 83 c0 01                 add    rax,0x1
    48 89 05 b1 3d 00 00        mov    QWORD PTR [rip+0x3db1],rax        # 52a8 <__gcov0.inc>
    return x+1;
    8b 45 fc                    mov    eax,DWORD PTR [rbp-0x4]
    83 c0 01                    add    eax,0x1
}
    5d                          pop    rbp
    c3                          ret
 
 
int add(int x, int y)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    89 7d fc                    mov    DWORD PTR [rbp-0x4],edi
    89 75 f8                    mov    DWORD PTR [rbp-0x8],esi
    48 8b 05 8c 3d 00 00        mov    rax,QWORD PTR [rip+0x3d8c]        # 52a0 <__gcov0.add>
    48 83 c0 01                 add    rax,0x1
    48 89 05 81 3d 00 00        mov    QWORD PTR [rip+0x3d81],rax        # 52a0 <__gcov0.add>
    return x+y;
    8b 55 fc                    mov    edx,DWORD PTR [rbp-0x4]
    8b 45 f8                    mov    eax,DWORD PTR [rbp-0x8]
    01 d0                       add    eax,edx
}
    5d                          pop    rbp
    c3                          ret
 
 
int foo(int x)
{
    f3 0f 1e fa                 endbr64
    55                          push   rbp
    48 89 e5                    mov    rbp,rsp
    89 7d ec                    mov    DWORD PTR [rbp-0x14],edi
    48 8b 05 55 3d 00 00        mov    rax,QWORD PTR [rip+0x3d55]        # 5290 <__gcov0.foo>
    48 83 c0 01                 add    rax,0x1
    48 89 05 4a 3d 00 00        mov    QWORD PTR [rip+0x3d4a],rax        # 5290 <__gcov0.foo>
    int r = 0;
    c7 45 fc 00 00 00 00        mov    DWORD PTR [rbp-0x4],0x0
    while (x--) {
    eb 16                       jmp    1565 <foo+0x3c>
        r++;
    83 45 fc 01                 add    DWORD PTR [rbp-0x4],0x1
    48 8b 05 3e 3d 00 00        mov    rax,QWORD PTR [rip+0x3d3e]        # 5298 <__gcov0.foo+0x8>
    48 83 c0 01                 add    rax,0x1
    48 89 05 33 3d 00 00        mov    QWORD PTR [rip+0x3d33],rax        # 5298 <__gcov0.foo+0x8>
    while (x--) {
    8b 45 ec                    mov    eax,DWORD PTR [rbp-0x14]
    8d 50 ff                    lea    edx,[rax-0x1]
    89 55 ec                    mov    DWORD PTR [rbp-0x14],edx
    85 c0                       test   eax,eax
    75 dd                       jne    154f <foo+0x26>
    }
    return r;
    8b 45 fc                    mov    eax,DWORD PTR [rbp-0x4]
}
    5d                          pop    rbp
    c3                          ret

Textové porovnání jasně ukazuje, že samotný strojový kód zůstal nezměněn, pouze do něj byly (mechanicky) vloženy úpravy čítačů, jejichž hodnoty jsou na konci uloženy do souboru .gcda (a tím došlo k posunutí adres instrukcí, teoreticky se rozšíří šířka relativních skoků atd.):

--- normal.asm  2023-04-14 14:33:34.712189422 +0200
+++ coverage.asm        2023-04-14 14:33:45.540229057 +0200
@@ -3,6 +3,9 @@
     f3 0f 1e fa                endbr64
     55                         push   rbp
     48 89 e5                   mov    rbp,rsp
+    48 8b 05 e8 3d 00 00       mov    rax,QWORD PTR [rip+0x3de8]        # 52b0 <__gcov0.zero>
+    48 83 c0 01                add    rax,0x1
+    48 89 05 dd 3d 00 00       mov    QWORD PTR [rip+0x3ddd],rax        # 52b0 <__gcov0.zero>
     return 0;
     b8 00 00 00 00             mov    eax,0x0
 }
@@ -16,6 +19,9 @@
     55                         push   rbp
     48 89 e5                   mov    rbp,rsp
     89 7d fc                   mov    DWORD PTR [rbp-0x4],edi
+    48 8b 05 bc 3d 00 00       mov    rax,QWORD PTR [rip+0x3dbc]        # 52a8 <__gcov0.inc>
+    48 83 c0 01                add    rax,0x1
+    48 89 05 b1 3d 00 00       mov    QWORD PTR [rip+0x3db1],rax        # 52a8 <__gcov0.inc>
     return x+1;
     8b 45 fc                   mov    eax,DWORD PTR [rbp-0x4]
     83 c0 01                   add    eax,0x1
@@ -31,6 +37,9 @@
     48 89 e5                   mov    rbp,rsp
     89 7d fc                   mov    DWORD PTR [rbp-0x4],edi
     89 75 f8                   mov    DWORD PTR [rbp-0x8],esi
+    48 8b 05 8c 3d 00 00       mov    rax,QWORD PTR [rip+0x3d8c]        # 52a0 <__gcov0.add>
+    48 83 c0 01                add    rax,0x1
+    48 89 05 81 3d 00 00       mov    QWORD PTR [rip+0x3d81],rax        # 52a0 <__gcov0.add>
     return x+y;
     8b 55 fc                   mov    edx,DWORD PTR [rbp-0x4]
     8b 45 f8                   mov    eax,DWORD PTR [rbp-0x8]
@@ -46,18 +55,24 @@
     55                         push   rbp
     48 89 e5                   mov    rbp,rsp
     89 7d ec                   mov    DWORD PTR [rbp-0x14],edi
+    48 8b 05 55 3d 00 00       mov    rax,QWORD PTR [rip+0x3d55]        # 5290 <__gcov0.foo>
+    48 83 c0 01                add    rax,0x1
+    48 89 05 4a 3d 00 00       mov    QWORD PTR [rip+0x3d4a],rax        # 5290 <__gcov0.foo>
     int r = 0;
     c7 45 fc 00 00 00 00       mov    DWORD PTR [rbp-0x4],0x0
     while (x--) {
-    eb 04                      jmp    117b <foo+0x18>
+    eb 16                      jmp    1565 <foo+0x3c>
         r++;
     83 45 fc 01                add    DWORD PTR [rbp-0x4],0x1
+    48 8b 05 3e 3d 00 00       mov    rax,QWORD PTR [rip+0x3d3e]        # 5298 <__gcov0.foo+0x8>
+    48 83 c0 01                add    rax,0x1
+    48 89 05 33 3d 00 00       mov    QWORD PTR [rip+0x3d33],rax        # 5298 <__gcov0.foo+0x8>
     while (x--) {
     8b 45 ec                   mov    eax,DWORD PTR [rbp-0x14]
     8d 50 ff                   lea    edx,[rax-0x1]
     89 55 ec                   mov    DWORD PTR [rbp-0x14],edx
     85 c0                      test   eax,eax
-    75 ef                      jne    1177 <foo+0x14>
+    75 dd                      jne    154f <foo+0x26>
     }
     return r;
     8b 45 fc                   mov    eax,DWORD PTR [rbp-0x4]

Na rozdíl se můžeme podívat i ve vizuální podobě:

Obrázek 4: Rozdíl v obou strojových kódech vzniklých překladem.

17. Nástroj gcov a Clang

Nástroj gcov je sice primárně určen pro ekosystém GCC, ale lze ho využít i v ekosystému LLVM, konkrétně s Clangem. Pro překlad programu do podoby, která po svém spuštění vytvoří výstup kompatibilní s gcov se používají přepínače -ftest-coverage popř. –coverage. Ovšem nativně Clang (a vlastně i celé LLVM) namísto nástroje gcov používá SanitizerCoverage (viz též https://clang.llvm.org/doc­s/SanitizerCoverage.html). Příslušný přepínač předávaný překladači vypadá takto: -fsanitize-coverage=trace-pc-guard.

bitcoin_skoleni

18. Repositář s demonstračními příklady

Demonstrační soubory použité v dnešním článku byly uloženy do Git repositáře, jenž je dostupný na adrese https://github.com/tisnik/slides/. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé zdrojové soubory, které naleznete v následující tabulce:

# Soubor Stručný popis Adresa
1 tree1.c průchod prázdným binárním stromem bez jeho konstrukce https://github.com/tisnik/sli­des/blob/master/sources/tre­e1.c
2 tree2.c konstrukce binárního stromu s jediným uzlem; průchod tímto stromem https://github.com/tisnik/sli­des/blob/master/sources/tre­e2.c
3 tree3.c konstrukce binárního stromu s více uzly; průchod tímto stromem https://github.com/tisnik/sli­des/blob/master/sources/tre­e3.c
4 factorial.c naivní rekurzivní výpočet faktoriálu https://github.com/tisnik/sli­des/blob/master/sources/fac­torial.c
5 test.c několik funkcí s různým počtem parametrů, které jsou volány z main https://github.com/tisnik/sli­des/blob/master/sources/tes­t.c

19. Předchozí články s tématem testování

Tématem testování jsme se již na stránkách Rootu několikrát zabývali. Jedná se mj. o následující články:

  1. Použití Pythonu pro tvorbu testů: od jednotkových testů až po testy UI
    https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-od-jednotkovych-testu-az-po-testy-ui/
  2. Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
    https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-pouziti-tridy-mock-z-knihovny-unittest-mock/
  3. Použití nástroje pytest pro tvorbu jednotkových testů a benchmarků
    https://www.root.cz/clanky/pouziti-nastroje-pytest-pro-tvorbu-jednotkovych-testu-a-benchmarku/
  4. Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů
    https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-fixtures-vyjimky-parametrizace-testu/
  5. Nástroj pytest a jednotkové testy: životní cyklus testů, užitečné tipy a triky
    https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-zivotni-cyklus-testu-uzitecne-tipy-a-triky/
  6. Struktura projektů s jednotkovými testy, využití Travis CI
    https://www.root.cz/clanky/struktura-projektu-s-jednotkovymi-testy-vyuziti-travis-ci/
  7. Omezení stavového prostoru testovaných funkcí a metod
    https://www.root.cz/clanky/omezeni-stavoveho-prostoru-testovanych-funkci-a-metod/
  8. Testování aplikací s využitím nástroje Hypothesis
    https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis/
  9. Testování aplikací s využitím nástroje Hypothesis (dokončení)
    https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis-dokonceni/
  10. Testování webových aplikací s REST API z Pythonu
    https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu/
  11. Testování webových aplikací s REST API z Pythonu (2)
    https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu-2/
  12. Testování webových aplikací s REST API z Pythonu (3)
    https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu-3/
  13. Behavior-driven development v Pythonu s využitím knihovny Behave
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave/
  14. Behavior-driven development v Pythonu s využitím knihovny Behave (druhá část)
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-druha-cast/
  15. Behavior-driven development v Pythonu s využitím knihovny Behave (závěrečná část)
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-zaverecna-cast/
  16. Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-pomoci-knihoven-schemagic-a-schema/
  17. Validace datových struktur v Pythonu (2. část)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/
  18. Validace datových struktur v Pythonu (dokončení)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/
  19. Univerzální testovací nástroj Robot Framework
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/
  20. Univerzální testovací nástroj Robot Framework a BDD testy
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/
  21. Úvod do problematiky fuzzingu a fuzz testování
    https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/
  22. Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru
    https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani-slozeni-vlastniho-fuzzeru/
  23. Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure
    https://www.root.cz/clanky/knihovny-a-moduly-usnadnujici-testovani-aplikaci-naprogramovanych-v-jazyce-clojure/
  24. Validace dat s využitím knihovny spec v Clojure 1.9.0
    https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/
  25. Testování aplikací naprogramovaných v jazyce Go
    https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/
  26. Knihovny určené pro tvorbu testů v programovacím jazyce Go
    https://www.root.cz/clanky/knihovny-urcene-pro-tvorbu-testu-v-programovacim-jazyce-go/
  27. Testování aplikací psaných v Go s využitím knihoven Goblin a Frisby
    https://www.root.cz/clanky/testovani-aplikaci-psanych-v-go-s-vyuzitim-knihoven-goblin-a-frisby/
  28. Testování Go aplikací s využitím knihovny GΩmega a frameworku Ginkgo
    https://www.root.cz/clanky/testovani-go-aplikaci-s-vyuzitim-knihovny-gomega-mega-a-frameworku-ginkgo/
  29. Tvorba BDD testů s využitím jazyka Go a nástroje godog
    https://www.root.cz/clanky/tvorba-bdd-testu-s-vyuzitim-jazyka-go-a-nastroje-godog/
  30. Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem
    https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem/
  31. Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem (dokončení)
    https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem-dokonceni/
  32. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
    https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/
  33. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
    https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure-2/

20. Odkazy na Internetu

  1. gcov manual: Test Coverage Program
    https://gcc.gnu.org/online­docs/gcc/Gcov.html
  2. How to Analyze Code Coverage with gcov
    https://www.linuxtoday.com/blog/a­nalyzing-code-coverage-with-gcov/
  3. gcov – Unix, Linux Command
    https://www.tutorialspoin­t.com/unix_commands/gcov.htm
  4. Testing code coverage in C using GCOV
    https://www.youtube.com/wat­ch?v=UOGMNRcV9–4
  5. Nástroj objdump: švýcarský nožík pro vývojáře
    https://www.root.cz/clanky/nastroj-objdump-svycarsky-nozik-pro-vyvojare/
  6. What is code coverage?
    https://www.atlassian.com/continuous-delivery/software-testing/code-coverage
  7. Everything you need to know about code coverage
    https://www.codegrip.tech/pro­ductivity/everything-you-need-to-know-about-code-coverage/
  8. GCC, the GNU Compiler Collection
    https://gcc.gnu.org/
  9. Clang 17.0.0: Source-based Code Coverage
    https://clang.llvm.org/doc­s/SourceBasedCodeCoverage­.html
  10. Clang 17.0.0: SanitizerCoverage
    https://clang.llvm.org/doc­s/SanitizerCoverage.html
  11. Name mangling
    https://en.wikipedia.org/wi­ki/Name_mangling
  12. Pokrytí kódu testy (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Pokryt%C3%AD_k%C3%B3du_tes­ty
  13. Code coverage (Wikipedia)
    https://en.wikipedia.org/wi­ki/Code_coverage
  14. Using the GNU Compiler Collection (GCC)
    https://gcc.gnu.org/online­docs/gcc/index.html#Top
  15. Programming Languages Supported by GCC
    https://gcc.gnu.org/online­docs/gcc/G_002b_002b-and-GCC.html#G_002b_002b-and-GCC

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.