Les pointeurs en C

Cet article rappel le fonctionnement de l'arithmétique des pointeurs du langage C, spécifique au projet système et son environnement x86 (32 bits).

Bases

Le langage C permet de définir des pointeurs, un pointeur est défini par le type pointé (connu uniquement à la compilation) et une adresse:

1
int *ptr = (int *) 0x1000;

L'utilisation du cast permet d'éviter un warning, et surtout d'être clair sur nos intentions.

Dans le code ci-dessus, on pointe vers un entier (int, 4 octets sur notre architecture) à l'adresse 0x1000. Attention, cela ne signifie pas qu'il existe un entier valide et lisible à cette adresse, cela signifie simplement que l'on peut à souhait écrire sur ou lire depuis ce pointeur.

1
2
int foo = *ptr;
*ptr = foo * 2;
Dans le code ci-dessus, on lis la valeur pointé puis on lui écrit le double de la valeur lue. Attention, pour pouvoir faire cette opération, on doit s'assurer que le pointeur est réellement valide. Typiquement, un pointeur qui a pour adresse 0 (NULL) provoquera directement une Page Fault.

Arithmétique

Il arrive souvent de vouloir incrémenter ou décaler un pointeur, pour ce faire vous devez garder en tête les règles particulière des opérations arithmétiques (+/-) concernant deux pointeurs (ptr +/- ptr) ou un pointeur et un entier (ptr +/- entier).

Il est par exemple interdit de faire une opération entre deux pointeurs et d'en attendre un pointeur, peu importe leur type. En effet, cela ne fait aucun sens d'obtenir une adresse absolue à partir de deux autres:

1
2
3
4
int *p0 = ...;
int *p1 = ...;

int *p2 = p0 + p2;  // error: invalid operands to binary +

Il est cependant possible de faire une soustraction entre deux pointeurs si on attend comme résultat un entier (on peut d'ailleurs choisir le type ptrdiff_t comme résultat):

1
2
3
4
int *p0 = (int *) 0x1000;
int *p1 = (int *) 0x1004;

ptrdiff_t diff = p1 - p0; // 0x0004

Le dernier cas à prendre en compte est l'opération entre un pointeur et un entier, dans ce cas l'entier donné est implicitement multiplié par la taille du type pointé. Pour mieux comprendre, voici un exemple:

1
2
int *ptr = (int *) 0x1000;
ptr++;

Dans l'exemple précédent, on incrémente notre pointeur de 1 (car ++ est équivalent de += 1). Cela signifie dans le cadre de notre pointeur, que celui-ci va se voir incrémenter de 1 * sizeof(int), soit 4 octets et non 1. Notre adresse vaut donc après incrément: 0x1004.

Voici une liste d'exemples permettant de mieux comprendre la règle:

1
2
3
int *res = ((int *) 0x1000) + 4; // 0x1000 + (4 * sizeof(int)) = 0x1000 + (4 * 4) = 0x1010

int **res = ((int **) 0x1000) - 8; // 0x1000 - (8 * sizeof(int *)) = 0x1000 - (8 * 4) = 0x0FE0

<