Séance 6 — Pointeurs de fonctions et généricité

Adresse d’une fonction, types, tables de dispatch, rappels, qsort, conteneurs génériques

Dr. El Hadji Bassirou TOURÉ

Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar

Objectifs de la séance

Ce que la séance construit

Une fonction a une adresse, celle de son code ; un pointeur de fonction la garde, et un appel par ce pointeur choisit à l’exécution le code à lancer. Avec des tables de fonctions, des fonctions de rappel, qsort et bsearch, puis le triplet void *, taille, fonction, les structures des séances 4 et 5 s’écrivent une fois pour tout type d’élément.

  • Relever l’adresse d’une fonction (%p, gdb, nm) et la situer dans le segment de code.
  • Lire et écrire un type de pointeur de fonction, avec et sans typedef.
  • Appeler par un pointeur, et remplacer un switch par une table de dispatch.
  • Écrire une fonction de rappel et un comparateur correct pour qsort et bsearch.
  • Programmer un échange, un tri, un tableau dynamique et une liste génériques (void *, taille, fonction).
  • Dire ce que le compilateur ne vérifie plus, et reconnaître les comportements indéfinis qui en découlent.

Une fonction en mémoire

Le nom d’une fonction, une adresse

int avec_tva(int prix)
{
    return prix + prix * 18 / 100;
}
int main(void)
{
    int x = 0;
    printf("avec_tva  : %p\n", (void *)avec_tva);
    printf("&avec_tva : %p\n", (void *)&avec_tva);
    printf("main      : %p\n", (void *)main);
    printf("&x        : %p\n", (void *)&x);
    printf("sizeof    : %zu\n", sizeof &avec_tva);
    return 0;
}

Résultat \(\rightarrow\)

avec_tva  : 0x555555555169
&avec_tva : 0x555555555169
main      : 0x55555555519d
&x        : 0x7fffffffece4
sizeof    : 8

Compilateur, avec -Wpedantic \(\rightarrow\)

s1_adresse.c:9:32: warning: ISO C forbids conversion of function pointer to object pointer type [-Wpedantic]
s1_adresse.c:10:32: warning: ISO C forbids conversion of function pointer to object pointer type [-Wpedantic]
s1_adresse.c:11:32: warning: ISO C forbids conversion of function pointer to object pointer type [-Wpedantic]

Lecture. Le code d’une fonction est rangé dans le segment de code ; son nom désigne l’adresse de sa première instruction, comme &avec_tva. %p attend un void * : conversion non définie par C17 (POSIX l’impose, gcc l’accepte).

Le code d’une fonction, sous gdb

Résultat de gdb \(\rightarrow\)

