Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

assert() et génération de code

13 réponses
Avatar
Pierre Maurette
Bonjour,

Le rapport avec ma question précédente "Ça se fait,ça ?" n'est
imaginaire.

Je connais les règles de base sur l'optimisation code source, en gros,
vu sur la toile:

(1) Don't do it.
(2) (For experts only!) Don't do it yet.

Néanmoins, il est des cas qui ressemblent à de la contre-optimisation.
En particulier si je sais des choses sur une donnée - non constante -
que le compilateur ne peut intégrer dans sa démarche.

void func(/* ... */, size_t val)
{
/*
du code dans lequel val est utilisé lourdement,
à la fois dans des offset et dans des bornes de boucles
*/
}

Le compilateur va générer le code en considérant que val peut prendre
toutes les valeurs du type. Mais moi, je *sais* que val ne pourra
prendre que les valeurs 1 et 3. Et je vois clairement qu'il sera plus
efficace de dupliquer tout le code, une fois en remplaçant val par
(size_t)1, une fois par (size_t)3, avec un test de tête. Sans même
parler de duplication de la fonction.

Je sais bien que la norme laisse liberté au compilateur de générer du
code pour peu que le résultat soit celui que le programmeur est en
droit d'attendre. Mais d'un autre coté le programmeur est en droit
d'attendre un minimum de qualité.

Y-a-t-il un moyen reconnu de faire passer l'information au compilateur
avec un maximum de chances que le message passe ?

Dans ce cas précis, faire de val une enum ? Introduire dans la fonction
ou ailleurs un:
const size_t val[] = {1, 3};

Ou alors, solution plus générale (on peut avoir une inégalité, ou
l'indication d'une congruence à 4 par exemple):
assert(val == 1 || val == 3);
ou
assert((val % 4) == 0);

Je serais moi-même compilateur, c'est la solution de l'assert() qui me
conviendrait le mieux.

J'ai fait (trop ?) rapidement des tests avec gcc, il semble qu'il s'en
tamponne le coquillard, de mon assert().

Bonne journée législative...

--
Pierre Maurette

3 réponses

1 2
Avatar
Charlie Gordon
"Marc Espie" a écrit dans le message de news:
f542kl$bkt$
In article <467581ae$0$5083$,
Harpo wrote:
On Sun, 17 Jun 2007 10:08:45 +0000, Marc Espie wrote:

Vu les conneries qu'ils ont deja fait ces dernieres annees (la plus
belle
etant sans doute de virer les memcpy a 0 "inutiles"...


Qu'est-ce que c'est ?


J'ai ecrit memcpy en pensant memset, en fait.
memset fait partie de la norme, c'est donc une fonction magique.
Dans les versions recentes, gcc remplace systematiquement memset par
des boucles (ce qui occasionne son lot de problemes dans des cas
d'alignement
un peu bizarres... ca encore c'est pas trop grave).

La ou ca pose vraiment probleme, c'est sur un cas d'utilisation assez
courant, style:


void
f()
{
char passwd[250];
// code qui demande un mot de passe a l'utilisateur.
// code qui s'en sert
memset(passwd, 0, sizeof passwd);
}

comme passwd est une variable locale, non utilisee ailleurs, gcc decrete
que le memset ne sert a rien... et hop, un passwd dans la nature.


Voilà un bon exemple de contexte où utiliser memshred(), qui réécrit la
mémoire plusieurs fois avec des valeurs aléatoires pour s'assurer que la NSA
ne pourra pas récupérer le mot de passe par analyse moléculaire des chips de
RAM ;-)

