web-dev-qa-db-fra.com

Quelle est la manière la plus simple de se conformer à la norme pour produire un Segfault en C?

Je pense que la question dit tout. Un exemple couvrant la plupart des normes de C89 à C11 serait utile. J'ai pensé à celui-ci, mais je suppose que c'est juste un comportement indéfini:

#include <stdio.h>

int main( int argc, char* argv[] )
{
  const char *s = NULL;
  printf( "%c\n", s[0] );
  return 0;
}

MODIFIER:

Comme certains votes ont demandé des éclaircissements: je voulais avoir un programme avec une erreur de programmation habituelle (la plus simple à laquelle je pouvais penser était un défaut de segmentation), c'est-à-dire garanti (par standard) pour abandonner. C'est un peu différent de la question de la faute de ségrégation minimale, qui ne se soucie pas de cette assurance.

56
math

Une erreur de segmentation est un comportement défini par l'implémentation . La norme ne définit pas comment l'implémentation doit gérer comportement indéfini et en fait l'implémentation pourrait optimiser le comportement indéfini tout en restant conforme. Pour être clair, le comportement défini par l'implémentation est un comportement qui n'est pas spécifié par la norme mais l'implémentation doit documenter. Un comportement indéfini est un code non portable ou erroné et dont le comportement est imprévisible et ne peut donc pas être invoqué.

Si nous regardons le projet de norme C99 §3.4.3 comportement indéfini qui relève des termes, définitions et symboles section dans le paragraphe 1 il est dit ( accent sur le mien à l'avenir ):

comportement, lors de l'utilisation d'une construction de programme non portable ou erronée ou de données erronées, pour laquelle la présente Norme internationale n'impose aucune exigence

et au paragraphe 2 dit:

REMARQUE Le comportement indéfini possible va de l'ignorance complète de la situation avec des résultats imprévisibles, au comportement pendant la traduction ou l'exécution du programme d'une manière documentée caractéristique de l'environnement (avec ou sans émission d'un message de diagnostic), à la fin d'une traduction ou d'une exécution (avec le émission d'un message de diagnostic).

Si, d'autre part, vous voulez simplement une méthode définie dans la norme qui causera un défaut de segmentation sur la plupart des systèmes Unix , alors raise(SIGSEGV) devrait accomplir cet objectif. Bien que, à proprement parler, SIGSEGV soit défini comme suit:

SIGSEGV un accès invalide au stockage

et §7.14 Traitement du signal <signal.h> dit:

Une implémentation n'a pas besoin de générer aucun de ces signaux, sauf à la suite d'appels explicites à la fonction raise . Des signaux et des pointeurs supplémentaires vers des fonctions non déclarables, avec des définitions de macro commençant respectivement par les lettres SIG et une lettre majuscule ou avec SIG_ et une lettre majuscule, 219) peuvent également être spécifiés par l'implémentation. L'ensemble complet des signaux, leur sémantique et leur gestion par défaut sont définis par l'implémentation ; tous les numéros de signal doivent être positifs.

67
Shafik Yaghmour

raise() peut être utilisé pour déclencher une erreur de segmentation:

raise(SIGSEGV);
99
msam

La norme mentionne uniquement un comportement indéfini. Il ne sait rien de la segmentation de la mémoire. Notez également que le code qui produit l'erreur n'est pas conforme à la norme. Votre code ne peut pas invoquer un comportement indéfini et être conforme au standard en même temps.

Néanmoins, le moyen le plus court de produire une erreur de segmentation sur des architectures qui font génèrent de telles erreurs serait:

int main()
{
    *(int*)0 = 0;
}

