Adresses, pointeurs, tableaux, pile et tas
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
La mémoire vue par un programme C : une suite d’octets numérotés, des objets rangés à des adresses, des pointeurs qui contiennent ces adresses, des tableaux et des chaînes posés côte à côte, les zones où vivent les objets et l’allocation à la demande.
int, un short, un char, une chaîne ou un pointeur, en little-endian comme en big-endian.p, *p et &p, et suivre une double indirection **pp.a[i], de p + i et de m[i][j] à partir de l’adresse de base et de sizeof.malloc, calloc, realloc et free, et reconnaître une fuite, un double free et un accès après free.show_bytes.D’où l’on vient, où l’on va
Amont. Séance 2 : un octet s’écrit en deux chiffres hexadécimaux, un int est en complément à deux ; le TD a déjà fait lire un dump little-endian.
Cette séance. Où les octets sont rangés et comment un programme les atteint : adresses, pointeurs, tableaux, chaînes, pile et tas.
Aval. Séance 4 : les structures et les unions, dont l’alignement et le remplissage reposent sur les adresses et sur sizeof.
Définitions
Vue par le programme, la mémoire d’un processus est un très grand tableau d’octets, la mémoire virtuelle. Le mot machine est l’unité que traite le processeur : 32 ou 64 bits. Il fixe la largeur des registres et des adresses : \(2^{32}\) octets (4 Gio) en 32 bits ; en 64 bits, 48 bits d’adresse utilisés aujourd’hui (\(2^{48}\) octets, 256 Tio). Une adresse est le numéro d’un octet ; un objet de plusieurs octets a pour adresse celle de son premier octet, et l’atteindre demande son adresse et sa taille.
Résultat \(\rightarrow\)
char 1, short 2, int 4, long 8
float 4, double 8
void * 8, int * 8
Lecture. La norme ne fixe que des minimums et l’ordre char \(\leq\) short \(\leq\) int \(\leq\) long. long : 8 octets ici (modèle LP64, Linux), 4 sous Windows 64 bits (LLP64) ; d’où <stdint.h> (int32_t, uint8_t).
int en mémoire, octet par octet
Adresses simplifiées ; sur la machine, x est en 0x7fffffffecbc et o en 0x7fffffffecc0, avec la même disposition.
Sortie \(\rightarrow\)
67 45
23 01
Deux conventions
Little-endian : l’octet de poids faible à l’adresse la plus basse (x86-64, la plupart des ARM).
Big-endian : l’octet de poids fort à l’adresse la plus basse (protocoles réseau, certains formats de fichiers).
0x01234567 en 0x100 : 67 45 23 01 en little-endian, 01 23 45 67 en big-endian.
L’ordre compte dès que des octets changent de machine ou de type : réseau, fichier binaire, lecture avec un autre type.
Résultat \(\rightarrow\)
Cette machine est little-endian
Lecture. x = 1 n’a qu’un octet non nul : son premier octet vaut 1 en little-endian, 0 en big-endian.
0x12345678 est rangé sur 4 octets en 0x100 ; en 0x200, un dump relevé sur une machine little-endian.
| Addr | +0 | +1 | +2 | +3 |
|---|---|---|---|---|
0x100 (big) |
12 |
34 |
56 |
78 |
0x100 (little) |
7878 |
5656 |
34 |
12 |
0x200 |
7878 |
5656 |
3434 |
1212 |
Énoncé
0x100 en big-endian ;0x100 ;short lu en 0x100 ;int rangé en 0x200 ;Correction
12 34 56 78.78 56 34 12.0x100 : 0x78.78 56, lus de droite à gauche : 0x5678.78 56 34 12 lus de droite à gauche : 0x12345678.0x78563412.Une adresse est un nombre ; une variable peut donc la contenir.
Quoi
Une variable de type \(T\) * dont la valeur est l’adresse d’un objet de type \(T\). &x : l’adresse de x ; *p : l’objet rangé à l’adresse contenue dans p. Comme toute variable, p a une valeur (une adresse) et sa propre adresse, &p.
Pourquoi
Une fonction reçoit des copies de ses arguments : lui passer une adresse lui permet de modifier une variable de l’appelant, ou d’accéder à une zone sans la copier. Le type pointé dit combien d’octets lire et comment les interpréter.
Quand ça échoue
Rien ne vérifie qu’un pointeur désigne un objet vivant : pointeur nul, non initialisé, ou dont l’objet a disparu. Le déréférencer est un comportement indéfini : arrêt par SIGSEGV, ou corruption silencieuse.
Adresses simplifiées ; sur la machine, x est en 0x7fffffffeca8, avec la même disposition.
Sortie \(\rightarrow\)
x=10 y=20 *p=10
x=30 y=20 *p=30
x=30 y=25 *p=25
Lecture. Ligne 5 : x et y occupent 4 octets chacun, côte à côte ; 10 s’écrit 0a 00 00 00 (little-endian).
Lecture. Ligne 6 : p reçoit l’adresse de x, 0x10 ; sur x86-64, un pointeur occupe un mot de 8 octets.
Lecture. Ligne 7 : premier affichage ; *p lit les 4 octets en 0x10, donc 10.
Lecture. Ligne 8 : *p = 30 écrit à l’adresse 0x10 : x devient 30 (1e) sans être nommé.
Lecture. Ligne 9 : deuxième affichage ; p n’a pas changé.
Lecture. Ligne 10 : p = &y change le contenu de p : 0x14. x ne bouge pas.
Lecture. Ligne 11 : *p += 5 lit 20 en 0x14 et y écrit 25 (19).
Lecture. Ligne 12 : troisième affichage ; *p désigne maintenant y.
Adresses simplifiées ; sur la machine, x est en 0x7fffffffecc0 et a en 0x7fffffffec68.
Sortie \(\rightarrow\)
x = 2, y = 1
Lecture. Dans main : x et y, dans le stack frame de main.
Lecture. L’appel copie &x et &y dans les paramètres a et b, dans le stack frame de echange, plus bas dans la pile ; t n’a pas encore de valeur.
Lecture. t = *a : lecture de 4 octets à l’adresse contenue dans a, donc x : t vaut 1.
Lecture. *a = *b : l’écriture passe par l’adresse ; x de main vaut 2.
Lecture. *b = t : y vaut 1.
Lecture. Au retour, le stack frame de echange est rendu ; l’affichage montre l’échange des variables de l’appelant.
Résultat \(\rightarrow\)
p = (nil)
Fin du processus : signal SIGSEGV (Segmentation fault)
Lecture. NULL ne désigne aucun objet : l’écriture en *p arrête le processus. Le pointeur non initialisé contient ce qui traînait sur la pile : ici l’écriture tombe quelque part et le programme semble fonctionner. Seul l’avertissement de gcc signale l’erreur.
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x1000 |
0a0a |
0000 |
0000 |
0000 |
141414 |
000000 |
000000 |
000000 |
0x1008 |
1e1e |
0000 |
0000 |
0000 |
00 |
00 |
00 |
00 |
0x1010 |
0404 |
1010 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x1018 |
1010 |
1010 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
Machine little-endian, 64 bits (pointeurs sur 8 octets).
Énoncé : donner
arr[0], arr[1], arr[2] ;p, l’objet qu’il désigne, *p ;pp et *pp ;**pp, et le chemin suivi.Correction
int de 4 octets à partir de 0x1000 : 10, 20, 30 (0a, 14, 1e).0x1010 (0x100c à 0x100f : inutilisés, alignement) : p = 0x1004 \(=\) &arr[1] ; *p = 20.0x1018 : pp = 0x1010 \(=\) &p ; *pp est p, donc 0x1004.**pp \(=\) *p \(=\) 20 : pp \(\rightarrow\) p (0x1010) \(\rightarrow\) arr[1] (0x1004).
Adresses simplifiées dans la grille ; la sortie montre les vraies (0x7fffffffecd0 pour a[0]).
Sortie \(\rightarrow\)
&a[0] = 0x7fffffffecd0 a[0] = 10
&a[1] = 0x7fffffffecd4 a[1] = 20
&a[2] = 0x7fffffffecd8 a[2] = 30
&a[3] = 0x7fffffffecdc a[3] = 40
&a[4] = 0x7fffffffece0 a[4] = 50
Lecture. Un tableau range \(n\) objets de même type côte à côte. Ligne 5 : 5 int, 20 octets de 0x10 à 0x23 ; rien ne marque la fin.
Lecture. i = 0 : &a[0] \(=\) a \(+\,0\times4\) octets (0x10 ici) ; la vraie adresse affichée avance aussi de 4.
Lecture. i = 1 : &a[1] \(=\) a \(+\,1\times4\) octets (0x14 ici) ; la vraie adresse affichée avance aussi de 4.
Lecture. i = 2 : &a[2] \(=\) a \(+\,2\times4\) octets (0x18 ici) ; la vraie adresse affichée avance aussi de 4.
Lecture. i = 3 : &a[3] \(=\) a \(+\,3\times4\) octets (0x1C ici) ; la vraie adresse affichée avance aussi de 4.
Lecture. i = 4 : &a[4] \(=\) a \(+\,4\times4\) octets (0x20 ici) ; la vraie adresse affichée avance aussi de 4.
Adresses simplifiées ; disposition réelle du stack frame de main (gcc 13, -O0, lue avec objdump -d), a en 0x7fffffffecd0 ; ·· : octet non montré.
Sortie \(\rightarrow\)
a 0x7fffffffecd0, voisin 0x7fffffffecc8
voisin = 7
Lecture. Ligne 4 : a occupe 16 octets ; gcc range le tableau au-dessus des scalaires, avant 8 octets inutilisés et le canari.
Lecture. Ligne 5 : voisin reçoit 7 ; il est avant le tableau, en 0x08.
Lecture. Les vraies adresses confirment la disposition : voisin 8 octets avant a.
Lecture. Tours i = 0 à 3 : les quatre éléments passent à 0.
Lecture. Tour i = 4 : a[4], en 0x20, est hors du tableau ; l’écriture tombe sans erreur dans un emplacement inutilisé.
Lecture. voisin, rangé avant, vaut toujours 7 ; rien ne le garantit (comportement indéfini).
Le même programme, compilé avec -fsanitize=address : des zones interdites entourent chaque tableau, et tout accès y est signalé.
Sortie d’erreur avec AddressSanitizer \(\rightarrow\)
==28885==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffff5400050
WRITE of size 4 at 0x7ffff5400050 thread T0
#0 0x555555555481 in main s3_debord.c:9
...
[64, 80) 'a' (line 4) <== Memory access at offset 80 overflows this variable
...
SUMMARY: AddressSanitizer: stack-buffer-overflow s3_debord.c:9 in main
Lecture. L’écriture de 4 octets à l’offset 80 du stack frame sort de l’intervalle [64, 80) occupé par a, à la ligne 9 (a[i] = 0, tour i = 4). Sans cet outil, rien ne signale l’erreur ; la norme ne prévoit aucun contrôle des indices.
Adresses simplifiées ; sur la machine, mat est en 0x7fffffffece0.
Sortie \(\rightarrow\)
8
12
20
Lecture. Ligne 5 : 6 int rangés ligne par ligne : la ligne 0 (bleu) puis la ligne 1 (vert).
Lecture. b désigne le premier octet ; les écarts se comptent en octets.
Lecture. &mat[0][2] : \((0\times3+2)\times4=8\) octets après le début.
Lecture. &mat[1][0] : \((1\times3+0)\times4=12\) octets après le début.
Lecture. &mat[1][2] : \((1\times3+2)\times4=20\) octets après le début.
Adresses simplifiées ; sur la machine, p est en 0x7fffffffecb8 et arr en 0x7fffffffece0.
Sortie \(\rightarrow\)
30 30 30 30 30
40 30 3
Lecture. Ligne 5 : le tableau, 20 octets à partir de 0x38.
Lecture. Ligne 6 : arr se convertit en &arr[0] : p contient 0x38.
Lecture. Les cinq écritures lisent le même int, à \(\texttt{arr}+2\times4\) : a[i] est par définition *(a + i), d’où même 2[arr].
Lecture. Ligne 10 : p + 3 avance de \(3\times4=12\) octets : 0x44.
Lecture. *p vaut 40, p[-1] 30 ; p - arr compte des éléments, pas des octets : 3.
Compilateur \(\rightarrow\)
s3_decroissance.c:4:43: warning: 'sizeof' on array function parameter 't' will return size of 'int *' [-Wsizeof-array-argument]
Résultat \(\rightarrow\)
main : sizeof a = 20
f : sizeof t = 8
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x7000 |
0a |
00 |
00 |
00 |
1414 |
0000 |
0000 |
0000 |
0x7008 |
1e1e |
0000 |
0000 |
0000 |
28 |
00 |
00 |
00 |
0x7010 |
32 |
00 |
00 |
00 |
3c3c |
0000 |
0000 |
0000 |
Énoncé : donner
p et *p ;q et q - p ;*(p + 3) et p[-1] ;(char *)q - (char *)p ;(T *)0x7000 + 3 pour int, short, char ; (double *)0x7000 + 2 ;&m[1][2] pour int m[3][4] rangé en 0x7000.Correction
p = 0x7008 ; *p = 30.q = 0x7014 ; q - p = 3 (des int, pas des octets).*(p + 3) \(=\) tab[5] = 60 ; p[-1] \(=\) tab[1] = 20.char * : 12 octets.0x700c (\(+12\)), 0x7006 (\(+6\)), 0x7003 (\(+3\)), 0x7010 (\(+16\)).0x7018.Au-delà des variables locales : la place d’un objet et sa durée de vie dépendent de sa déclaration.
| classe | déclaration | zone | durée de vie |
|---|---|---|---|
| automatique | locale sans static |
pile (stack) | du début à la fin du bloc {} |
| statique | globale, ou locale static |
segments .data, .bss, .rodata |
toute l’exécution |
| dynamique | bloc rendu par malloc, calloc, realloc |
tas (heap) | jusqu’au free |
Lecture. La classe de stockage fixe la zone et la durée de vie ; utiliser un objet après la fin de sa durée de vie est un comportement indéfini.
int global_init = 221; /* .data */
int global_zero; /* .bss : a zero */
const int constante = 7; /* .rodata */
int main(void)
{
static int compteur = 0; /* statique local */
int local = 1; /* automatique */
int *tas = malloc(sizeof *tas); /* allouee */
char *gros = malloc(1 << 20); /* allouee, 1 Mio */
for (int k = 0; k < 9; k++)
printf("%-12s %p\n", nom[k], adr[k]);
free(tas);
free(gros);
return 0;
}Résultat \(\rightarrow\)
main 0x5555555551a9
"Kaolack" 0x555555556053
constante 0x555555556004
global_init 0x555555558010
global_zero 0x555555558018
compteur 0x55555555801c
*tas 0x5555555592a0
*gros 0x7ffff7ea9010
local 0x7fffffffec38
Lecture. Dans l’ordre croissant : le code, les constantes et littéraux (.rodata), les statiques (.data, puis .bss pour ceux qui valent 0, dont compteur), le tas, la zone mmap, la pile.
Les adresses affichées par le programme précédent, rangées par zone dans l’ordre croissant.
Lecture. Le code et les données statiques viennent de l’exécutable ; le tas grandit vers les adresses hautes, la pile vers les basses. La glibc place un grand bloc (ici 1 Mio, au-delà de 128 Kio par défaut) dans la zone mmap, avec les bibliothèques.
Compilateur \(\rightarrow\)
s5_retour.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr]
Résultat \(\rightarrow\)
p = (nil)
Fin du processus : signal SIGSEGV (Segmentation fault)
Lecture. t disparaît au retour de creer ; gcc le signale et rend NULL à sa place : lire p[0] arrête le processus. Ailleurs, l’adresse rendue serait invalide (comportement indéfini).
staticRésultat \(\rightarrow\)
1
99
p == q : 1
Lecture. tab est statique : il survit à l’appel, et l’adresse rendue reste valide. Mais il est unique : tous les appels rendent le même tableau, si bien que q[0] = 99 modifie aussi ce que désigne p.
Résultat \(\rightarrow\)
c[2] = 3
d[0] = 1, e[0] = 99
Lecture. remplir écrit dans la zone de l’appelant. creer_tas alloue un bloc par appel : d et e sont indépendants, à libérer par l’appelant.
mallocQuoi
Des fonctions de <stdlib.h> qui réservent un bloc sur le tas et en rendent l’adresse : malloc(n) (\(n\) octets non initialisés), calloc(k, t) (\(k\times t\) octets à zéro), realloc(p, n) (redimensionne, en déplaçant si besoin) ; free(p) rend le bloc.
Pourquoi
Une taille connue seulement à l’exécution, ou un objet qui doit survivre à la fonction qui le crée.
Quand ça échoue
Le retour vaut NULL si la mémoire manque : il se teste. Le bloc reste réservé jusqu’au free : l’oublier est une fuite ; libérer deux fois, ou utiliser le bloc après free, est un comportement indéfini.
malloc, free, calloc
Sortie \(\rightarrow\) adresses simplifiées ; premier bloc en 0x5555555592a0, sur le tas.
p = 0x5555555592a0
q = 0x5555555592a0
0x5555555592a0 : 59 55 55 55 05 00 00 00
0x5555555592a8 : 00 00 00 00 00 00 00 00
0x5555555592b0 : 41 41 41 41 41 41 41 41
0x55555555a2d0 : 00 00 00 00 00 00 00 00
0x55555555a2d8 : 00 00 00 00 00 00 00 00
0x55555555a2e0 : 00 00 00 00 00 00 00 00
Lecture. Ligne 6 : 24 octets sur le tas, au contenu indéterminé ; p contient leur adresse (0x10 ici).
Lecture. memset écrit 24 fois 0x41 ('A').
Lecture. L’adresse du bloc est affichée avant sa libération.
Lecture. free rend le bloc ; le lire serait un comportement indéfini.
Lecture. Un malloc de même taille rend le même bloc : q reçoit l’adresse de l’ancien p.
Lecture. calloc(3, 8) : un autre bloc de 24 octets, mis à zéro.
Lecture. Même adresse pour l’ancien p et pour q.
Lecture. q : 16 octets réécrits par la glibc (liste des blocs libres) ; les 8 derniers gardent les 'A'.
Lecture. Le bloc de calloc est entièrement à zéro ; celui de malloc contient ce qui s’y trouvait.
reallocint *t = malloc(4 * sizeof *t);
for (int i = 0; i < 4; i++)
t[i] = 10 * (i + 1);
int *bouchon = malloc(16); /* apres t */
int *n = realloc(t, 1000 * sizeof *n); /* 4 000 o */
if (n == NULL) { /* t valide */
free(t);
return 1;
}
int *m = realloc(n, 2000 * sizeof *m); /* 8 000 o */
if (m == NULL) {
free(n);
return 1;
}
printf("m[3] = %d\n", m[3]);
free(m);
free(bouchon);Résultat \(\rightarrow\)
t = 0x5555555592a0, bouchon = 0x5555555592c0
n = 0x5555555592e0, deplace : oui
m = 0x5555555592e0, deplace : non
m[3] = 40
Lecture. bouchon occupe la place qui suit t : realloc déplace le bloc et recopie les 16 octets ; au second appel, la place suivante est libre : même adresse. L’ancienne adresse ne doit plus servir.
free et accès après freeCompilateur \(\rightarrow\)
s6_double_free.c:9:5: warning: pointer 'p' used after 'free' [-Wuse-after-free]
Résultat \(\rightarrow\)
premier free fait
Sortie d’erreur \(\rightarrow\)
free(): double free detected in tcache 2
Fin du processus : signal SIGABRT (Aborted)
Lecture. La glibc détecte le double free : SIGABRT. L’accès après free passe : p[0] lit le lien écrit par la glibc dans le bloc libéré (0x55555559 \(=\) 1431655769), et non plus 221.
Sortie d’erreur sous valgrind \(\rightarrow\)
==28965== 12 bytes in 1 blocks are definitely lost in loss record 1 of 1
==28965== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==28965== by 0x1091D0: copier (s6_fuite.c:5)
==28965== by 0x10920F: main (s6_fuite.c:12)
Lecture. Le second appel écrase la seule copie de l’adresse du premier bloc : ces 12 octets ne peuvent plus être libérés. valgrind les compte comme definitely lost et donne la ligne de l’allocation.
Résultat \(\rightarrow\)
pile 10 : 40 o
pile 1000000 : 4000000 o
Fin du processus : signal SIGSEGV (Segmentation fault)
Résultat \(\rightarrow\)
tas 4000000 : 16000000 o
Lecture. int v[n] (C99) reste sur la pile : 16 Mo dépassent les 8 Mio, SIGSEGV ; le tas, lui, peut rendre NULL. Un VLA n’est ni static ni global ; optionnel depuis C11 (__STDC_NO_VLA__).
Énoncé
a, pour b ;a et de b ;a[0] juste après le malloc ?Correction
a : indéterminé (par exemple les restes d’un bloc libéré) ; b : 20 octets à zéro.free(a); free(b); puis a = b = NULL;.free, comportement indéfini ; cas 2 : fuite des 40 premiers octets ; cas 3 : double free, arrêt par la glibc (SIGABRT).Les tableaux l’ont montré : p + i désigne le \(i\)-ième objet après p. La règle générale : pour T *p, p + n ajoute \(n\times\texttt{sizeof(T)}\) octets à l’adresse, et q - p compte des objets de type T.
Résultat \(\rightarrow\)
pi = 0x7fffffffece4, pc = 0x7fffffffece4
*pi = 0x01234567, *pc = 0x67
pi + 1 = 0x7fffffffece8 (+ 4)
pc + 1 = 0x7fffffffece5 (+ 1)
Lecture. pi et pc contiennent la même adresse ; *pi lit 4 octets, *pc un seul, le poids faible 67. pi + 1 avance de 4 octets, pc + 1 de 1 ; un double * avancerait de 8.
Adresses simplifiées ; sur la machine, x est en 0x7fffffffecb8.
Sortie \(\rightarrow\)
octet 0 : 0x44
octet 1 : 0x33
octet 2 : 0x22
octet 3 : 0x11
Lecture. Ligne 5 : 0x11223344 rangé 44 33 22 11.
Lecture. Le cast donne un unsigned char * vers le même octet : le type pointé change, pas l’adresse.
Lecture. cp[0] : l’octet en 0x10, cp \(+\,0\) (pas de 1 octet).
Lecture. cp[1] : l’octet en 0x11, cp \(+\,1\) (pas de 1 octet).
Lecture. cp[2] : l’octet en 0x12, cp \(+\,2\) (pas de 1 octet).
Lecture. cp[3] : l’octet en 0x13, cp \(+\,3\) (pas de 1 octet).
Adresses simplifiées ; sur la machine, s est en 0x7fffffffece2 (première ligne du dump).
Sortie \(\rightarrow\)
sizeof s = 6, strlen(s) = 5
0x7fffffffece2 : 44 61 6b 61 72 00
s = "Da", strlen(s) = 2
s + 3 = "ar"
Lecture. Ligne 5 : 5 lettres et le terminateur '\0' (00) : 6 octets.
Lecture. sizeof mesure le tableau (6), strlen compte les octets avant le premier 00 (5).
Lecture. Le dump montre les codes ASCII : D = 44, a = 61, k = 6b, r = 72.
Lecture. Ligne 9 : un 00 en s[2] coupe la chaîne en deux.
Lecture. s s’arrête au premier 00 : "Da", longueur 2.
Lecture. s + 3 désigne la chaîne "ar", toujours en place après la coupure.
Résultat \(\rightarrow\)
t = 0x7fffffffece2, sizeof t = 6
p = 0x555555556004, sizeof p = 8
q = 0x555555556004
t = thies
Lecture. char t[] : une copie modifiable sur la pile. char *p : l’adresse du littéral, en .rodata, partagé par q ; sizeof p est la taille d’un pointeur. Écrire dans le littéral arrête le processus.
strcpy, strcat, strlen et sizeof
Adresses simplifiées ; sur la machine, dest est en 0x7fffffffecd0.
Sortie \(\rightarrow\)
Longueur : 11
Taille : 20
Lecture. Ligne 6 : 20 octets réservés, au contenu indéterminé (··).
Lecture. strcpy copie les 5 lettres et le terminateur ; le reste n’est pas touché.
Lecture. strcat cherche le 00, écrit " World" par-dessus, puis un nouveau terminateur.
Lecture. strlen : 11 caractères avant le 00.
Lecture. sizeof : 20 octets ; ni strcpy ni strcat ne connaissent cette taille (danger si dest est trop petit : strncpy borne la copie). strcmp compare, strchr cherche un caractère.
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x20 |
4242 |
6f6f |
6e6e |
6a6a |
6f6f |
7575 |
7272 |
0000 |
0x28 |
5555 |
4343 |
4141 |
4444 |
0000 |
00 |
00 |
00 |
Énoncé : donner
s1 ;strlen(s1) et sizeof("Bonjour") ;s2 et strlen(s2) ;sizeof(arr) et sizeof(ptr) ;arr[0] = 'X', ptr[0] = 'X' : valides ?ptr = "World", arr = "World" : valides ?ASCII : 41 A, 42 B, 43 C, 44 D, 55 U, 6a j, 6e n, 6f o, 72 r, 75 u.
Correction
42 6f 6e 6a 6f 75 72 : « Bonjour ».00 en 0x27 ; 8, terminateur compris.55 43 41 44 : « UCAD », longueur 4.arr ; ptr[0] : écriture dans un littéral, comportement indéfini ; ici SIGSEGV (.rodata en lecture seule).ptr change d’adresse : valide. arr = … : refusé (assignment to expression with array type).show_bytes : les octets d’un objettypedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, size_t len) {
for (size_t i = 0; i < len; i++)
printf(" %.2x", start[i]); /* 2 car. */
printf("\n");
}
void show_int(int x) {
printf("int %d (0x%X) :", x, x);
show_bytes((byte_pointer)&x, sizeof(int));
}
void show_float(float x) {
printf("float %f :", x);
show_bytes((byte_pointer)&x, sizeof(float));
}
void show_pointer(void *x) {
printf("pointeur %p :", x);
show_bytes((byte_pointer)&x, sizeof(void *));
}Lecture. Un unsigned char * parcourt l’objet octet par octet : int et pointeur en little-endian, 1.0f \(=\) 0x3f800000.
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x00 |
1e |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
0x08 |
aa |
bb |
cc |
dd |
eeee |
ffff |
00 |
11 |
0x10 |
8a |
7c |
6f |
22 |
9a |
66 |
44 |
17 |
0x18 |
3333 |
777777 |
6f6f |
6c6c6c |
6666 |
6262 |
7979 |
7474 |
0x20 |
6565 |
7373 |
0000 |
44 |
00 |
00 |
88 |
c0 |
0x28 |
dede |
adad |
bebe |
efef |
caca |
fefe |
f0f0 |
0d0d |
Énoncé (little-endian) : type et valeur
*s ;*p ;*(c+2) ;p[1] ;*((short *)(c-1)) ;strlen(c).Correction
short, ee ff : 0xffee (\(-18\)).int, de ad be ef : 0xefbeadde.char en 0x1b : 0x6c, 'l'.int en 0x2c : 0x0df0feca.short en 0x18, 33 77 : 0x7733.size_t : 9, la chaîne « wolfbytes » s’arrête au 00 en 0x22.À retenir
*p est l’objet désigné, de la taille du type pointé.&a[0], sauf sous sizeof et & ; et a[i] \(\equiv\) *(a + i).p + n ajoute \(n\times\texttt{sizeof(*p)}\) octets ; q - p compte des éléments.free) : la classe fixe la durée de vie.char terminé par 00 ; strlen ne compte pas ce terminateur, sizeof mesure le tableau ; un littéral est en lecture seule.Séance 4 : structures et unions
Une struct range des champs de types différents côte à côte ; une union les superpose aux mêmes adresses. La séance repart des outils de celle-ci : sizeof, adresses des champs, lecture des octets avec show_bytes, et explique l’alignement et le remplissage entre les champs.
À préparer
Le TD et le TP de la séance 3 : dumps à lire, pointeurs à suivre, programme show_bytes à étendre à un tableau et à une chaîne.
show_bytes, endianness), chap. 3.8 (tableaux), chap. 9.9 (allocation).Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, valgrind 3.22.0 ; adresses rendues déterministes par setarch -R, pile de 8192 Kio.
Programmation C avancée — Séance 3 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture. Ligne 5 :
xoccupe 4 octets, de0x14à0x17; l’octet de poids faible,67, est à l’adresse la plus basse : little-endian.Lecture. Ligne 6 :
oreçoit l’adresse dex; un pointeur occupe un mot de 8 octets, lui aussi rangé en little-endian.Lecture. Ligne 7 :
o[0]lit l’octet en0x14,o[1]celui en0x15:67et45.Lecture. Ligne 8 :
o[2]eto[3]:23et01, le poids fort en dernier.