Articles tagués “segmentation fault

Caster, c’est mal

En C comme en C++, on se retrouve de temps en temps à caster une variable (de l’anglais to casttranstyper en français). Pourtant, caster, c’est mal. Il faut le faire avec parcimonie et avoir de bonnes raisons quand on décide de le faire. Juste « faire taire un warning du compilateur » n’est pas une bonne raison. Quand on caste, on dit au compilateur : « considère cette variable comme étant d’un autre type, ne fais pas de vérification dessus ». Dans un code parfaitement écrit, on en devrait jamais avoir besoin de caster puisque les types seraient toujours bons. La réalité est un peu différente et il faut éviter de cacher les problèmes potentiellement graves.

Voici un premier exemple :

void print(float* p) {
    printf("%f", *p);
}

int main() {
    int i = 31415;
    print(&i);
}

Ce code génère un warning en C :

warning: passing argument 1 of 'print' from incompatible pointer type [-Wincompatible-pointer-types]
     print(&i);
           ^
main.c:3:6: note: expected 'float *' but argument is of type 'int *'
 void print(float* p) {
      ^~~~~

Il génère une erreur en C++ :

error: cannot convert 'int*' to 'float*' for argument '1' to 'void print(float*)'
     print(&i);

Dans les 2 cas, caster permet de faire taire le compilateur mais le résultat est bien sûr faux, ça affiche 0.000000. Le cast permet ici de considérer une adresse vers un type comme étant une adresse vers un autre type. On ne change pas la valeur de l’adresse et donc à l’emplacement mémoire, printf() trouve des bits qui correspondent à un entier de 32 bits (probablement codé en complément à 2) et les lit en considérant qu’ils représentent un flottant (logiquement codé en IEEE754). Il n’y a quasiment aucune chance que les 2 représentations donnent la même valeur et l’affichage est faux.

Le problème de ce premier code était assez évident. Prenons un second code un peu plus subtil :

void print(float p) {
	printf("%f", p);
}

int main() {
	int i = 31415;
	print(i);
}

Compilé avec les options -Wall -Wextra, ce code ne génère pas de warning. On pourrait se dire que tout va bien, les langages C et C++ autorisant les conversions entre nombres. Autorisées ne veut pas forcément dire parfaites. Ajoutons l’option -Wconversion pour voir ce que GCC a beau à nous dire :

warning: conversion to 'float' from 'int' may alter its value [-Wconversion]
  print(i);

Que vérifie exactement ce warning ? Voici sa documentation :

-Wconversion
Warn for implicit conversions that may alter a value. This includes conversions between real and integer, like abs (x) when x is double; conversions between signed and unsigned, like unsigned ui = -1; and conversions to smaller types, like sqrtf (M_PI). Do not warn for explicit casts like abs ((int) x) and ui = (unsigned) -1, or if the value is not changed by the conversion like in abs (2.0). Warnings about conversions between signed and unsigned integers can be disabled by using -Wno-sign-conversion.

For C++, also warn for confusing overload resolution for user-defined conversions; and conversions that never use a type conversion operator: conversions to void, the same type, a base class or a reference to them. Warnings about conversions between signed and unsigned integers are disabled by default in C++ unless -Wsign-conversion is explicitly enabled.

Bien sûr, un cast empêche le warning d’apparaître. Warning ou pas, la sortie console est bien 31415.000000. En revanche, si i = 987654321, alors la sortie devient 987654336.000000… Et de manière marrante, si i = 987654336 alors la sortie est bien 987654336.000000 et est donc correcte. Vous êtes surpris qu’un code qui passe bien avec -Wall -Wextra puisse produire des résultats erronés ? Hé hé… Premièrement, ces deux options sont le minimum vital et vous pouvez (devez ?) en ajouter d’autres, par exemple -Wwrite-strings. Deuxièmement, les conversions entre nombres, que ce soit lors de simples affectations ou lors de calculs, réservent bien des surprises et vous devriez vous méfiez. -Wconversion est fait pour ça ; -Wsign-conversion et -Wfloat-conversion sont un peu moins brutaux (ils sont activés par -Wconversion automatiquement). Il existe aussi -Wdouble-promotion qui peut révéler des possibles pertes de performances.

Ici, ce n’est que de l’affichage, ce n’est pas catastrophique, mais si c’était des calculs pour la stabilisation votre drône DIY, vous rigoleriez moins ! ; )

Voici un dernier exemple bien plus violent :

void modify(char* p) {
	p[0] += 1;
}

int main() {
	char* message = (char*) "hello";
	modify(message);
}

Ce code cache un warning en C++ :

warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
  char* message = "hello";
                  ^~~~~~~

En C, il faut rajouter l’option explicitement car elle n’est pas inclus dans -Wall -Wextra. Que se passe t-il si on exécute ce code ? Une erreur de segmentation, tout simplement, puisque le programme va tenter d’écrire dans une zone mémoire qui est en lecture seule.

