OVH Cloud OVH Cloud

Conversion des arguments de printf()

69 réponses
Avatar
candide
Ce post reprend Message-ID: <g5ift4$1ckv$1@biggoron.nerim.net>

Marc Espie a écrit :
> In article <487cb490$0$6783$426a34cc@news.free.fr>,
> candide <candide@free.invalid> wrote:
>> Je ne comprends pas ce que ça ajoute par rapport à
>>
>> printf("%u\n",strlen("toto"));
>>
>> puisque size_t est un type entier non signé.
>
> Et quelle taille ? printf est une fonction a nombre variable d'arguments,
> de prototype
> int printf(const char *, ...);
>
> il n'y a donc pas de verif de type passe le format. Si tu es sur une
plateforme
> ou size_t vaut unsigned long, ton printf ne marchera pas: il
recuperera juste
> un unsigned int comme parametre.

Bon j'ai cherché à me documenter sur la question, hélas rien de très
clair. Si je lis la FAQ de clc, il est effectivement explicitement dit
que je dois caster, je le saurai pour la prochaine fois.

Mais que se passe-t-il, étape par étape, quand l'instruction

printf("%u\n",strlen("toto"));


est exécutée ? La valeur de strlen("toto") est convertie dans le type
int parce que printf est variadique, c'est cela ?



_Ensuite_, cette valeur est convertie en unsigned int à cause du
spécificateur %u, c'est ça ?


Maintenant que se passe-t-il, étape par étape, quand l'instruction

printf("%u\n",(unsigned)strlen("toto"));

est exécutée ?

L'expression strlen("toto") est évaluée puis sa valeur est convertie en
unsigned int. Mais ensuite, pourquoi l'argument (unsigned)strlen("toto")
n'est-il pas converti en int puisque c'est un argument d'une fonction
variadique ?


Et puis, je ne vois pas où il est dit dans la norme que les arguments
entiers sont convertis en int. Si j'ai repéré l'endroit adéquat, la
conversion s'appelle "default argument promotion". Pour moi la
promotion, c'est la promotion :

char, short, champs de bit -> int ou unsigned int.




Mais la conversion

size_t (le type le plus large) -> int

c'est plus de la promotion, c'est une dégradation.

10 réponses

3 4 5 6 7
Avatar
Vincent Lefevre
Dans l'article <g5u567$9e9$,
Marc Espie écrit:

Mais tu ne detectes rien ici !!! ca suppose que tu sais suffisamment
de choses sur le comportement du code pour dire `attention, sur les 5000
lignes de code presentes, il y en a 5 qui peuvent ne pas marcher sur
cette machine.'



En général, le programmeur est censé savoir ce qu'il écrit. Au moment
où il ajoute du code qui n'est pas conforme sur telle ou telle
implémentation, il ajoute l'assert qui va bien au début du code
(éventuellement en double) et le problème est bien détecté le jour
où l'implémentation en question est utilisée. C'est aussi ce que
permettent de faire les scripts style configure (e.g. générés par
autoconf) afin de détecter les extensions requises qui ne sont pas
disponibles. Au moins l'utilisateur sait pourquoi ça ne peut pas
marcher.

Dans la pratique, en C, les types abstraits dont tu me parles m'ont
toujours complique la vie en terme de portage.



Ca va certainement te paraitre choquant, mais dans de l'audit de code,
moins on a d'indirection a la con a travers des types abstraits, moins
on se tape de bugs cretins.



Le C n'est pas fait pour utiliser des types abstraits. Il n'a pas de
notion de type abstrait. Typedef fait de l'equivalence de types.
Pas de garde-fou. Nada. Rien.



Ça ne résout pas tout, mais c'est bien utile quand c'est bien utilisé.
Et l'équivalence de types peut amplement suffire dans bien des cas.

J'ai deja passe quelques heures a debugguer du code bati aux dessus
de typedef foireux, je m'en passerais bien. Je prefere largement
limiter le nombre de niveaux d'indirections. Et j'ai deja passe du
temps a debugguer des include foireux qui definissaient du typedef a
gogo.



C'est juste parce que c'est mal programmé.

Le langage est pourri de ce point de vue. Les noms de types sont
globaux.



Pas complètement globaux, e.g. ils ne sont pas censés apparaître dans
les symboles.

Des que tu as deux projets un peu gros que tu veux utiliser
simultanement, les types abstraits en question te foutent la zone.



C'est exactement pareil pour les noms de fonctions et macros déclarés
par les fichiers d'en-tête. Dans les deux cas, le problème se résout
plus ou moins bien par des namespaces utilisateur.

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)
Avatar
espie
In article <20080720104843$,
Vincent Lefevre <vincent+ wrote:
Dans l'article <g5u567$9e9$,
Marc Espie écrit:

Mais tu ne detectes rien ici !!! ca suppose que tu sais suffisamment
de choses sur le comportement du code pour dire `attention, sur les 5000
lignes de code presentes, il y en a 5 qui peuvent ne pas marcher sur
cette machine.'