(gdb) print avec_tva
$1 = {int (int)} 0x1169 <avec_tva>
(gdb) break main
Breakpoint 1 at 0x11a9: file s1_adresse.c, line 7.
(gdb) run
Breakpoint 1, main () at s1_adresse.c:7
7 {
(gdb) print avec_tva
$2 = {int (int)} 0x555555555169 <avec_tva>
(gdb) x/8xb avec_tva
0x555555555169 <avec_tva>: 0xf3 0x0f 0x1e 0xfa 0x55 0x48 0x89 0xe5
(gdb) info symbol avec_tva
avec_tva in section .text of ./s1_adresse
(gdb) x/2i avec_tva
   0x555555555169 <avec_tva>: endbr64
   0x55555555516d <avec_tva+4>: push   %rbp

Résultat de nm \(\rightarrow\)

0000000000001169 T avec_tva
000000000000119d T main

Lecture des adresses

Avant run, gdb donne 0x1169, la valeur de nm : le décalage de avec_tva dans l’exécutable. Au lancement, le chargeur ajoute la base du programme, ici 0x555555554000 ; sans setarch -R, la base change à chaque exécution, le décalage reste.

Lecture. x/8xb : les premiers octets du code (f3 0f 1e fa : endbr64), dans .text, zone lisible et exécutable.

Un pointeur de fonction, pas à pas

Pile simplifiée (gdb : f en 0x7fffffffec68) ; code : adresses de nm (avec_tva : 0x555555555149).

Sortie \(\rightarrow\)

2655

Lecture. f reçoit l’adresse du code de avec_tva : 0x1149 (f3 0f 1e fa : endbr64).

Lecture. Appel indirect : l’adresse est lue dans f, le processeur y saute ; p \(=\) 2950.

Lecture. f = remise : seuls les 8 octets de f changent, pas le code.

Lecture. Même appel, autre adresse : remise s’exécute ; \(2950-295=2655\).

Lecture. Une donnée de 8 octets : elle se copie, se compare, se passe en paramètre.

Quoi, pourquoi, quand ça échoue : pointeur de fonction

Quoi

Un pointeur de fonction contient l’adresse du code d’une fonction ; son type donne les types des paramètres et du résultat : int (*f)(int). Il s’affecte (f = remise), se compare, se range dans un tableau ou une structure, se passe en paramètre ; f(x) appelle la fonction désignée.

Pourquoi

Le choix du code à exécuter devient une donnée : un tri reçoit sa règle de comparaison, un menu associe une commande à une fonction, une bibliothèque rappelle le code de l’utilisateur. Le code appelant s’écrit une fois.

Quand ça échoue

Appeler par un pointeur NULL ou non initialisé est un comportement indéfini (C17, annexe J.2) ; appeler par un pointeur d’un type incompatible avec la fonction désignée aussi (6.3.2.3 §8, 6.5.2.2 §9) : au mieux SIGSEGV, au pire un résultat faux sans message.

Lecture et écriture des types

Déclaration d’un pointeur de fonction

Le type d’un pointeur de fonction se lit autour de son nom ; les parenthèses de (*f) sont nécessaires, car () lie plus fort que *.

déclaration lecture
int f(int); f : fonction (int) rendant int
int *f(int); f : fonction (int) rendant int *
int (*f)(int); f : pointeur vers fonction (int) rendant int
int (*t[4])(int, int); t : tableau de 4 pointeurs vers fonction (int, int) rendant int
int (p)(int *, int); p : pointeur vers fonction (int *, int) rendant int *
int ((q)(char))(int, int); q : pointeur vers fonction (char) rendant un pointeur
vers fonction (int, int) rendant int

Dans une expression, le nom d’une fonction devient un pointeur vers elle, sauf sous & et sizeof (C17 6.3.2.1 §4) : f = avec_tva et f = &avec_tva sont équivalents ; sizeof f vaut 8. Sans nom, int (*)(int) est le type de f, pour un cast ou un paramètre.

Lecture de l’intérieur vers l’extérieur

La règle

  1. Partir du nom déclaré.
  2. Lire à droite : (…) « fonction (…) rendant », [n] « tableau de \(n\) ».
  3. Puis à gauche : * « pointeur vers ».
  4. Une parenthèse fermante arrête la lecture vers la droite, une parenthèse ouvrante celle vers la gauche : sortir de la parenthèse, et reprendre en 2.
  5. Finir par le type de gauche.

Ce trajet, à droite puis à gauche en s’éloignant du nom, est appelé règle de droite à gauche.

void (*signal(int, void (*)(int)))(int)

  1. signal : à droite (int, void (*)(int)) : fonction (int, pointeur vers fonction (int) rendant void)
  2. à gauche * : rendant un pointeur
  3. on sort de la parenthèse ; à droite (int) : vers fonction (int)
  4. à gauche void : rendant void.

signal (<signal.h>) reçoit un numéro de signal et un gestionnaire, et rend l’ancien gestionnaire.

Nom du type : typedef

typedef int (*Operation)(int, int);
Operation op = plus;
Operation (*q)(char);
Operation choisir(char c);

typedef void (*Gestionnaire)(int);
Gestionnaire signal(int sig, Gestionnaire g);

typedef int Fonction(int, int);
Fonction *op2 = plus;

Sans typedef

 int (*op)(int, int) = plus;
int (*(*q)(char))(int, int);
int (*choisir(char c))(int, int);
void (*signal(int sig,
    void (*g)(int)))(int);
int (*op2)(int, int) = plus;

Lecture. typedef se lit comme une déclaration de variable, puis le nom déclaré devient un type. Fonction nomme un type fonction : l’étoile reste visible dans Fonction *op2.

Exercice : lecture de déclarations

char *f(const char *);
char (*g)(const char *);
double (*t[3])(double);
int (*(*h)(int))(int, int);

Énoncé

  1. Lire f et g ;
  2. lire t, puis le type de t[2] ;
  3. lire h ;
  4. réécrire h avec Operation ;
  5. que vaut h(0)(2, 3) si h rend plus ?

Correction

  1. f : fonction (const char *) rendant char *. g : pointeur vers fonction (const char *) rendant char.
  2. t : tableau de 3 pointeurs vers fonction (double) rendant double ; t[2] : double (*)(double).
  3. h : pointeur vers fonction (int) rendant un pointeur vers fonction (int, int) rendant int.
  4. Operation (*h)(int);
  5. h(0) rend plus, appelé avec 2 et 3 : 5.

Appel indirect et table de dispatch

f, *f et **f : un même appel indirect

int avec_tva(int prix)
{
    return prix + prix * 18 / 100;
}
int main(void)
{
    int (*f)(int) = avec_tva;
    int a = avec_tva(1000);
    int b = f(1000);
    int c = (*f)(1000);
    int d = (**f)(1000);
    printf("%d %d %d %d\n", a, b, c, d);
    printf("%d\n", f == &avec_tva);

Résultat \(\rightarrow\)

1180 1180 1180 1180
1

Résultat de objdump -d \(\rightarrow\)

1189: lea    -0x47(%rip),%rax        # 1149 <avec_tva>
1199: call   1149 <avec_tva>
11a1: mov    -0x8(%rbp),%rax
11aa: call   *%rax
11af: mov    -0x8(%rbp),%rax
11b8: call   *%rax
11bd: mov    -0x8(%rbp),%rax
11c6: call   *%rax
11f0: lea    -0xae(%rip),%rax        # 1149 <avec_tva>

Lecture. Trois écritures d’un même appel (*f redevient aussitôt un pointeur). Appel direct : call 1149, adresse inscrite dans l’instruction ; par f : adresse lue dans la pile, puis call *%rax, un appel indirect.

Une table de dispatch, pas à pas

Pile simplifiée (gdb : op en 0x7fffffffec40) ; code : adresses de nm (plus : 0x555555555169).

Sortie \(\rightarrow\)

500

Lecture. Quatre pointeurs de 8 octets côte à côte : l’adresse du code de chaque opération.

Lecture. acc \(=\) 1200 \(=\) 0x4b0.

Lecture. op[2] est lu en op \(+\;2\times8\) \(=\) 0x28 : fois ; \(1200\times3=3600\).

Lecture. L’indice choisit la fonction : op[0] est plus, \(3600+400=4000\).

Lecture. op[3] : divise, \(4000/8=500\).

Lecture. Ni if ni switch : l’indice choisit la case, l’appel est indirect.

Un menu de commandes

typedef struct {
    char symbole;
    Operation f;
} Commande;
static const Commande menu[] = {
    {'+', plus},
    {'-', moins},
    {'*', fois},
    {'/', divise},
};
#define NCMD (sizeof menu / sizeof menu[0])
const Commande *trouver(char c)
{
    for (size_t i = 0; i < NCMD; i++)
        if (menu[i].symbole == c)
            return &menu[i];
    return NULL;    /* symbole inconnu */
}
int main(void)
{
    const char op[4] = {'*', '+', '%', '/'};
    const int val[4] = {4, 350, 2, 5};
    int acc = 250;
    for (int i = 0; i < 4; i++) {
        const Commande *k = trouver(op[i]);
        if (k != NULL)
            acc = k->f(acc, val[i]);
        printf("%c%d -> %d\n", op[i], val[i], acc);
    }

Résultat \(\rightarrow\)

*4 -> 1000
+350 -> 1350
/5 -> 270

Lecture. Symbole inconnu (%) : trouver rend NULL ; une commande de plus, une ligne de plus.

Un automate par table

typedef int (*Etat)(char c, int *mots);
int dehors(char c, int *mots)
{
    if (c == ' ')
        return 0;           /* reste dehors */
    (*mots)++;              /* debut d'un mot */
    return 1;
}
int dedans(char c, int *mots)
{
    (void)mots;
    return c == ' ' ? 0 : 1;
}
int main(void)
{
    Etat automate[2] = {dehors, dedans};
    const char *phrase = " Thies  Mbour Kaolack ";
    int e = 0;
    int mots = 0;
    for (const char *p = phrase; *p != '\0'; p++)
        e = automate[e](*p, &mots);
    printf("%d mots\n", mots);
    return 0;
}

Résultat \(\rightarrow\)

3 mots

Lecture. L’état courant e est un indice dans automate ; chaque fonction d’état lit un caractère et rend l’état suivant. Trois mots : chaque passage de dehors à dedans en compte un.

Le switch et sa table de sauts

int frais(int zone)
{
    switch (zone) {
    case 0:
        return 500;
    case 1:
        return 1000;
    case 2:
        return 1500;
    case 3:
        return 2500;
    case 4:
        return 3000;
    default:
        return -1;
    }
}

Résultat de objdump -d \(\rightarrow\)

1154: cmpl   $0x4,-0x4(%rbp)
1158: ja     11a1 <frais+0x58>
115a: mov    -0x4(%rbp),%eax
115d: lea    0x0(,%rax,4),%rdx
1165: lea    0xe98(%rip),%rax        # 2004
116c: mov    (%rdx,%rax,1),%eax
116f: cltq
1171: lea    0xe8c(%rip),%rdx        # 2004
1178: add    %rdx,%rax
117b: notrack jmp *%rax
117e: mov    $0x1f4,%eax
1193: mov    $0x9c4,%eax
11a1: mov    $0xffffffff,%eax

Résultat de objdump -s -j .rodata \(\rightarrow\)

 2000 01000200 7af1ffff 81f1ffff 88f1ffff  ....z...........
 2010 8ff1ffff 96f1ffff 25640a00           ........%d..    

Lecture. Cas voisins : gcc range une table de sauts (jump table) dans .rodata, un décalage de 4 octets par cas, relatif à 0x2004. Cas 0 : \(\texttt{0x2004}+\texttt{0xfffff17a}\) (soit \(-\)0xe86) \(=\texttt{0x117e}\), le mov $0x1f4 (500).

Exercice : une table de dispatch dans un dump

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x00 0909 5353 5555 5555 5555 5555 0000 0000
0x08 3434 5353 5555 5555 5555 5555 0000 0000
0x10 f7f7 5252 5555 5555 5555 5555 0000 0000
0x18 222222 535353 5555 5555 5555 5555 0000 0000

Action table[4] (typedef int (*Action)(int);), recopiée en 0x00. nm : doubler 12f7, moitie 1309, oppose 1322, carre 1334 ; base 0x555555554000.

Énoncé

  1. la fonction de chaque case ;
  2. x = 6; puis, pour k de 0 à 2, x = table[(x + k) % 4](x); : valeur finale ;
  3. que modifier pour que table[3] soit doubler ?

Correction

  1. \(\texttt{0x555555555309}-\text{base}=\texttt{0x1309}\) : moitie ; puis carre, doubler, oppose.
  2. table[2] : 12 ; table[1] : 144 ; table[2] : 288.
  3. Écrire 0x5555555552f7 en 0x18 : 22 53 devient f7 52.

Fonctions de rappel

Fonction passée en paramètre

void appliquer(int *t, size_t n, int (*f)(int))
{
    for (size_t i = 0; i < n; i++)
        t[i] = f(t[i]);
}
size_t filtrer(const int *t, size_t n, int *dest,
               int (*garde)(int))
{
    size_t k = 0;
    for (size_t i = 0; i < n; i++)
        if (garde(t[i]))
            dest[k++] = t[i];
    return k;
}
int cher(int prix)
{
    return prix >= 2000;
}
int main(void)
{
    int prix[5] = {1500, 700, 2500, 350, 4000};
    int choisis[5];
    appliquer(prix, 5, avec_tva);
    size_t k = filtrer(prix, 5, choisis, cher);
    for (size_t i = 0; i < k; i++)
        printf("%d ", choisis[i]);
    printf("(%zu sur 5)\n", k);
    return 0;
}

Résultat \(\rightarrow\)

2950 4720 (2 sur 5)

Lecture. Le traitement de chaque élément vient de l’appelant : une fonction de rappel (callback), avec_tva ou cher.

Le flux d’un rappel

Qui appelle qui

main appelle filtrer ; filtrer, écrite sans connaître cher, la rappelle pour chaque élément : cinq appels, deux réponses 1.

Lecture. prix vaut alors 1770, 826, 2950, 413, 4720. filtrer empile l’adresse de retour, cher crée son propre stack frame.

Un contexte en void *

typedef struct {
    int seuil;
    int nombre;
    long total;
} Bilan;
void pour_chaque(const int *t, size_t n,
                 void (*f)(int, void *), void *ctx)
{
    for (size_t i = 0; i < n; i++)
        f(t[i], ctx);
}
void compter(int x, void *ctx)
{
    Bilan *b = ctx;     /* au bon type */
    if (x >= b->seuil) {
        b->nombre++;
        b->total += x;
    }
}
int main(void)
{
    int v[5] = {12000, 3500, 27500, 800, 15000};
    Bilan b = {10000, 0, 0};
    pour_chaque(v, 5, compter, &b);
    printf("%d ventes, %ld\n", b.nombre, b.total);

Résultat \(\rightarrow\)

3 ventes, 54500

Lecture. ctx traverse pour_chaque sans être lu ; \(12000+27500+15000=54500\) FCFA. Modèle de qsort_r, pthread_create.

qsort et bsearch

Quoi, pourquoi, quand ça échoue : le comparateur

Quoi

void qsort(void *base, size_t n, size_t taille,
           int (*cmp)(const void *, const void *));
void *bsearch(const void *cle, const void *base, size_t n, size_t taille,
              int (*cmp)(const void *, const void *));

Le comparateur reçoit deux adresses d’éléments et rend un entier négatif, nul ou positif (C17 7.22.5).

Pourquoi

qsort déplace des blocs de taille octets et confie l’ordre au comparateur : il trie des int, des chaînes, des structures.

Quand ça échoue

Soustraction qui déborde, double converti en int, mauvais niveau d’indirection : tri faux, sans message ; réponses incohérentes : comportement indéfini (C17 7.22.5 §4). Algorithme et ordre des égaux non fixés.

Tri des étudiants (par moyenne puis par nom)

typedef struct {
    char nom[12];
    double moyenne;
} Etudiant;
int par_moyenne(const void *a, const void *b)
{
    const Etudiant *x = a;
    const Etudiant *y = b;
    if (x->moyenne != y->moyenne)
        return x->moyenne < y->moyenne ? 1 : -1;
    return strcmp(x->nom, y->nom);  /* puis nom */
}

Résultat \(\rightarrow\)

Amadou  16.00
Awa     14.50
Fatou   14.50
Binta   12.25
Moussa  12.25
int main(void)
{
    Etudiant promo[5] = {
        {"Moussa", 12.25}, {"Awa", 14.50},
        {"Fatou", 14.50}, {"Amadou", 16.00},
        {"Binta", 12.25},
    };
    qsort(promo, 5, sizeof promo[0], par_moyenne);
    for (int i = 0; i < 5; i++) {
        const Etudiant *e = &promo[i];
        printf("%-7s %5.2f\n", e->nom, e->moyenne);
    }

Lecture. Moyenne décroissante, puis nom : Awa avant Fatou, Binta avant Moussa.

Les appels au comparateur

static int appels = 0;
int cmp_trace(const void *a, const void *b)
{
    int x = *(const int *)a;
    int y = *(const int *)b;
    appels++;
    printf("(%d,%d) ", x, y);
    return (x > y) - (x < y);
}
int main(void)
{
    int t[5] = {42, 17, 8, 23, 4};
    qsort(t, 5, sizeof t[0], cmp_trace);
    printf("\n%d appels :", appels);
    for (int i = 0; i < 5; i++)
        printf(" %d", t[i]);
    printf("\n");

Résultat \(\rightarrow\)

(42,17) (23,4) (8,4) (8,23) (17,4) (17,8) (17,23) (42,23) 
8 appels : 4 8 17 23 42

Un tri par fusion

La glibc 2.39 coupe en \(\lfloor n/2\rfloor\) et \(n-\lfloor n/2\rfloor\), trie chaque partie, fusionne : (42,17) ; (23,4), puis [8] avec [4 23] ; enfin 17, puis 42, face à [4 8 23].

Lecture. 8 appels pour 5 éléments ; la norme n’impose pas l’algorithme.

Deux pièges du comparateur

int cmp_faux(const void *a, const void *b)
{
    return *(const int *)a - *(const int *)b;
}
    int t[4] = {2000000000, -2000000000, 5, -7};
    int u[4] = {2000000000, -2000000000, 5, -7};
    qsort(t, 4, sizeof t[0], cmp_faux);
    qsort(u, 4, sizeof u[0], cmp_juste);

Résultat \(\rightarrow\)

-7 5 2000000000 -2000000000 
-2000000000 -7 5 2000000000 
int cmp_tronque(const void *a, const void *b)
{
    double x = *(const double *)a;
    double y = *(const double *)b;
    return (int)(x - y);
}
int main(void)
{
    double m[4] = {12.75, 9.5, 12.25, 12.5};
    qsort(m, 4, sizeof m[0], cmp_tronque);

Résultat \(\rightarrow\)

9.50 12.75 12.25 12.50 

Lecture. \(2\,000\,000\,000-(-2\,000\,000\,000)\) dépasse INT_MAX : comportement indéfini, ici un négatif ; la 2e ligne vient de cmp_juste, (x > y) - (x < y). \((\text{int})(12{,}75-12{,}25)=0\) : deux moyennes différentes passent pour égales.

bsearch dans un catalogue

typedef struct {
    int code;
    const char *nom;
    int prix;           /* FCFA */
} Produit;
int par_code(const void *a, const void *b)
{
    int x = ((const Produit *)a)->code;
    int y = ((const Produit *)b)->code;
    return (x > y) - (x < y);
}
int cle_code(const void *cle, const void *e)
{
    int x = *(const int *)cle;
    int y = ((const Produit *)e)->code;
    return (x > y) - (x < y);
}
int main(void)
{
    Produit cat[3] = {
        {310, "sucre 1 kg", 700},
        {120, "riz 25 kg", 15500},
        {208, "huile 1 L", 1300},
    };
    qsort(cat, 3, sizeof cat[0], par_code);
    int cles[2] = {208, 300};
    for (int i = 0; i < 2; i++) {
        Produit *p = bsearch(&cles[i], cat, 3,
                             sizeof cat[0], cle_code);
        printf("%d : %s\n", cles[i],
               p != NULL ? p->nom : "absent");
    }

Résultat \(\rightarrow\)

208 : huile 1 L
300 : absent

Lecture. Le comparateur de bsearch reçoit la clé (int), puis un élément (Produit).

Exercice : trace d’un qsort

int t[6] = {31, 12, 27, 5, 19, 8};
qsort(t, 6, sizeof t[0], cmp_trace);

cmp_trace est celui de la frame précédente ; glibc 2.39, tri par fusion.

Énoncé

  1. découper : les deux moitiés, puis leurs parties ;
  2. écrire les appels (x,y), dans l’ordre ;
  3. combien d’appels ?
  4. le tri par insertion du cours en fait 14 : pourquoi plus ?

Correction

  1. [31 12 27] et [5 19 8] ; puis [31], [12 27] et [5], [19 8].
  2. (12,27) (31,12) (31,27) : [12 27 31] ; (19,8) (5,8) : [5 8 19] ; fusion : (12,5) (12,8) (12,19) (27,19), puis 27 et 31 sans comparaison.
  3. 9 appels.
  4. L’insertion compare chaque élément à ceux qui le précèdent jusqu’à sa place : 1, 2, 3, 3 et 5 comparaisons, soit 14 ; au pire \(n(n-1)/2\), contre \(O(n\log n)\) pour la fusion.

Généricité

Quoi, pourquoi, quand ça échoue : la généricité

Quoi

Une fonction générique traite des éléments de type quelconque à partir de trois informations : l’adresse (void *), la taille en octets, et les fonctions qui connaissent le type (comparer, afficher, libérer). Elle copie par memcpy et calcule les adresses en octets : (char *)base + i * taille.

Pourquoi

Le tri, l’échange, le tableau dynamique et la liste de la Séance 5 s’écrivent une fois : le code qui connaît le type tient dans quelques fonctions courtes, passées en paramètre. C’est le modèle de qsort, bsearch, memcpy.

Quand ça échoue

Le compilateur ne vérifie plus les types : une mauvaise taille, un comparateur écrit pour un autre type, un élément converti au mauvais type se compilent sans avertissement, et l’erreur est un comportement indéfini à l’exécution.

Un échange générique, pas à pas

Adresses simplifiées (gdb : x en 0x7fffffffec58, tmp en 0x7fffffffebe0 ; la pile dépend de l’environnement).

Sortie \(\rightarrow\)

7 12

Lecture. x \(=\) {12, 1300} : deux int, 8 octets (0x514 \(=\) 1300).

Lecture. y \(=\) {7, 650}.

Lecture. Deux adresses et un nombre d’octets, sans type ; tmp est réservé dans le stack frame.

Lecture. Les 8 octets de x sont copiés dans tmp, sans être interprétés.

Lecture. Les octets de y recouvrent ceux de x.

Lecture. tmp rend à y les anciens octets de x : l’échange est fait.

Lecture. Le même code échange deux double ou deux pointeurs : seule la taille change.

Un tri générique

typedef int (*Comparateur)(const void *, const void *);
void tri_insertion(void *base, size_t n,
                   size_t taille, Comparateur cmp)
{
    char *p = base;     /* adresses en octets */
    for (size_t i = 1; i < n; i++)
        for (char *b = p + i * taille;
             b > p && cmp(b - taille, b) > 0;
             b -= taille)
            echanger(b - taille, b, taille);
}
int cmp_int(const void *a, const void *b)
{
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}
int par_nom(const void *a, const void *b)
{
    const char *const *x = a; /* char * */
    const char *const *y = b;
    return strcmp(*x, *y);
}
int main(void)
{
    int t[4] = {42, 17, 8, 23};
    const char *v[3] = {"Matam", "Kolda",
                        "Bakel"};
    tri_insertion(t, 4, sizeof *t, cmp_int);
    tri_insertion(v, 3, sizeof *v, par_nom);
    for (int i = 0; i < 4; i++)
        printf("%d ", t[i]);
    printf("| %s %s %s\n", v[0], v[1], v[2]);

Résultat \(\rightarrow\)

8 17 23 42 | Bakel Kolda Matam

Lecture. p est un char * : b - taille recule d’un élément, en octets.

Le Vecteur générique

typedef struct {
    void *donnees;      /* bloc du tas */
    size_t taille;      /* elements ranges */
    size_t capacite;    /* places du bloc */
    size_t taille_elem; /* octets par element */
} VecteurG;
void vg_init(VecteurG *v, size_t te, size_t cap)
{
    v->donnees = malloc(cap * te);
    v->taille = 0;
    v->capacite = v->donnees != NULL ? cap : 0;
    v->taille_elem = te;
}
void *vg_elem(const VecteurG *v, size_t i)
{
    char *base = v->donnees;
    return base + i * v->taille_elem;
}
int vg_ajouter(VecteurG *v, const void *x)
{
    if (v->taille == v->capacite) {
        size_t cap = v->capacite == 0 ? 1
                     : 2 * v->capacite;
        size_t octets = cap * v->taille_elem;
        void *t = realloc(v->donnees, octets);
        if (t == NULL)
            return 0;
        v->donnees = t;
        v->capacite = cap;
    }
    void *dest = vg_elem(v, v->taille);
    memcpy(dest, x, v->taille_elem);
    v->taille++;
    return 1;
}

Lecture. vg_ajouter copie les octets reçus, sans connaître leur type.

Le Vecteur générique, pas à pas

Adresses simplifiées (gdb : v en 0x7fffffffec40, bloc en 0x5555555592a0).

Sortie \(\rightarrow\)

4 4800

Lecture. v : 32 octets non initialisés ; un champ de plus qu’en Séance 5, taille_elem.

Lecture. malloc(2 * 8) : bloc de 32 octets, 24 utiles ; taille_elem \(=\) sizeof(Article).

Lecture. memcpy de 8 octets en donnees \(+\;0\times8\), sans connaître le type Article.

Lecture. Deuxième élément à donnees \(+\;1\times8\) : le bloc est plein.

Lecture. realloc de \(4\times8=32\) octets : le bloc, dernier du tas, grandit en place (0x31).

Lecture. (char *)donnees + 2 * 8 \(=\) 0x90, rendu en void *, converti en Article * : a.

Lecture. Le type n’apparaît qu’à la conversion : le conteneur ne manipule que des octets.

Des éléments qui possèdent un bloc

void vg_liberer(VecteurG *v, void (*lib)(void *))
{
    if (lib != NULL)
        for (size_t i = 0; i < v->taille; i++)
            lib(vg_elem(v, i));
    free(v->donnees);
    *v = (VecteurG){NULL, 0, 0, v->taille_elem};
}
void liberer_chaine(void *e)
{
    free(*(char **)e);  /* un char * */
}

Résultat \(\rightarrow\)

Fatou Khadim Awa 

Sortie d’erreur sous valgrind \(\rightarrow\)

All heap blocks were freed -- no leaks are possible
int main(void)
{
    const char *s[3] = {"Fatou", "Khadim", "Awa"};
    VecteurG noms;
    vg_init(&noms, sizeof(char *), 1);
    for (int i = 0; i < 3; i++) {
        char *copie = malloc(strlen(s[i]) + 1);
        if (copie == NULL)
            break;
        strcpy(copie, s[i]);
        vg_ajouter(&noms, &copie);
    }
    for (size_t i = 0; i < noms.taille; i++) {
        char **e = vg_elem(&noms, i);
        printf("%s ", *e);
    }
    printf("\n");
    vg_liberer(&noms, liberer_chaine);

Lecture. lib reçoit l’adresse de l’élément, un char ** : liberer_chaine lit le pointeur et libère la chaîne.

Une liste générique

typedef struct NoeudG {
    void *donnee;           /* possedee */
    struct NoeudG *suivant;
} NoeudG;
void liste_ajouter(NoeudG **pt, void *donnee)
{
    NoeudG *n = malloc(sizeof *n);
    if (n == NULL)
        return;
    n->donnee = donnee;
    n->suivant = *pt;
    *pt = n;
}
void parcourir(const NoeudG *p, void (*f)(void *))
{
    for (; p != NULL; p = p->suivant)
        f(p->donnee);
}
void liste_liberer(NoeudG **pt,
                   void (*lib)(void *))
{
    NoeudG *p = *pt;
    while (p != NULL) {
        NoeudG *s = p->suivant;
        if (lib != NULL)
            lib(p->donnee); /* la donnee d'abord */
        free(p);
        p = s;
    }
    *pt = NULL;
}

Lecture. La liste possède nœuds et données : lib libère chaque donnée.

Une liste générique d’agences

typedef struct {
    char ville[12];
    int clients;
} Agence;
Agence *agence(const char *v, int c)
{
    Agence *a = malloc(sizeof *a);
    if (a == NULL)
        return NULL;
    strcpy(a->ville, v);
    a->clients = c;
    return a;
}
void afficher(void *d)
{
    const Agence *a = d;
    printf("%s (%d) ", a->ville, a->clients);
}
int main(void)
{
    NoeudG *l = NULL;
    liste_ajouter(&l, agence("Podor", 120));
    liste_ajouter(&l, agence("Matam", 85));
    liste_ajouter(&l, agence("Kedougou", 64));
    parcourir(l, afficher);
    printf("\n");
    liste_liberer(&l, free);
    return 0;
}

Résultat \(\rightarrow\)

Kedougou (64) Matam (85) Podor (120) 

Sortie d’erreur sous valgrind \(\rightarrow\)

All heap blocks were freed -- no leaks are possible

Lecture. Une Agence ne possède aucun autre bloc : free convient comme fonction de libération.

Exercice : un tableau générique dans un dump

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x00 4040 0000 0000 0000 0000 0000 0000 0000
0x08 0303 0000 0000 0000 0000 0000 0000 0000
0x10 0404 0000 0000 0000 0000 0000 0000 0000
0x18 0808 0000 0000 0000 0000 0000 0000 0000
Addr +0 +1 +2 +3 +4 +5 +6 +7
0x38 3131 0000 0000 0000 0000 0000 0000 0000
0x40 e3e3 0707 5454 6868 6969 6565 7373 0000
0x48 e5e5 0707 4c4c 6f6f 7575 6767 6161 0000
0x50 e8e8 0707 4d4d 6262 6f6f 7575 7272 0000
0x58 4141 fdfd 0101 0000 0000 0000 0000 0000

v en 0x00, bloc en 0x40 (adresses abrégées) ; éléments : struct { short annee; char region[6]; }.

Énoncé

  1. les champs de v ;
  2. l’en-tête : taille du bloc ;
  3. les éléments ;
  4. vg_elem(&v, 2) ;
  5. un ajout de plus, puis un autre ?

Correction

  1. donnees \(=\) 0x40, taille 3, capacite 4, taille_elem 8.
  2. 0x31 : bloc de 48 octets, 40 utiles, soit 5 places de 8, dont 4 à v (32 octets demandés au realloc).
  3. 2019 (e3 07), « Thies » ; 2021, « Louga » ; 2024, « Mbour ».
  4. \(\texttt{0x40}+2\times8=\texttt{0x50}\).
  5. En 0x58, sans realloc ; le suivant : realloc de 64 octets, capacité 8.

Limites

La perte du typage

void echanger(void *a, void *b, size_t taille)
{
    unsigned char tmp[taille];
    memcpy(tmp, a, taille);
    memcpy(a, b, taille);
    memcpy(b, tmp, taille);
}
int main(void)
{
    int x = 5;
    int y = 9;
    echanger(&x, &y, sizeof(double));
    printf("%d %d\n", x, y);

Sortie d’erreur \(\rightarrow\)

*** stack smashing detected ***: terminated

Fin du processus : signal SIGABRT (Aborted)

Sortie d’erreur avec AddressSanitizer \(\rightarrow\)

==PID==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffff5400034
READ of size 8 at 0x7ffff5400034 thread T0
    #0 0x7ffff78fb42d in memcpy sanitizer_common_interceptors_memintrinsics.inc:115
    #1 0x5555555553ad in echanger s7_typage.c:6
...
Address 0x7ffff5400034 is located in stack of thread T0 at offset 52 in frame
    #0 0x55555555542e in main s7_typage.c:11
...
SUMMARY: AddressSanitizer: stack-buffer-overflow sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy

Lecture. memcpy copie 8 octets depuis un int de 4, sans avertissement : comportement indéfini, ici le canari de la pile est écrasé.

Un appel par un type incompatible

typedef int (*Cmp)(const void *, const void *);
double moyenne(double a, double b)
{
    return (a + b) / 2;
}
int main(void)
{
    int (*f)(int, int) = (int (*)(int, int))moyenne;
    printf("%d\n", f(12, 16));   /* indefini */
    const char *v[3] = {"Thies", "Dakar", "Louga"};
    qsort(v, 3, sizeof v[0], (Cmp)strcmp);
    printf("%s %s %s\n", v[0], v[1], v[2]);

Résultat \(\rightarrow\)

1431654793
Thies Dakar Louga

C17 6.3.2.3 §8

Un pointeur de fonction peut être converti en pointeur vers un autre type de fonction, puis reconverti : il redonne le pointeur d’origine. Appeler la fonction par un pointeur d’un type incompatible avec le sien est un comportement indéfini (aussi 6.5.2.2 §9).

Compilateur \(\rightarrow\)

s7_incompatible.c:11:26: warning: cast between incompatible function types from 'double (*)(double,  double)' to 'int (*)(int,  int)' [-Wcast-function-type]

Lecture. moyenne lit %xmm0, %xmm1 ; l’appelant lit %eax, ici les 32 bits bas de l’adresse de moyenne (0x55555189). strcmp compare les octets bas des adresses, croissants : l’ordre reste.

Synthèse

À retenir

À retenir

  1. Le nom d’une fonction désigne l’adresse de son code, dans le segment de code ; un pointeur de fonction est une donnée de 8 octets qui la garde.
  2. Un type de pointeur de fonction se lit à partir du nom, à droite puis à gauche : int (*f)(int) ; typedef le nomme.
  3. f(x) par un pointeur est un appel indirect (call *%rax) ; une table de pointeurs remplace un switch et s’étend sans toucher au code qui appelle.
  4. Une fonction de rappel transmet un traitement à du code écrit sans le connaître ; un void *ctx lui passe un contexte.
  5. Un comparateur rend un signe, sans soustraction ni conversion d’un double en int ; bsearch exige un tableau trié selon le même ordre.
  6. La généricité du C tient en trois éléments : void *, une taille, des fonctions qui connaissent le type ; le compilateur ne vérifie plus les types, et un appel par un type de fonction incompatible est un comportement indéfini (C17 6.3.2.3 §8, 6.5.2.2 §9).

Synthèse du cours

séance ce qui se voit en mémoire
1. Syntaxe C complète (fondamentaux) types, opérateurs, contrôle, fonctions, compilation, comportement indéfini
2. Représentation numérique entiers non signés et en complément à deux, débordements, IEEE 754
3. Organisation de la mémoire mot, adresses, endianness, pointeurs, tableaux, chaînes, pile et tas
4. Structures et unions champs, alignement, padding, unions, champs de bits
5. Allocation dynamique et structures chaînées propriété, tableau dynamique, listes, piles, files, erreurs du tas, malloc
6. Pointeurs de fonctions et généricité segment de code, appel indirect, rappels, qsort, conteneurs génériques

Une même méthode

Des octets à des adresses : dump, grille, show_bytes ; -std=c17 -Wall -Wextra -O0 -g ; gdb, nm, objdump, valgrind, AddressSanitizer ; ce que la norme garantit, ce qu’une plateforme fait.

Références

  • R. E. Bryant, D. R. O’Hallaron, Computer Systems: A Programmer’s Perspective, 3e éd., Pearson, 2016 : § 3.6.8 (switch et tables de sauts), § 3.7 (procédures, call), § 7.12 (code indépendant de la position).
  • B. W. Kernighan, D. M. Ritchie, The C Programming Language, 2e éd., Prentice Hall, 1988 : § 5.11 (pointeurs de fonctions), § 5.12 (déclarations complexes).
  • K. N. King, C Programming: A Modern Approach, 2e éd., Norton, 2008 : § 17.7 (pointeurs de fonctions, qsort), § 18.4 (déclarateurs complexes).
  • R. C. Seacord, Effective C, 2e éd., No Starch Press, 2024 : chap. 2 (types, pointeurs de fonctions) et chap. 5 (instructions de contrôle, switch).
  • ISO/IEC 9899:2018 (C17) : 6.3.2.1 §4 (désignateur de fonction), 6.3.2.3 §8 (conversion de pointeurs de fonctions), 6.7.6.3 (déclarateurs de fonctions), 7.22.5 (qsort, bsearch).

Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, gdb 15.1, valgrind 3.22.0 ; adresses rendues déterministes par setarch -R.