Pourquoi est-ce sûr de produire un défaut de segmentation? Parce que l'accès à l'adresse mémoire 0 est toujours intercepté par le système; il ne peut jamais s'agir d'un accès valide (du moins pas par le code de l'espace utilisateur.)

Notez bien sûr que toutes les architectures ne fonctionnent pas de la même manière. Sur certains d'entre eux, ce qui précède ne pouvait pas planter du tout, mais plutôt produire d'autres types d'erreurs. Ou l'instruction pourrait être parfaitement fine, même, et l'emplacement mémoire 0 est très bien accessible. C'est l'une des raisons pour lesquelles la norme ne définit pas réellement ce qui se passe.

16
Nikos C.

Un programme correct ne produit pas de défaut de segmentation. Et vous ne pouvez pas décrire le comportement déterministe d'un programme incorrect.

Un "défaut de segmentation" est une chose qu'un CPU x86 fait. Vous l'obtenez en tentant de référencer la mémoire d'une manière incorrecte. Il peut également se référer à une situation où l'accès à la mémoire provoque une erreur de page (c'est-à-dire essayer d'accéder à la mémoire qui n'est pas chargée dans les tables de pages) et le système d'exploitation décide que vous n'aviez pas le droit de demander cette mémoire. Pour déclencher ces conditions, vous devez programmer directement pour votre système d'exploitation et votre matériel. Ce n'est rien qui soit spécifié par le langage C.

12
Kerrek SB

Si nous supposons que nous n'émettons pas de signal appelant raise, la faute de segmentation est susceptible de provenir d'un comportement non défini. Le comportement indéfini n'est pas défini et un compilateur est libre de refuser de traduire, donc aucune réponse avec indéfini ne peut échouer sur toutes les implémentations. De plus, un programme qui invoque un comportement non défini est un programme erroné.

Mais celui-ci est le plus court que je puisse obtenir ce segfault sur mon système:

main(){main();}

(Je compile avec gcc et -std=c89 -O0).

Et au fait, ce programme invoque-t-il vraiment un comportement indéfini?

7
ouah

Sur certaines plates-formes, un programme C conforme aux normes peut échouer avec une erreur de segmentation s'il demande trop de ressources au système. Par exemple, l'allocation d'un grand objet avec malloc peut sembler réussir, mais plus tard, lorsque l'objet est accédé, il se bloque.

Notez qu'un tel programme n'est pas strictement conforme; les programmes qui répondent à cette définition doivent rester dans chacune des limites minimales de mise en œuvre.

Un programme C conforme aux normes ne peut pas produire un défaut de segmentation autrement, car les seules autres façons sont via un comportement non défini.

Le signal SIGSEGV peut être élevé explicitement, mais il n'y a pas de symbole SIGSEGV dans la bibliothèque C standard.

(Dans cette réponse, "conforme aux normes" signifie: "Utilise uniquement les fonctionnalités décrites dans une version de la norme ISO C, évitant un comportement non spécifié, défini par l'implémentation ou non défini, mais pas nécessairement limité aux limites d'implémentation minimales.")

2
Kaz

La plupart des réponses à cette question portent sur le point clé, qui est: La norme C n'inclut pas le concept de défaut de segmentation. (Puisque C99 il inclut le numéro de signal SIGSEGV, mais il ne définit aucune circonstance où ce signal est délivré, autre que raise(SIGSEGV), qui, comme expliqué dans d'autres réponses, ne compte pas.)

Par conséquent, il n'y a aucun programme "strictement conforme" (c'est-à-dire un programme qui utilise uniquement des constructions dont le comportement est entièrement défini par la norme C, seul) qui est garanti pour provoquer un défaut de segmentation.

Les défauts de segmentation sont définis par une norme différente, POSIX . Ce programme est garanti pour provoquer soit un défaut de segmentation, soit une "erreur de bus" fonctionnellement équivalente (SIGBUS), sur tout système entièrement conforme à POSIX.1-2008, y compris les options Protection de la mémoire et Options avancées en temps réel, à condition que les appels à sysconf, posix_memalign et mprotect réussissent. Ma lecture de C99 est que ce programme a un comportement défini par l'implémentation (pas indéfini!) Considérant uniquement cette norme, et donc c'est conforme mais pas strictement conforme .

#define _XOPEN_SOURCE 700
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(void)
{
    size_t pagesize = sysconf(_SC_PAGESIZE);
    if (pagesize == (size_t)-1) {
        fprintf(stderr, "sysconf: %s\n", strerror(errno));
        return 1;
    }
    void *page;
    int err = posix_memalign(&page, pagesize, pagesize);
    if (err || !page) {
        fprintf(stderr, "posix_memalign: %s\n", strerror(err));
        return 1;
    }
    if (mprotect(page, pagesize, PROT_NONE)) {
        fprintf(stderr, "mprotect: %s\n", strerror(errno));
        return 1;
    }
    *(long *)page = 0xDEADBEEF;
    return 0;
}
1
zwol