En général, le programmeur est censé savoir ce qu'il écrit. Au moment
où il ajoute du code qui n'est pas conforme sur telle ou telle
implémentation, il ajoute l'assert qui va bien au début du code
(éventuellement en double) et le problème est bien détecté le jour
où l'implémentation en question est utilisée. C'est aussi ce que
disponibles. Au moins l'utilisateur sait pourquoi ça ne peut pas
marcher.



Tu supposes deux choses qui sont fausses.
- le programmeur sait ce qu'il ecrit;
- c'est la meme personne qui relit le code derriere.

Ca n'est en fait rarement pas le cas.

Cote autoconf, plus le temps passe, moins j'aime. J'etais deja pas tres fan
au depart, mais a force de corriger des bugs dans les scripts auto-generes
de 5000 lignes, on finit par se dire qu'il n'y a pas grand chose a sauver
dedans.

La seule chose a sauver, sans doute, c'est l'idee d'avoir des ifdef
feature-based, et pas os-based.

Si tu veux une infrastructure plutot mieux, cmake se debrouille nettement
mieux.

De toutes facons, tot ou tard, tous ces systemes grossissent jusqu'a etre
incontrolable. C'est un vrai probleme...
Avatar
Vincent Lefevre
Dans l'article <g5vngh$h2m$,
Marc Espie écrit:

Tu supposes deux choses qui sont fausses.
- le programmeur sait ce qu'il ecrit;



Mauvais programmeur, changer de programmeur.

Ce genre de programmeur n'est pas censé écrire en C.

- c'est la meme personne qui relit le code derriere.



Justement non. Un des buts des assert est notamment d'indiquer à
des relecteurs les limitations éventuelles.

Cote autoconf, plus le temps passe, moins j'aime. J'etais deja pas tres fan
au depart, mais a force de corriger des bugs dans les scripts auto-generes
de 5000 lignes, on finit par se dire qu'il n'y a pas grand chose a sauver
dedans.



Pas vraiment vu de bug à part dans libtool, mais ça vient surtout de
plateformes particulières. Le principal reproche que je ferais aux
autotools c'est qu'ils sont basés sur m4, et aussi trop sur le shell.

La seule chose a sauver, sans doute, c'est l'idee d'avoir des ifdef
feature-based, et pas os-based.



Si tu veux une infrastructure plutot mieux, cmake se debrouille nettement
mieux.



C'est probablement mieux conçu, mais ça a l'air d'être nettement
moins puissant, donc pas vraiment utilisable sur de gros projets
multiplateformes, sauf à refaire une grosse partie du travail.

Es-tu capable d'écrire une configuration cmake pour MPFR, qui
fonctionne tout aussi bien sur les plateformes testées? :)

De toutes facons, tot ou tard, tous ces systemes grossissent jusqu'a
etre incontrolable. C'est un vrai probleme...



Oui.

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)
Avatar
Mickael
Vincent Lefevre wrote:
Dans l'article <g5vngh$h2m$,
Marc Espie écrit:

Tu supposes deux choses qui sont fausses.
- le programmeur sait ce qu'il ecrit;



Mauvais programmeur, changer de programmeur.

Ce genre de programmeur n'est pas censé écrire en C.



Manifestement, tu n'es pas professionnel.
Le code C, ça fait longtemps que ça ne s'écrit plus.
ça se copie-colle, en se foutant royalement de toute norme et de la
qualité du code, à patir de l'existant ou de google.
Le client veut juste une appli qui tourne et que les délais soient
respectés.
Et "programmeur" n'existe plus, le terme étant "développeur", qui est le
plus bas dans la hiérarchie et donc le plus mal payé (souvent < 4K€
nets/mois au statut d'employé, même avec 10 ans d'XP, alors que tu as
quasi le double pour faire des TCD sous Excel en MOA...)
Avatar
Thierry B.
--{ Marc Espie a plopé ceci: }--


Si tu veux une infrastructure plutot mieux, cmake se debrouille nettement
mieux.



C'est d'ailleur une question que je me pose depuis longtemps.
Je suis aussi de plus en plus effrayé par le 'monstre' automachin,
et je cherche un truc plus simple/moderne/facile pour configurer
des softs à trois balles en C. J'entend parler de cmake, mais aussi
de scons...

