Segmenteringsfel

Vid datoranvändning är ett segmenteringsfel (ofta förkortat till segfault ) eller åtkomstbrott ett fel , eller feltillstånd, orsakat av hårdvara med minnesskydd , som meddelar ett operativsystem (OS) programvaran har försökt komma åt ett begränsat minnesområde (a minnesåtkomstbrott). På vanliga x86- datorer är detta en form av allmänt skyddsfel . Operativsystemets kärna kommer, som svar, vanligtvis att utföra någon korrigerande åtgärd, i allmänhet föra felet vidare till den kränkande processen genom att skicka en signal till processen . Processer kan i vissa fall installera en anpassad signalhanterare, så att de kan återhämta sig på egen hand, men annars används OS-standardsignalhanteraren, vilket i allmänhet orsakar onormal avslutning av processen (en programkrasch ) , och ibland en kärndump .

Segmenteringsfel är en vanlig felklass i program skrivna på språk som C som ger låg minnesåtkomst och få eller inga säkerhetskontroller. De uppstår främst på grund av fel vid användning av pekare för virtuell minnesadressering, särskilt olaglig åtkomst. En annan typ av minnesåtkomstfel är ett bussfel , som också har olika orsaker, men som idag är mycket sällsyntare; dessa uppstår främst på grund av felaktig fysisk minnesadressering, eller på grund av felaktig minnesåtkomst – dessa är minnesreferenser som hårdvaran inte kan adressera, snarare än referenser som en process inte får adressera.

Många programmeringsspråk kan använda mekanismer utformade för att undvika segmenteringsfel och förbättra minnessäkerheten. Till exempel Rust en ägarbaserad modell för att säkerställa minnessäkerhet. Andra språk, som Lisp och Java , använder garbage collection , vilket undviker vissa klasser av minnesfel som kan leda till segmenteringsfel.

Översikt

Exempel på mänsklig genererad signal
Segmenteringsfel som påverkar Krita i KDE :s skrivbordsmiljö

Ett segmenteringsfel uppstår när ett program försöker komma åt en minnesplats som det inte är tillåtet att komma åt, eller försöker komma åt en minnesplats på ett sätt som inte är tillåtet (till exempel försöker skriva till en skrivskyddad plats, eller för att skriva över en del av operativsystemet ).

Termen "segmentering" har olika användningsområden i datoranvändning; i sammanhanget "segmenteringsfel", en term som använts sedan 1950-talet, [ citat behövs ] hänvisar den till adressutrymmet för ett program. Med minnesskydd är endast programmets eget adressutrymme läsbart, och av detta är endast stacken och läs/skrivdelen av datasegmentet i ett program skrivbara, medan läsbara data och kodsegmentet inte är skrivbara. Att försöka läsa utanför programmets adressutrymme, eller skriva till ett skrivskyddat segment av adressutrymmet, resulterar alltså i ett segmenteringsfel, därav namnet.

På system som använder hårdvaruminnessegmentering för att tillhandahålla virtuellt minne , uppstår ett segmenteringsfel när hårdvaran upptäcker ett försök att referera till ett icke-existerande segment, eller att referera till en plats utanför gränserna för ett segment, eller att referera till en plats i ett sätt som inte tillåts av de behörigheter som beviljats ​​för det segmentet. På system som endast använder personsökning leder ett ogiltigt sidfel i allmänhet till ett segmenteringsfel, och segmenteringsfel och sidfel är båda fel som uppstår av det virtuella minneshanteringssystemet . Segmenteringsfel kan också uppstå oberoende av sidfel: olaglig åtkomst till en giltig sida är ett segmenteringsfel, men inte ett ogiltigt sidfel, och segmenteringsfel kan uppstå mitt på en sida (därav inget sidfel), till exempel i en buffertspill som stannar på en sida men olagligt skriver över minnet.

På hårdvarunivån uppstår felet initialt av minneshanteringsenheten ( MMU) vid olaglig åtkomst (om referensminnet finns), som en del av dess minnesskyddsfunktion, eller ett ogiltigt sidfel (om referensminnet inte finns ). Om problemet inte är en ogiltig logisk adress utan istället en ogiltig fysisk adress, uppstår istället ett bussfel , även om dessa inte alltid särskiljs.

På operativsystemnivå fångas detta fel och en signal skickas vidare till den kränkande processen, vilket aktiverar processens hanterare för den signalen. Olika operativsystem har olika signalnamn för att indikera att ett segmenteringsfel har inträffat. På Unix-liknande operativsystem skickas en signal som kallas SIGSEGV (förkortad från segmentation violation ) till den kränkande processen. Microsoft Windows får den kränkande processen ett STATUS_ACCESS_VIOLATION- undantag .

Orsaker