Il est difficile de définir une méthode pour erreur de segmentation un programme sur des plateformes non définies. Un défaut de segmentation est un terme vague qui n'est pas défini pour toutes les plates-formes (par exemple, les petits ordinateurs simples).

En considérant uniquement les systèmes d'exploitation qui prennent en charge processus, les processus peuvent recevoir une notification indiquant qu'une erreur de segmentation s'est produite.

De plus, en limitant les systèmes d'exploitation à des systèmes d'exploitation "de type Unix", une méthode fiable pour qu'un processus reçoive un signal SIGSEGV est kill(getpid(),SIGSEGV)

Comme c'est le cas dans la plupart des problèmes multiplates-formes, chaque plate-forme peut (et a généralement) une définition différente de la défaillance de segmentation.

Mais pour être pratique, les systèmes d'exploitation Mac, Lin et Win actuels se

*(int*)0 = 0;

De plus, ce n'est pas un mauvais comportement de provoquer une erreur de segmentation. Certaines implémentations de assert() provoquent un signal SIGSEGV qui pourrait produire un fichier core. Très utile lorsque vous devez faire une autopsie.

Ce qui est pire que de provoquer une erreur de segmentation, c'est de le cacher:

try
{
     anyfunc();
}
catch (...) 
{
     printf("?\n");
}

qui cache l'origine d'une erreur et tout ce que vous avez à faire est:

?

.

1
effbiae
 main;

C'est ça.

Vraiment.

Essentiellement, ce que cela fait, c'est qu'il définit main comme une variable . En C, les variables et les fonctions sont à la fois symboles - pointeurs en mémoire, donc le compilateur ne les distingue pas, et ce code ne génère pas d'erreur.

Cependant, le problème réside dans la façon dont le système exécute les exécutables. En un mot, la norme C requiert que tous les exécutables C aient un point d'entrée de préparation d'environnement intégré, qui se résume essentiellement à "appeler main".

Dans ce cas particulier, cependant, main est une variable, elle est donc placée dans une section de mémoire non exécutable appelée .bss, destiné aux variables (par opposition à .text pour le code). Essayer d'exécuter du code dans .bss viole sa segmentation spécifique, de sorte que le système envoie une erreur de segmentation.

Pour illustrer, voici (une partie de) un objdump du fichier résultant:

# (unimportant)

Disassembly of section .text:

0000000000001020 <_start>:
    1020:   f3 0f 1e fa             endbr64 
    1024:   31 ed                   xor    %ebp,%ebp
    1026:   49 89 d1                mov    %rdx,%r9
    1029:   5e                      pop    %rsi
    102a:   48 89 e2                mov    %rsp,%rdx
    102d:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
    1031:   50                      Push   %rax
    1032:   54                      Push   %rsp
    1033:   4c 8d 05 56 01 00 00    lea    0x156(%rip),%r8        # 1190 <__libc_csu_fini>
    103a:   48 8d 0d df 00 00 00    lea    0xdf(%rip),%rcx        # 1120 <__libc_csu_init>

    # This is where the program should call main
    1041:   48 8d 3d e4 2f 00 00    lea    0x2fe4(%rip),%rdi      # 402c <main> 
    1048:   ff 15 92 2f 00 00       callq  *0x2f92(%rip)          # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    104e:   f4                      hlt    
    104f:   90                      nop

# (Nice things we still don't care about)

Disassembly of section .data:

0000000000004018 <__data_start>:
    ...

0000000000004020 <__dso_handle>:
    4020:   20 40 00                and    %al,0x0(%rax)
    4023:   00 00                   add    %al,(%rax)
    4025:   00 00                   add    %al,(%rax)
    ...

Disassembly of section .bss:

0000000000004028 <__bss_start>:
    4028:   00 00                   add    %al,(%rax)
    ...

# main is in .bss (variables) instead of .text (code)

000000000000402c <main>:
    402c:   00 00                   add    %al,(%rax)
    ...

# aaand that's it! 

PS: Cela ne fonctionnera pas si vous compilez en un exécutable plat. Au lieu de cela, vous provoquerez un comportement indéfini.

1
TheSola10

La forme la plus simple compte tenu du plus petit nombre de caractères est:

++*(int*)0;
0
Enock Gomes Neto