Quelques retours d'info des connaisseurs ?


--
Statique: tu remplis tu remplis tu remplis y'a plus de place sur la
partition/dans le fichier alloué au swap -> *POUF*
Dynamique: le fichier de swap se remplit, se remplit, se remplit, y'a
plus de place sur c: -> *POUF*
Avatar
espie
In article ,
Thierry B. wrote:
--{ Marc Espie a plopé ceci: }--


Si tu veux une infrastructure plutot mieux, cmake se debrouille nettement
mieux.



C'est d'ailleur une question que je me pose depuis longtemps.
Je suis aussi de plus en plus effrayé par le 'monstre' automachin,
et je cherche un truc plus simple/moderne/facile pour configurer
des softs à trois balles en C. J'entend parler de cmake, mais aussi
de scons...



Les quelques avantages de cmake.
- il genere tout seul des trucs qui passent tres bien a travers un
make parallele.
- il remplace autoconf/automake/libtool. Surtout libtool, en fait.
Au moins il sait pondre des bibliotheques sans se planter, et surtout
il ne multiplie pas les temps de compilation par deux grace a un shell-script
fourre au milieu.
- cmake a un langage de script a peu pres decent. C'est plus facile de
corriger un bout de script cmake que des macros autoconf. Et surtout, il
n'y a rien a regenerer derriere.
- le systeme de cache de valeurs fonctionne, et est standardisee.

L'autoconfig facon cmake pedale environ 10 fois plus vite que celle
d'autoconf. Il y a un peu de portabilite a faire pour cmake lui-meme sur
des systemes particuliers (et bon, c'est pas tres simple a faire), cmake
lui-meme est un peu gros et un peu complexe. Il n'a que deux moteurs
de generation de Makefile dans l'immediat: make generique, et gnumake
(faudrait que quelqu'un se paluche un generateur pour make bsd).
Ceci-dit, chapeau bas aux gens de cmake, leur generateur de makefile
fait des trucs clairs et raisonnablement complets, et surtout qui
fonctionnent avec make -j.

Les inconvenients de cmake:
- c'est un nouveau truc, faut apprendre a s'en servir. La doc est plutot
bien, mais il y a quelques astuces pas super-visibles pour voir en
detail ce qui se passe a la compile.
- il n'y a pas l'immense bibliotheque de *.m4 d'autoconf, ce qui est a la
fois un inconvenient et une chance, vu qu'il n'y a pas les bugs qui vont
avec.

Sur des projets existants, plus le projet est complet, plus son
infrastructure de build est a moitie cassee, plus ca va etre dur de
reverse-engineerer pour nettoyer et passer a cmake
<mauvaise langue>ca doit etre le cas de mpfr, par exemple</mauvaise langue>.

A cote de ca, certains tres gros projets ont adopte cmake, kde en tete,
et en sont fort contents... entre autres parce que leur build va
incroyablement plus vite, il n'y a qu'a comparer kde3 avec un autotools
*amenage pour essayer d'aller plus vite* et kde4 avec cmake *qui compile
reellement plus vite*, surtout sur des machines multi-processeurs.
La difference est flagrante.

Il y a tres certainement un probleme politique derriere, egalement.

cmake est pondu par une boite (kitware) qui fait des sous sur d'autres
choses, et qui diffuse cmake sous licence BSD.

cmake fonctionne avec d'autres choses que des outils gnu. Il ne necessite
pas d'utiliser gnu-make, il ne reclame pas l'installation de gnu-m4,
et il sait meme pondre des bibliotheques pour MacOS X.
En plus, il est dote d'un semblant d'interface graphique pour Windows,
c'est dire si c'est le Mal a l'etat pur...

Bref, tu risques d'attendre tres longtemps avant de voir du cmake dans
tout projet vaguement affilie a la FSF. La ligne du parti etant que
autoconf/automake/libtool c'est bien, et que leurs defauts sont lies
a la complexite de ce qu'ils doivent gerer (tu parles, leur defaut,
c'est surtout un design foireux qui a passe la date de peremption de
10 ans ou a peu pres. Ecrire du meta-truc en perl*4*, en m4, et en Bourne
Shell, il y a de quoi se flinguer).

cote scons, je connais tres mal. Dans l'immediat, les deux sont epsilonesques
niveau logiciel libre, par rapport a autoconf, et c'est bien dommage...

Le plus gros probleme de scons, c'est sans doute qu'il faut un python
d'installe pour s'en servir, tandis que cmake, une fois compile,
fonctionne avec un systeme de base...
Avatar
Antoine Leca
En news:g5oi6o$2nvs$, Marc Espie va escriure:
In article <20080717220053$,
Vincent Lefevre wrote:

Euh, les assert, c'est au runtime (en C). Donc beaucoup trop tard
en prod.



Pas trop tard: tout d'abord, c'est reproductible, car ça se met
typiquement au début du programme, et ça se détecte lors du beta test
de la toute première version.



Bzzzt! Ca suppose que tu aies des tests de couverture *complets*,



Un certain AT a demontré (il y a pas mal de temps) que cela n'existait
pas...

deja, et qui tombent sur ces bugs-la. C'est quand la derniere
fois que tu as vu ce genre de choses ?



C'est vrai que c'est rare...


Dans tes tests, tu verifies ce qui se passe si tu alloues des
structures de 5G ?



Euh cela, par contre, cela m'arrive quand même assez souvent (sur les
machines 64 bits). Cela fait partie des « tests idiots » que je passe sur
n'importe quel programme (juste après avoir vérifier qu'il marche sur des
données « normales », donc sur environ... 50% des programmes que je teste
:-( ), et c'est vrai que cela casse souvent des choses.