Förhållandena under vilka segmenteringsöverträdelser uppstår och hur de visar sig är specifika för hårdvaran och operativsystemet: olika hårdvara ger upphov till olika fel för givna förhållanden, och olika operativsystem omvandlar dessa till olika signaler som skickas vidare till processer. Den närmaste orsaken är ett minnesåtkomstbrott, medan den underliggande orsaken i allmänhet är ett programvarufel av något slag. Att fastställa grundorsaken att felsöka felet – kan vara enkelt i vissa fall, där programmet konsekvent kommer att orsaka ett segmenteringsfel (t.ex. avläsning av en nollpekare ), medan felet i andra fall kan vara svårt att reproducera och beror på minnesallokering på varje löpning (t.ex. att hänvisa till en dinglande pekare ).

Följande är några typiska orsaker till ett segmenteringsfel:

  • Försök att komma åt en obefintlig minnesadress (utanför processens adressutrymme)
  • Försök att komma åt minne som programmet inte har rättigheter till (som kärnstrukturer i processsammanhang)
  • Försöker skriva skrivskyddat minne (som kodsegment)

Dessa i sin tur orsakas ofta av programmeringsfel som resulterar i ogiltig minnesåtkomst:

  • Avlägsna en nollpekare , som vanligtvis pekar på en adress som inte är en del av processens adressutrymme
  • Avhänvisning eller tilldelning till en oinitierad pekare ( vildpekare , som pekar på en slumpmässig minnesadress)
  • Avlägsna eller tilldela en frigjord pekare ( hängande pekare , som pekar på minne som har frigjorts/avallokerats/raderats)
  • Ett buffertspill
  • Ett stackspill
  • Försöker köra ett program som inte kompileras korrekt. (Vissa kompilatorer [ vilken? ] kommer att mata ut en körbar fil trots förekomsten av kompileringsfel.)

I C-kod uppstår segmenteringsfel oftast på grund av fel i pekaranvändning, särskilt i C dynamisk minnesallokering . Att avreferensera en nollpekare, vilket resulterar i odefinierat beteende , kommer vanligtvis att orsaka ett segmenteringsfel. Detta beror på att en nollpekare inte kan vara en giltig minnesadress. Å andra sidan pekar vilda pekare och dinglande pekare på minne som kanske existerar eller inte, och som kanske inte är läsbara eller skrivbara, och därmed kan resultera i övergående buggar. Till exempel:

              
                   
        
                                        
                   char  *  p1  =  NULL  ;  // Nollpekare  char  *  p2  ;  // Wild-pekare: inte initierad alls.  char  *  p3  =  malloc  (  10  *  storleken på  (  char  ));  // Initialiserad pekare till tilldelat minne  // (förutsatt att malloc inte misslyckades)  fri  (  p3  );  // p3 är nu en dinglande pekare, eftersom minnet har frigjorts 

Att avreferensera någon av dessa variabler kan orsaka ett segmenteringsfel: avreferensering av nollpekaren kommer i allmänhet att orsaka ett segfault, medan läsning från vildpekaren istället kan resultera i slumpmässiga data men inget segfault, och läsning från den dinglande pekaren kan resultera i giltiga data för en while, och sedan slumpmässiga data när de skrivs över.

Hantering

Standardåtgärden för ett segmenteringsfel eller bussfel är onormal avslutning av processen som utlöste det. En kärnfil kan genereras för att underlätta felsökning, och andra plattformsberoende åtgärder kan också utföras. Till exempel Linux- system som använder grsecurity patch logga SIGSEGV-signaler för att övervaka eventuella intrångsförsök med buffertspill .

På vissa system, som Linux och Windows, är det möjligt för själva programmet att hantera ett segmenteringsfel. Beroende på arkitektur och operativsystem kan det körande programmet inte bara hantera händelsen utan kan extrahera en del information om dess tillstånd som att få en stackspårning , processorregistervärden , raden i källkoden när den triggades, minnesadress som var ogiltig åtkomst och om åtgärden var en läsning eller en skrivning.

Även om ett segmenteringsfel i allmänhet innebär att programmet har en bugg som behöver åtgärdas, är det också möjligt att avsiktligt orsaka ett sådant fel i syfte att testa, felsöka och även för att emulera plattformar där direktåtkomst till minne behövs. I det senare fallet måste systemet kunna tillåta programmet att köra även efter att felet uppstått. I detta fall, när systemet tillåter, är det möjligt att hantera händelsen och öka processorprogramräknaren för att "hoppa" över den misslyckade instruktionen för att fortsätta exekveringen.

Exempel

Segmenteringsfel på en EMV- knappsats

Skriver till skrivskyddat minne

Att skriva till skrivskyddat minne ger upphov till ett segmenteringsfel. På nivån för kodfel inträffar detta när programmet skriver till en del av sitt eget kodsegment eller den läsbara delen av datasegmentet, eftersom dessa laddas av operativsystemet i skrivskyddat minne.

