Wat er gebeurt als een getal omslaat

Een geheel getal met een vaste breedte heeft geen ruimte boven zijn maximum, dus er één bij optellen levert het minimum op. Een achtbits teller zonder teken gaat van 255 naar 0; een teller mét teken gaat van 127 naar −128. Er wordt geen fout opgeworpen en er bereikt standaard geen signaal het programma, en juist dat maakt overloop een soort bug in plaats van een foutmelding: de rekensom is stil verkeerd en alles verderop gelooft hem.

De codering die dit veroorzaakt heet two’s complement, en dit is er geen gebrek van. De omslag volgt rechtstreeks uit de eigenschap die de codering de moeite waard maakt, namelijk dat optellen en aftrekken aan één schakeling genoeg hebben.

Wat doet die omslag precies?

Hij gooit de bits weg die er niet in pasten. De optelling verloopt normaal, het resultaat heeft één bit meer nodig dan het type heeft, en dat bit valt af — wat neerkomt op rekenen modulo 2 tot de macht van de breedte.

Breedte Zonder teken Met teken
8 bits 0 tot 255 −128 tot 127
16 bits 0 tot 65.535 −32.768 tot 32.767
32 bits 0 tot 4.294.967.295 −2.147.483.648 tot 2.147.483.647
64 bits 0 tot circa 1,8 × 10^19 circa −9,2 × 10^18 tot 9,2 × 10^18

De regels met teken zijn het onthouden waard, want 2.147.483.647 komt voortdurend voorbij. Het is de grootste waarde die in een 32-bits geheel getal met teken past, en opvallend veel software bewaart aantallen, identificatiecodes en tijdstempels daar nog altijd in.

Waarom is overloop met teken anders dan zonder?

In C en C++ verschillen de twee niet alleen in uitkomst maar in status. Overloop zonder teken is gedefinieerd als omslaan, en een programma mag erop bouwen. Overloop met teken is ongedefinieerd gedrag, wat betekent dat de compiler mag aannemen dat het nooit voorkomt.

Die aanname heeft tanden. Een controle geschreven als if (x + 1 < x) om overloop te betrappen mag door een optimalisator volledig worden weggehaald, met als redenering dat overloop met teken niet kan optreden en de voorwaarde dus altijd onwaar is. De test verdwijnt, de build is volgens de standaard correct, en de bescherming is weg.

De betrouwbare vorm toetst de operanden in plaats van het resultaat, of gebruikt de ingebouwde functies met overloopcontrole die de grote compilers leveren. Rust controleert in debugbuilds en slaat om in releasebuilds, en Python omzeilt de vraag door over te gaan op onbeperkte precisie.

Waar is het scheve bereik een valkuil?

Bij de ene waarde zonder positieve tegenhanger. Het negatief maken van het meest negatieve getal kan niets opleveren dat representeerbaar is, dus levert het zichzelf op: de negatie van −2.147.483.648 in 32 bits is −2.147.483.648.

Dat breekt de aanname dat een absolute waarde nooit negatief is, en dat is precies waar een controle op leunt. Een routine die de grootte van een verschil neemt en een niet-negatief getal verwacht, krijgt voor één invoer een negatief getal, en indexeert daar vervolgens een array mee.

Delen kent dezelfde rand. Het minimum delen door −1 loopt om dezelfde reden over, en op meerdere processors geeft dat een hardwarefout in plaats van een omslag.

Welke echte storingen waren dit?

De goed gedocumenteerde gevallen zijn het kennen waard, want ze waren alle goedkoop te voorkomen en duur om mee te maken. Ariane 5-vlucht 501 ging verloren toen een 64-bits kommagetal werd omgezet naar een 16-bits geheel getal met teken dat het niet kon bevatten, in code die was overgenomen van een tragere raket waar die waarde nooit zo groot werd.

Voor de Boeing 787 verscheen een luchtwaardigheidsrichtlijn die een periodieke herstart voorschreef, omdat een teller van honderdsten van seconden in een 32-bits geheel getal met teken na 248 dagen overliep en de generatorbesturingen tegelijk in een veiligheidsstand zette.

Het jaar 2038 is dezelfde vorm op een schema dat iedereen kan zien: een 32-bits telling met teken van seconden sinds 1970 raakt in januari 2038 op. De oplossing is een breder type, en het werk zit in het vinden van elke plek waar het smalle type is opgeslagen.

Hoe vang je er een voordat hij live gaat?

Door de machine te laten klagen. De sanitizers in GCC en Clang onderscheppen overloop met teken tijdens het draaien, en er één aanzetten voor een testsuite is meestal een dag werk met een blijvend resultaat.

Het type bewust kiezen is de andere helft. Een telling die plausibel boven de twee miljard kan komen hoort vanaf het begin in een 64-bits type, en een identificatiecode die door andere systemen wordt opgeslagen en gelezen net zo goed.

Waar een waarde juist moet blijven staan in plaats van om te slaan — audiomonsters, pixelkanalen — moet dat expliciet worden geschreven, want geen enkel geheeltallig type levert het gratis.

Vragen die mensen stellen

Weet de processor dat het gebeurde? Ja. De hardware zet een overloopvlag, en de reden dat je programma die niet ziet is dat de meeste talen hem niet na elke bewerking controleren.

Is omslaan ooit gewenst? Vaak. Hashes, controlegetallen en cyclische volgnummers leunen allemaal op modulorekenen, en daarom is het gedrag zonder teken gedefinieerd.

Lossen 64-bits typen het op? Voor tellers en tijdstempels in de praktijk wel. Voor producten en sommaties niet, want twee grote waarden vermenigvuldigen loopt in 64 bits net zo goed over.

En JavaScript? Getallen zijn kommagetallen, dus gewoon rekenen slaat niet om; boven 2^53 verliest het precisie, wat nog stiller is. De bitoperatoren zijn de uitzondering en zetten eerst om naar 32 bits met teken.

De omslag is rekenkunde die precies doet wat de codering zegt, op een waarde waarvan niemand verwachtte dat hij de rand zou halen. De two’s complement-calculator laat zien hoe een waarde er bij elke breedte uitziet en waar de grenzen liggen, en de talstelselomzetter is de snelste manier om te zien wat de bits deden.