tu verifies que les tailles sont affichees correctement, par
exemple ?



Non, pas particulièrement, c'est bien exact.
Je m'apercevrais peut-être du souci si par hasard je récupère un affichage
manifestement bogué (vérification /a posteriori/), mais je ne vais pas aller
vérifier que les affichages sont corrects /a priori/.

Donc d'accord avec Marc, le problème est franchement ch***t.


Antoine
Avatar
Antoine Leca
En news:20080718003505$, Vincent Lefevre va escriure:
Le problème est que si tu stockes un (void *) 0 et que tu le relis en
T *, rien n'est garanti par la norme: tu n'es pas certain d'obtenir
un pointeur nul, ni même une valeur valide de pointeur vers T (e.g.
ça peut planter à ce moment).



En fait il n'est pas besoin d'avoir affaire à des pointeurs nuls. Si tu
émets un (void*)&objet, et que tu le relis en T* (avec T type de objet),
rien n'est garanti par la norme, et surtout pas que tu puisses récupérer ton
objet.

Exemple pratique : sur certaines architectures, les adresses sont des
adresses de int/click/cluster/que_sais_je, donc un pointeur vers char (et
donc un void*) est plus « gros », à l'adresse classique se rajoute une
sous-adresse pour savoir de quel char il s'agit à l'intérieur du machin dont
on a l'adresse.

Évidemment, dans ce cas-là, sizeof(char*) > sizeof(T*).
Si par hasard le mode de passage des paramètres veut que l'on ait la
sous-adresse _avant_ l'adresse (c'est débile, mais bon, avec les
gros-boutiens tout est possible), tu as une erreur directe, la fonction
variadique va lire la sous-adresse et l'interpréter comme une adresse, BOUM.
Cependant la cause la plus claire de problème est le cas indirect : comme le
void* est plus gros, cela décale les paramètres suivants; donc en lisant le
T* on va lire l'adresse de objet et c'est OK, on récupère ce que l'on veut;
par contre la sous-adresse qui suit n'a pas été dépilée, et quand la
fonction variadique va lire l'argument suivant, BOUM.