Här är ett exempel på ANSI C- kod som vanligtvis orsakar ett segmenteringsfel på plattformar med minnesskydd. Den försöker modifiera en sträng literal , vilket är odefinierat beteende enligt ANSI C-standarden. De flesta kompilatorer kommer inte att fånga detta vid kompileringstillfället, och istället kompilera detta till körbar kod som kraschar:

 

       
      
 int  main  (  void  )  {  char  *  s  =  "hej värld"  ;  *  s  =  'H'  ;  } 

När programmet som innehåller den här koden kompileras placeras strängen "hej världen" i rodatasektionen av programmets körbara fil : den skrivskyddade delen av datasegmentet . När det är laddat placerar operativsystemet det med andra strängar och konstanta data i ett skrivskyddat segment av minnet. När den exekveras ställs en variabel, s , in på att peka på strängens plats, och ett försök görs att skriva ett H -tecken genom variabeln in i minnet, vilket orsakar ett segmenteringsfel. Att kompilera ett sådant program med en kompilator som inte kontrollerar tilldelningen av skrivskyddade platser vid kompilering och kör det på ett Unix-liknande operativsystem ger följande körtidsfel :

 $  gcc segfault.c -g -o segfault  $  ./segfault  Segmenteringsfel 

Bakåtspårning av kärnfilen från GDB :

     
     
                  Programmera  mottagen  signal  SIGSEGV  ,  Segmenteringsfel  .  _  0x1c0005c2  i  main  ()  vid  segfault  .  c  :  6  6  *  s  =  'H'  ; 

Denna kod kan korrigeras genom att använda en array istället för en teckenpekare, eftersom denna allokerar minne på stacken och initierar det till värdet av strängen literal:

   
0     char  s  []  =  "hej värld"  ;  s  [  ]  =  'H'  ;  // ekvivalent, *s = 'H'; 

Även om strängliteraler inte bör modifieras (detta har odefinierat beteende i C-standarden), i C är de av statisk char [] -typ, så det finns ingen implicit konvertering i den ursprungliga koden (som pekar ett char * på den arrayen) , medan de i C++ är av statisk const char [] -typ, och det finns alltså en implicit omvandling, så kompilatorer kommer i allmänhet att fånga detta specifika fel.

Null pekaredereferens

I C- och C-liknande språk används nollpekare för att betyda "pekare till inget objekt" och som en felindikator, och att avleda en nollpekare (en läsning eller skrivning genom en nollpekare) är ett mycket vanligt programfel. C-standarden säger inte att nollpekaren är densamma som pekaren till minnesadress 0, även om det kan vara fallet i praktiken. De flesta operativsystem mappar nollpekarens adress så att åtkomst till den orsakar ett segmenteringsfel. Detta beteende garanteras inte av C-standarden. Avreferensering av en nollpekare är odefinierat beteende i C, och en överensstämmande implementering tillåts anta att varje pekare som avreferens inte är null.

   
  int  *  ptr  =  NULL  ;  printf  (  "%d"  ,  *  ptr  ); 

Den här exempelkoden skapar en nollpekare och försöker sedan komma åt dess värde (läs värdet). Att göra det orsakar ett segmenteringsfel vid körning på många operativsystem.

Att avreferensera en nollpekare och sedan tilldela den (att skriva ett värde till ett icke-existerande mål) orsakar vanligtvis också ett segmenteringsfel:

   
   int  *  ptr  =  NULL  ;  *  ptr  =  1  ; 

Följande kod innehåller en nollpekareferens, men när den kompileras kommer det ofta inte att resultera i ett segmenteringsfel, eftersom värdet är oanvänt och därför kommer dereferensen ofta att optimeras bort genom eliminering av död kod :

   
 int  *  ptr  =  NULL  ;  *  ptr  ; 

Buffer-överflöde

Följande kod får åtkomst till teckenmatrisen s bortom dess övre gräns. Beroende på kompilatorn och processorn kan detta resultera i ett segmenteringsfel.

   
    char  s  []  =  "hej värld"  ;  char  c  =  s  [  20  ]; 

Stack overflow

Ett annat exempel är rekursion utan basfall:

 

     
 int  main  (  void  )  {  return  main  ();  } 

vilket gör att stacken svämmar över vilket resulterar i ett segmenteringsfel. Oändlig rekursion kanske inte nödvändigtvis resulterar i ett stackspill beroende på språket, optimeringar som utförs av kompilatorn och den exakta strukturen hos en kod. I det här fallet är beteendet för oåtkomlig kod (retur-satsen) odefinierat, så kompilatorn kan eliminera det och använda en av slutanrop som kan resultera i ingen stackanvändning. Andra optimeringar kan inkludera att översätta rekursionen till iteration, vilket med tanke på strukturen i exempelfunktionen skulle resultera i att programmet körs för alltid, samtidigt som det förmodligen inte svämmar över sin stack.

Se även

externa länkar