Wie werden Zahlen größer als 2 ^ 32 von einer 32-Bit-Maschine verarbeitet?

7

Ich versuche zu verstehen, wie Berechnungen mit Zahlen größer als 2 32 auf einer 32-Bit-Maschine passieren.

C-Code

%Vor%

gcc-Ausgabe

%Vor%

Entsprechender Assemblercode

%Vor%

Was genau passiert in den Zeilen 1 - 4?

Ist das eine Art String-Verkettung auf Assembly-Ebene?

    
Lazer 09.10.2010, 21:17
quelle

7 Antworten

19

__printf_chk ist ein Wrapper um printf , der nach einem Stack-Überlauf sucht und einen zusätzlichen ersten Parameter, ein Flag (zB siehe hier .)

pow(2, 64) - 1 wurde auf 0xffffffffffffffff optimiert, da die Argumente Konstanten sind.

Gemäß den üblichen Aufrufkonventionen ist das erste Argument für __printf_chk() ( int flag ) ein 32-Bit-Wert im Stapel (bei %esp zum Zeitpunkt der Anweisung call ). Das nächste Argument, const char * format , ist ein 32-Bit-Zeiger (das nächste 32-Bit-Wort auf dem Stapel, d. H. Bei %esp+4 ). Und die 64-Bit-Menge, die gedruckt wird, belegt die nächsten zwei 32-Bit-Wörter (bei %esp+8 und %esp+12 ):

%Vor%

Der Compiler hat das effektiv umgeschrieben:

%Vor%

... hinein:

%Vor%

... und zur Laufzeit sieht das Stack-Layout für den Aufruf so aus (zeigt den Stack als 32-Bit-Wörter, wobei die Adressen von unten nach oben ansteigen):

%Vor%     
Matthew Slattery 09.10.2010, 21:37
quelle
6

ähnlich wie bei Zahlen größer als 9, mit nur Ziffern 0 - 9. (Verwenden von Positionsziffern). vorausgesetzt, die Frage ist eine konzeptionelle.

    
alvin 10.10.2010 14:57
quelle
3

In Ihrem Fall weiß der Compiler, dass 2 ^ 64-1 nur 0xffffffffffffffff ist, also hat er -1 (niedriges dword) und -1 (hohes dword) als Argument für printf auf den Stapel geschoben. Es ist nur eine Optimierung.

Im Allgemeinen können 64-Bit-Zahlen (und sogar größere Werte) mit mehreren Wörtern gespeichert werden, z. Ein unsigned long long verwendet zwei dword s. Um zwei 64-Bit-Zahlen hinzuzufügen, werden zwei Additionen durchgeführt - eine für die niedrigen 32 Bits und eine für die hohen 32 Bits plus dem Übertrag:

%Vor%

Sie können diese Reihenfolge von add und adc s so oft wiederholen, wie Sie beliebig große Zahlen hinzufügen möchten. Das Gleiche kann mit der Subtraktion gemacht werden - benutze einfach sub und sbb (subtrahiere mit borrow).

Multiplikation und Division sind viel komplizierter, und der Compiler erzeugt normalerweise einige kleine Hilfsfunktionen, um mit diesen umzugehen, wenn Sie 64-Bit-Zahlen zusammen multiplizieren. Pakete wie GMP , die sehr, sehr große Ganzzahlen unterstützen, verwenden SSE / SSE2, um die Dinge zu beschleunigen. Werfen Sie einen Blick auf diesen Wikipedia-Artikel für weitere Informationen zu Multiplikationsalgorithmen.

    
wj32 09.10.2010 21:36
quelle
2

Wie andere darauf hingewiesen haben, wurde das 64-Bit-Aritmetic in Ihrem Beispiel weg optimiert. Diese Antwort konzentriert sich auf die Frage im Titel.

Grundsätzlich behandeln wir jede 32-Bit-Zahl als eine Ziffer und arbeiten in der Basis 4294967296. Auf diese Weise können wir arbiteral große Zahlen bearbeiten.

Addition und Subtraktion sind am einfachsten. Wir arbeiten die Ziffern nacheinander ab, beginnend mit dem am wenigsten signifikanten bis zum signifikantesten. Im Allgemeinen wird die erste Ziffer mit einer normalen Additions- / Subtraktionsanweisung ausgeführt, und spätere Ziffern werden unter Verwendung einer spezifischen Anweisung "add with carry" oder "subtrahiere mit borrow" ausgeführt. Das Übertragsflag in dem Statusregister wird verwendet, um das Übertrag / Borgen-Bit von einer Ziffer zu der nächsten zu nehmen. Dank der Zweierkomplemente sind Vorzeichen und Vorzeichen Addition und Subtraktion gleich.