Cet article a prouvé au passage que le système de typage est plus fort en C++ qu’en C mais ce n’était pas son but premier. Le but était de vous montrer que le système de typage est là pour vous aider et que caster vous laisse seul face à vos erreurs au lieu de bénéficier de l’aide de votre compilateur. Rares sont les codes C et C++ sans cast, surtout si vous faites joujou avec du code très bas niveau. Essayez toujours de ne pas avoir à caster, utiliser les opérateur de cast en C++ pour expliciter le but du cast, méfiez-vous des conversions implicites et activer les options de votre compilateur pour qu’il vous aide à détecter autant d’erreurs que possibles.


Typedefs incohérents en C

Le hasard est parfois marrant. Il n’y a pas si longtemps, j’ai passé plusieurs jours à pister une erreur mystérieuse dans du code C. Le symptôme était plutôt simple (une fois qu’on y va en mode debug en regardant les variables qui vont bien) : les champs d’une structure ont des valeurs données dans une fonction, on passe la structure à une autre fonction, et la structure se retrouve avec les valeurs de ces champs décalées. En regardant le code assembleur (ça sert des fois), on pouvait constater que l’accès à un champ de la structure se faisait avec un offset de 0x44 dans une fonction et avec un offset de 0x40 dans l’autre. Il y avait donc une incohérence de définition des structures entre différents composants. La difficulté a été de trouver où, mais ce n’est pas exactement le sujet de l’article…

Là où ça devient « marrant », c’est que peu après avoir réglé ce problème, je suis tombé sur du code écrit pour faire un article sur ce blog et dont le sujet était justement l’incohérence de définition d’une structure entre deux fichiers C. Il est sans doute temps de reprendre ce code et de rédiger un article ^^

En général, quand plusieurs fichiers sources utilisent le même type de structure, on crée un fichier d’en-tête avec le typedef et on inclue cette en-tête dans les différents fichiers sources qui en ont besoin. Pour diverses raisons, on peut se retrouver avec des fichiers objets qui ont été compilés avec différentes définitions de la structure. Il se peut que la présence de certains champs dépende d’une compilation conditionnelle, qu’il existe plusieurs versions du fichier d’en-tête, ou encore parce que la structure contient d’autres types qui souffrent d’une incohérence.

Pour vous montrer l’effet d’une incohérence de structures, j’ai fait simple et j’ai utilisé une structure dont certains champs peuvent être « enlever » par le pré-processeur. Ce type de structure est définie dans definition.h :

#ifndef __DEFINITION_H
#define __DEFINITION_H

typedef struct
{
#ifdef MAKE_TYPE_COHERENT
	long number;
#endif
	void (*function)(void);
} type_t;
#endif

Il y a ensuite deux fichiers sources qui utilisent ce type de structure.

util.c

#include <stdio.h>

#define MAKE_TYPE_COHERENT
#include "definition.h"

void another_function(void)
{
	printf("I am '%s'\n", __func__);
}

void print(type_t t)
{
	puts("----------------");
	printf("number = %ld\n", t.number);
	printf("function = %p\n", t.function);
	puts("----------------");
}

type_t get(void)
{
	return (type_t) {666, another_function};
}

main.c

#include <stdio.h>

#include "definition.h"

extern void another_function(void);
extern void print(type_t t);
extern type_t get(void);

void one_function(void)
{
	printf("I am '%s'\n", __func__);
}

int main(void)
{
	printf("'one_function' is at @%p\n", one_function);
	printf("'another_function' is at @%p\n", another_function);

	type_t t = {0} ;
	t.function = one_function;
	print(t);
	t.function();

	t = get();
	print(t);
	printf("Calling function at %p...\n", t.function);
	t.function();

	return 0;
}

Je compile mon code avec la commande c99, qui rappelle clang sur mon Mac :

$ clang --version
Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.3.0
Thread model: posix

Si la macro qui va bien n’est définie pas dans main.c, on obtient le résultat suivant lors de l’exécution du programme :

$ c99 -c util.c && c99 -c main.c && c99 *.o && ./a.out 
'one_function' is at @0x109941ce0
'another_function' is at @0x109941dd0
----------------
number = 4455668960
function = 0x0
----------------
I am 'one_function'
----------------
number = 666
function = 0x70000000700
----------------
Calling function at 0x29a...
Segmentation fault: 11

Et si cette macro est définie dans main.c :

$ c99 -c util.c && c99 -c main.c -DMAKE_TYPE_COHERENT && c99 *.o && ./a.out 
'one_function' is at @0x10bd52cd0
'another_function' is at @0x10bd52dd0
----------------
number = 0
function = 0x10bd52cd0
----------------
I am 'one_function'
----------------
number = 666
function = 0x10bd52dd0
----------------
Calling function at 0x10bd52dd0...
I am 'another_function'