Adresse d’une fonction, types, tables de dispatch, rappels, qsort, conteneurs génériques
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
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.
%p, gdb, nm) et la situer dans le segment de code.typedef.switch par une table de dispatch.qsort et bsearch.void *, taille, fonction).D’où l’on vient, où l’on va
Amont. Séance 3 : segments, pointeurs, void * ; Séance 4 : structures ; Séance 5 : Vecteur, listes, propriété, valgrind.
Cette séance. La fonction comme donnée, la généricité qu’elle permet et ses limites ; puis la synthèse du cours.
Aval. Structures de données et algorithmes (Java) : interfaces, Comparator, types génériques vérifiés par le compilateur.
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).
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.
Pile simplifiée (gdb : f en 0x7fffffffec68) ; code : adresses de nm (avec_tva : 0x555555555149).
Sortie \(\rightarrow\)
2655
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.
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.
La règle
(…) « fonction (…) rendant », [n] « tableau de \(n\) ».* « pointeur vers ».Ce trajet, à droite puis à gauche en s’éloignant du nom, est appelé règle de droite à gauche.
void (*signal(int, void (*)(int)))(int)
signal : à droite (int, void (*)(int)) : fonction (int, pointeur vers fonction (int) rendant void)* : rendant un pointeur(int) : vers fonction (int)void : rendant void.signal (<signal.h>) reçoit un numéro de signal et un gestionnaire, et rend l’ancien gestionnaire.
typedefSans 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.
Énoncé
f et g ;t, puis le type de t[2] ;h ;h avec Operation ;h(0)(2, 3) si h rend plus ?Correction
f : fonction (const char *) rendant char *. g : pointeur vers fonction (const char *) rendant char.t : tableau de 3 pointeurs vers fonction (double) rendant double ; t[2] : double (*)(double).h : pointeur vers fonction (int) rendant un pointeur vers fonction (int, int) rendant int.Operation (*h)(int);h(0) rend plus, appelé avec 2 et 3 : 5.f, *f et **f : un même appel indirectRé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.
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.
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 */
}Résultat \(\rightarrow\)
*4 -> 1000
+350 -> 1350
/5 -> 270
Lecture. Symbole inconnu (%) : trouver rend NULL ; une commande de plus, une ligne de plus.
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.
switch et sa table de sautsRé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).
| 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é
x = 6; puis, pour k de 0 à 2, x = table[(x + k) % 4](x); : valeur finale ;table[3] soit doubler ?Correction
moitie ; puis carre, doubler, oppose.table[2] : 12 ; table[1] : 144 ; table[2] : 288.0x5555555552f7 en 0x18 : 22 53 devient f7 52.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.
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.
void *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 bsearchQuoi
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.
Résultat \(\rightarrow\)
Amadou 16.00
Awa 14.50
Fatou 14.50
Binta 12.25
Moussa 12.25
Lecture. Moyenne décroissante, puis nom : Awa avant Fatou, Binta avant Moussa.
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.
Résultat \(\rightarrow\)
-7 5 2000000000 -2000000000
-2000000000 -7 5 2000000000
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 cataloguetypedef 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).
qsortcmp_trace est celui de la frame précédente ; glibc 2.39, tri par fusion.
Énoncé
(x,y), dans l’ordre ;Correction
[31 12 27] et [5 19 8] ; puis [31], [12 27] et [5], [19 8].(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.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.
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.
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.
Vecteur génériquetypedef 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.
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.
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.
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);
}Lecture. La liste possède nœuds et données : lib libère chaque donnée.
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.
| 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é
v ;vg_elem(&v, 2) ;Correction
donnees \(=\) 0x40, taille 3, capacite 4, taille_elem 8.0x31 : bloc de 48 octets, 40 utiles, soit 5 places de 8, dont 4 à v (32 octets demandés au realloc).e3 07), « Thies » ; 2021, « Louga » ; 2024, « Mbour ».0x58, sans realloc ; le suivant : realloc de 64 octets, capacité 8.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é.
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.
À retenir
int (*f)(int) ; typedef le nomme.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.void *ctx lui passe un contexte.double en int ; bsearch exige un tableau trié selon le même ordre.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).| 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.
switch et tables de sauts), § 3.7 (procédures, call), § 7.12 (code indépendant de la position).qsort), § 18.4 (déclarateurs complexes).switch).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.
Programmation C avancée — Séance 6 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture.
freçoit l’adresse du code deavec_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 defchangent, pas le code.Lecture. Même appel, autre adresse :
remises’exécute ; \(2950-295=2655\).Lecture. Une donnée de 8 octets : elle se copie, se compare, se passe en paramètre.