Die Multiplikation ist ein wenig komplizierter. Wenn Sie zwei 32-Bit-Ziffern multiplizieren, können Sie ein 64-Bit-Ergebnis erzielen. Die meisten 32-Bit-Prozessoren haben Befehle, die zwei 32-Bit-Zahlen multiplizieren und ein 64-Bit-Ergebnis in zwei Registern erzeugen. Die Addition wird dann benötigt, um die Ergebnisse zu einer endgültigen Antwort zusammenzufassen. Dank der Zweier-Komplement-Vorzeichen sind Multiplikationen mit und ohne Vorzeichen gleich, vorausgesetzt, die gewünschte Ergebnisgröße entspricht der Argumentgröße. Wenn das Ergebnis größer ist als die Argumente, ist besondere Vorsicht geboten.

Zum Vergleich gehen wir von der höchstwertigen Ziffer aus. Wenn es gleich ist, gehen wir zur nächsten Ziffer, bis die Ergebnisse gleich sind.

Division ist zu komplex für mich, um in diesem Post zu beschreiben, aber es gibt viele Beispiele für Algorithmen. z.B. Ссылка

Einige Beispiele aus der realen Welt von gcc Ссылка , der Assembler ist in der Intel-Syntax.

Zuerst eine Ergänzung. Hinzufügen von zwei 64-Bit-Zahlen und Erzeugen eines 64-Bit-Ergebnisses. Die ASM ist für signierte und unsignierte Versionen gleich.

%Vor%

Das ist ziemlich einfach, lade ein Argument in eax und edx und füge dann das andere hinzu, indem du ein Add gefolgt von einem Add mit carry verwendest. Das Ergebnis bleibt in eax und edx für die Rückkehr zum Aufrufer.

Jetzt eine Multiplikation von zwei 64-Bit-Zahlen, um ein 64-Bit-Ergebnis zu erzeugen. Auch hier ändert sich der Code nicht von Vorzeichen zu Vorzeichen. Ich habe einige Kommentare hinzugefügt, um das Folgen zu vereinfachen.

Bevor wir uns den Code ansehen, betrachten wir die Mathematik. a und b sind 64-Bit-Zahlen. Ich verwende lo (), um die unteren 32 Bits einer 64-Bit-Zahl darzustellen, und hi (), um die oberen 32 Bits einer 64-Bit-Zahl darzustellen.

(a * b) = (lo (a) * lo (b)) + (hi (a) * lo (b) * 2 ^ 32) + (hi (b) * lo (a) * 2 ^ 32) + (hi (b) * hi (a) * 2 ^ 64)

(a * b) mod 2 ^ 64 = (lo (a) * lo (b)) + (lo (hi (a) * lo (b)) * 2 ^ 32) + (lo (hi (b ) * lo (a)) * 2 ^ 32)

lo ((a * b) mod 2 ^ 64) = lo (lo (a) * lo (b))

hi ((a * b) mod 2 ^ 64) = hi (lo (a) * lo (b)) + lo (hi (a) * lo (b)) + lo (hi (b) * lo (a))

%Vor%

Endlich, wenn wir eine Teilung versuchen, sehen wir.

%Vor%

Der Compiler hat entschieden, dass die Division zu komplex ist, um inline zu implementieren, und ruft stattdessen eine Bibliotheksroutine auf.

    
plugwash 27.07.2016 20:43
quelle
1

Der Compiler hat tatsächlich eine statische Optimierung Ihres Codes vorgenommen. Zeilen # 1 # 2 # 3 sind Parameter für printf ()

    
Nicolas Viennot 09.10.2010 21:20
quelle
1

Wie @Pafy erwähnt, hat der Compiler dies als Konstante ausgewertet.

2 bis 64. minus 1 ist 0xffffffffffffffff .

Wie 2 32-Bit-Ganzzahlen ist dies: 0xffffffff und 0xffffffff , und wenn Sie dies als ein Paar 32-Bit-Typen mit Vorzeichen nehmen, endet als: -1 und -1 .

Der generierte Code entspricht also Ihrem Compiler:

%Vor%

In der Versammlung ist es so geschrieben:

%Vor%

Übrigens, eine bessere Möglichkeit, Potenzen von 2 zu berechnen, ist das Verschieben von 1 links. Z.B. (1ULL << 64) - 1 .

    
asveikau 09.10.2010 21:34
quelle
0

Niemand in diesem Thread bemerkte, dass das OP gebeten hatte, die ersten vier Zeilen zu erklären, nicht die Zeilen 11-14.

Die ersten 4 Zeilen sind:

%Vor%

Folgendes geschieht in den ersten 4 Zeilen:

%Vor%

Dies ist eine Assembler-Anweisung, die besagt, dass wir gerade eine neue logische Datei namens "size.c" starten.

%Vor%

Dies ist auch eine Anweisung für schreibgeschützte Zeichenfolgen im Programm.

%Vor%

Diese Anweisung setzt den Positionszähler immer auf ein Vielfaches von 4.

%Vor%

Dies ist ein Label LC0 , zu dem beispielsweise gesprungen werden kann.

Ich hoffe, ich habe die richtige Antwort auf die Frage gegeben, da ich genau das beantwortet habe, was OP gefragt hat.

    
bodacydo 19.10.2014 03:44
quelle

Tags und Links