Le mot de passe lu de cette façon est present plus d'une fois en mémoire :
dans le buffer où il est reçu et dont on essaye désormais vainement de
l'effacer avec ce memset (on devrait d'ailleurs utiliser plutôt strset() ;-)
mais aussi dans le buffer d'entrées / sorties si on a utilisé stdio, et dans
les buffers du driver de clavier, de la console, ou ceux de l'interface
réseau selon le cas, sans parler du fichier de swap... ce n'est pas trivial
de se débarrasser d'un mot de passe ou d'éviter qu'il ne soit intercepté
(gdb, wireshark, strace, libpreload, redefinition des fonctions d'E/S...)
Si le petit curieux est root ou même simplement peut exécuter du code dans
le même process, la méthode simpliste qui ne marche plus a pu rendre la
tâche un peu moins facile, mais nullement impossible.

Chqrlie.



Avatar
espie
In article <4676f57d$0$23627$,
Charlie Gordon wrote:
Le mot de passe lu de cette façon est present plus d'une fois en mémoire :
dans le buffer où il est reçu et dont on essaye désormais vainement de
l'effacer avec ce memset (on devrait d'ailleurs utiliser plutôt strset() ;-)
mais aussi dans le buffer d'entrées / sorties si on a utilisé stdio, et dans
les buffers du driver de clavier, de la console, ou ceux de l'interface
réseau selon le cas, sans parler du fichier de swap... ce n'est pas trivial
de se débarrasser d'un mot de passe ou d'éviter qu'il ne soit intercepté
(gdb, wireshark, strace, libpreload, redefinition des fonctions d'E/S...)
Si le petit curieux est root ou même simplement peut exécuter du code dans
le même process, la méthode simpliste qui ne marche plus a pu rendre la
tâche un peu moins facile, mais nullement impossible.


Je ne suis pas d'accord avec ton approche nihiliste: meme si le mot
de passe existe en d'autres endroits, ce qui nous interesse, ici
et maintenant, c'est cette copie specifique en particulier. C'est
celle-la qui sera vulnerable, si par exemple il y a un bug dans le
reste du programme. Et si le programmeur a pense se proteger avec le
memset, histoire de s'assurer qu'un bug ailleurs dans le meme code
ne rendra pas le mot de passe accessible, eh bien il l'a dans l'os.

Et ce, independamment de tous les scenarios que tu peux imaginer... ce
genre de truc, ca se protege petit a petit (deja, si l'executable est
setuid, normalement, tu peux courir avec gdb, strace ou libpreload sur
un systeme correct, et comme je l'ai deja dit, j'utilise un systeme qui
chiffre son swap, precisement pour eviter ce genre d'espionnage).

C'est pas en laissant tomber des le debut sous pretexte qu'il va y avoir
d'autres problemes qu'on ira bien loin...

Avatar
Charlie Gordon
"Marc Espie" a écrit dans le message de news:
f56trp$1ddu$
In article <4676f57d$0$23627$,
Charlie Gordon wrote:
Le mot de passe lu de cette façon est present plus d'une fois en mémoire :
dans le buffer où il est reçu et dont on essaye désormais vainement de
l'effacer avec ce memset (on devrait d'ailleurs utiliser plutôt strset()
;-)
mais aussi dans le buffer d'entrées / sorties si on a utilisé stdio, et
dans
les buffers du driver de clavier, de la console, ou ceux de l'interface
réseau selon le cas, sans parler du fichier de swap... ce n'est pas
trivial
de se débarrasser d'un mot de passe ou d'éviter qu'il ne soit intercepté
(gdb, wireshark, strace, libpreload, redefinition des fonctions d'E/S...)
Si le petit curieux est root ou même simplement peut exécuter du code dans
le même process, la méthode simpliste qui ne marche plus a pu rendre la
tâche un peu moins facile, mais nullement impossible.


Je ne suis pas d'accord avec ton approche nihiliste: meme si le mot
de passe existe en d'autres endroits, ce qui nous interesse, ici
et maintenant, c'est cette copie specifique en particulier. C'est
celle-la qui sera vulnerable, si par exemple il y a un bug dans le
reste du programme. Et si le programmeur a pense se proteger avec le
memset, histoire de s'assurer qu'un bug ailleurs dans le meme code
ne rendra pas le mot de passe accessible, eh bien il l'a dans l'os.

Et ce, independamment de tous les scenarios que tu peux imaginer... ce
genre de truc, ca se protege petit a petit (deja, si l'executable est
setuid, normalement, tu peux courir avec gdb, strace ou libpreload sur
un systeme correct, et comme je l'ai deja dit, j'utilise un systeme qui
chiffre son swap, precisement pour eviter ce genre d'espionnage).

C'est pas en laissant tomber des le debut sous pretexte qu'il va y avoir
d'autres problemes qu'on ira bien loin...


Certes,

Mais le mot de passe sera quand même plus facile à extraire du buffer de
stdin que de la pile où il aura pu être écrasé maintes fois avant que le
code malveillant ne prenne la main.

Le cas que tu décris est problématique, et je suis d'accord que le
programmeur moyen écrit souvent des lourdeurs dont même une analyse sérieuse
par le compilateur ne peut supprimer l'inefficacité sémantique, exemples:

int f() {
char filename[PATH_MAX] = "";
...
}

On veut simplement initialiser filename[0] à '' et c'est tout le buffer
qui est mis à 0, avec un code pas toujours optimal.

Que memset soit enfin optimisé intrinsèquement par gcc ne me choque pas,
cela permet de simplifier les headers de la libc tout en généralisant la
génération en ligne à toutes les architectures où cela a un sens.

Que les memset inutiles soient supprimés, encore d'accord, ils sont trop
souvent le fait de codes idiomatiques douteux, voire de codes générés par
programme.

Que certtains d'entre eux aient une utilité particulière, c'est
effectivement un problème, mais pas plus que les "boucles de temporisation"
qui soudain ne produisent plus de code.

Je ferais bien une proposition pour C2029 : le qualifier volatile appriqué à
un bloc aurait pour effet que toute opération faite dans ce bloc devrait
produire le code escompté, sans optimisation aucune (suppression de
lectures, écritures...). Bon, il faudrait traduire cela dans la
terminologie de la norme et la sémantique de la machine abstraite, donc 2029
c'est peut-etre encore trop optimiste.

Où en est cette discussion ?
Quels autres problèmes se posent avec ces optimisations ?

Chqrlie


1 2