Les autres exemples sont les pointeurs vers fonctions différents des
pointeurs vers objets, ou sur une architecture segmentée d'avoir certains
pointeurs (ceux qui adresse un segment entier) plus « maigres » que les
pointeurs habituels (auquel cas entre void* et T* cela va fonctionner
jusqu'au jour où le compilateur décide que T est suffisament important pour
devenir un objet_à_segment... et à ce moment-là, BOUM).
Une autre classe sont les architectures de débogage: supposons une
implémentation qui range le type dans le pointeur (en plus de l'adresse de
l'objet pointé). Les conversions consistent donc à changer la partie «type»,
tandis qu'en utilisation tout se passe comme avec les pointeurs
passe-partout habituels; et en plus il est _possible_ de vérifier que les
types sont compatibles à chaque opération (mais il n'est pas obligatoire de
lever SIGSEGV à chaque fois que l'on détecte un problème).


Antoine
Avatar
espie
In article <g623ii$mo4$,
Antoine Leca wrote:
En news:20080718003505$, Vincent Lefevre va escriure:
Le problème est que si tu stockes un (void *) 0 et que tu le relis en
T *, rien n'est garanti par la norme: tu n'es pas certain d'obtenir
un pointeur nul, ni même une valeur valide de pointeur vers T (e.g.
ça peut planter à ce moment).



En fait il n'est pas besoin d'avoir affaire à des pointeurs nuls. Si tu
émets un (void*)&objet, et que tu le relis en T* (avec T type de objet),
rien n'est garanti par la norme, et surtout pas que tu puisses récupérer ton
objet.

Exemple pratique : sur certaines architectures, les adresses sont des
adresses de int/click/cluster/que_sais_je, donc un pointeur vers char (et
donc un void*) est plus « gros », à l'adresse classique se rajoute une
sous-adresse pour savoir de quel char il s'agit à l'intérieur du machin dont
on a l'adresse.

Évidemment, dans ce cas-là, sizeof(char*) > sizeof(T*).
Si par hasard le mode de passage des paramètres veut que l'on ait la
sous-adresse _avant_ l'adresse (c'est débile, mais bon, avec les
gros-boutiens tout est possible), tu as une erreur directe, la fonction
variadique va lire la sous-adresse et l'interpréter comme une adresse, BOUM.
Cependant la cause la plus claire de problème est le cas indirect : comme le
void* est plus gros, cela décale les paramètres suivants; donc en lisant le
T* on va lire l'adresse de objet et c'est OK, on récupère ce que l'on veut;
par contre la sous-adresse qui suit n'a pas été dépilée, et quand la
fonction variadique va lire l'argument suivant, BOUM.



Il me semble me souvenir que c'est explicitement interdit par la norme,
que les cast d'objets vers void* and back preservent la valeur... ce qui
veut dire que sur ces archis, tu dois reserver de la place en plus pour
tes void* pour permettre les cast.

Le seul cas non couvert (et la aussi c'est explicite), ce sont les pointeurs
de fonctions...
Avatar
Antoine Leca
En news:g6274t$mf0$, Marc Espie va escriure:
In article <g623ii$mo4$, Antoine Leca wrote:
Exemple pratique : sur certaines architectures, les adresses sont des
adresses de int/click/cluster/que_sais_je, donc un pointeur vers
char (et donc un void*) est plus « gros », à l'adresse classique se
rajoute une sous-adresse pour savoir de quel char il s'agit à
l'intérieur du machin dont on a l'adresse.

Évidemment, dans ce cas-là, sizeof(char*) > sizeof(T*).
[...] Cependant la cause la plus claire de problème est
le cas indirect : comme le void* est plus gros, cela décale les
paramètres suivants; donc en lisant le T* on va lire l'adresse de
objet et c'est OK, on récupère ce que l'on veut; par contre la
sous-adresse qui suit n'a pas été dépilée, et quand la fonction
variadique va lire l'argument suivant, BOUM.



Il me semble me souvenir que c'est explicitement interdit par la
norme, que les cast d'objets vers void* and back preservent la
valeur...



Certes, mais pas la représentation.
Cast vers void* : on rajoute une sous-adresse à 0, et le pointeur résultant
est plus gros.
Cast depuis void* : on vérifie éventuellement que la sous-adresse est à 0
(en fait si elle ne l'est pas on est en plein comportement indéfini, c'est
tout le propos du morceau de norme auquel tu fais référence), on laisse
tomber la sous-adresse et on revient au pointeur maigre original.


ce qui veut dire que sur ces archis, tu dois reserver de la
place en plus pour tes void* pour permettre les cast.



C'est toujours vrai (je crois que l'on peut montrer que sizeof(void*) majore
sizeof() de tous les pointeurs vers objets, en tous cas c'est une propriété
souvent utilisée pour déterminer les « alignements universels »).
Mais je n'arrive pas à en déduire que pour les fonctions variadiques dont
nous parlions, on est obligé de passer _tous_ les pointeurs comme des
pointeurs void*.

Donc j'imagine le cas suivant: à l'appel

coco( arg1, (void*)&obj2, arg3);

Dans coco(...), en supposant l'architecture habituelle, le va_list ap est en
fait un pointeur dans les arguments; au départ il vaut &arg1; on lit donc
avec va_arg(ap,T1) et on avance au suivant, jusque là ràs. Puis on lis avec
va_arg(ap,T), on lis (au format T*) l'adresse de obj2: avec le modèle
gros_void*, cela va souvent marcher, donc on va récupérer un (T*)&obj2, et
ap avance... de la taille d'un T*, *pas* de la taille d'un void*, donc
pointe en fait sur la sous-adresse (qui vaut 0).
Ensuite, coco exécute va_arg(ap,T3), mais comme ap n'a pas /assez/ avancé,
BOUM.


Antoine
3 4 5 6 7