2015-12-05 14 views
6

Ich habe eine einfache Montage Programm geschrieben:Warum wird der Wert von EDX beim Aufruf von printf überschrieben?

section .data 
str_out db "%d ",10,0 
section .text 
extern printf 
extern exit 
global main 
main: 

MOV EDX, ESP 
MOV EAX, EDX 
PUSH EAX 
PUSH str_out 
CALL printf 
SUB ESP, 8 ; cleanup stack 
MOV EAX, EDX 
PUSH EAX 
PUSH str_out 
CALL printf 
SUB ESP, 8 ; cleanup stack 
CALL exit 

Ich bin der NASM Assembler und der GCC die Objektdatei in eine ausführbare Datei auf Linux zu verknüpfen.

Im Wesentlichen setzt dieses Programm zuerst den Wert des Stapelzeigers in das Register EDX und druckt dann den Inhalt dieses Registers zweimal. Nach dem zweiten printf-Aufruf stimmt der Wert für das stdout jedoch nicht mit dem ersten überein.

Dieses Verhalten scheint seltsam. Wenn ich jede Verwendung von EDX in diesem Programm durch EBX ersetze, sind die ausgegebenen Ganzzahlen wie erwartet identisch. Ich kann nur folgern, dass EDX irgendwann während des printf-Funktionsaufrufs überschrieben wird.

Warum ist das der Fall? Und wie kann ich sicherstellen, dass die Register, die ich in Zukunft benutze, nicht mit den C-lib-Funktionen kollidieren?

+2

Das hat mich auch vor Jahren das erste Mal bekommen. Die Antwort, die Sie angenommen haben, ist korrekt, lässt aber "ebp" und "esp" als Aufrufer weg. Diese beiden scheinen selbstverständlich zu sein, aber Sie können das technisch durcheinander bringen. Willkommen in der Montage! – sqykly

+0

@sqykly Danke. Es ist sicherlich viel weniger nachsichtig als die höheren Sprachen, die ich gewohnt bin. Aber ich werde nicht davon besiegt werden! :) – Jake

+0

Beantworten Sie so viele Javascript-Fragen wie ich und Sie werden sich darüber wundern. – sqykly

Antwort

11

Nach dem x86 ABI, EBX, ESI, EDI und EBP sind Rufenen-save-Register und EAX, ECX und EDX sind Anrufer-Save-Register.

Dies bedeutet, dass Funktionen die vorherigen Werte EAX, ECX und EDX frei verwenden und zerstören können. Sichern Sie deshalb vor dem Aufruf von Funktionen die Werte EAX, ECX, EDX, wenn sich deren Werte nicht ändern sollen. Es ist was "caller-save" bedeutet.

Oder besser, verwenden Sie andere Register für Werte, die Sie nach einem Funktionsaufruf noch benötigen. Push/Pop von EBX am Anfang/Ende einer Funktion ist viel besser als Push/Pop von EDX innerhalb einer Schleife, die einen Funktionsaufruf macht. Wenn möglich, verwenden Sie Call-Clob-Register für Temporary, die nach dem Aufruf nicht benötigt werden. Werte, die bereits im Speicher sind, also nicht geschrieben werden müssen, bevor sie erneut gelesen werden, sind auch billiger zu verschütten.


Seit EBX, ESI, EDI und EBP sind Rufenen Spar Register, haben Funktionen, die Werte auf die ursprüngliche für alle diejenigen, die sie ändern, vor der Rückkehr wieder herzustellen.

ESP wird auch auf Abruf gespeichert, aber Sie können dies nicht durcheinander bringen, es sei denn, Sie kopieren die Absenderadresse irgendwo. Nicht übereinstimmender Call/ret ist für die Leistung schlecht, weil moderne CPUs einen Rücksprungadressen-Prädiktor verwenden.

+2

'EBP' ist auch callee-save! –

+0

Es ist nicht * das * schwer. 'ret 8' von einer Funktion ohne Parameter versagt' esp'. Stellen Sie fest, dass irgendeine Art von Tail-Call-Optimierung fehlgeschlagen ist. – sqykly

+0

Oder! Cdecl oder Stdcall falsch angewendet. – sqykly

5

Die ABI für die Zielplattform (z. B. 32bit x86 Linux) definiert, welche Register von Funktionen verwendet werden können, ohne zu speichern. (Wenn Sie möchten, dass sie während eines Anrufs erhalten bleiben, müssen Sie dies selbst tun).

Links zu ABI docs für Windows und Nicht-Fenster, 32 und 64-Bit, bei https://stackoverflow.com/tags/x86/info

einige Registern, die über Anrufe (erhältlich als Scratch-Register) nicht beibehalten werden, bedeuten Funktionen kleiner sein können. Einfache Funktionen können oft vermeiden, push/pop speichern/Wiederherstellungen. Dies verringert die Anzahl der Anweisungen und führt zu schnellerem Code.

Es ist wichtig, einige von jedem zu haben: den ganzen Staat in den Speicher über Anrufe verschütten würde den Code von Nicht-Blatt-Funktionen aufgebläht, und verlangsamen Dinge vor allem. in Fällen, in denen die aufgerufene Funktion nicht alle Register berührt hat.

+0

Dieser letzte Absatz klingt komisch. Wenn Sie den gesamten Speicher speichern müssen, sind die Blattfunktionen genau die, die er aufblähen würde. Die Nicht-Blatt-Funktionen tun im Wesentlichen Blähungen in beide Richtungen, da sie sowohl ein Anrufer als auch ein Angerufener sind. –

+0

@DanielStevens: Der letzte Absatz spricht über den Fall, dass alle Register geplottert sind, wie die xmm-Regs sind in der SysV 64bit ABI. Blattfunktionen müssen nichts speichern. Außerdem: Nicht-Blatt-Funktionen haben oft genug callee-gespeicherte Register, um ein paar wichtige Teile des Zustands in regs zu halten, und verwendeten hauptsächlich die Anrufer-speichern Regs als Scratch-Space, um die Funktion-Aufruf-Parameter zu berechnen. Sie müssen nur ein reg speichern/wiederherstellen, wenn Sie es nach dem Funktionsaufruf noch benötigen. Normalerweise brauchen Sie ein Paar, das wie ein Schleifenzähler und ein Zeiger oder zwei denkt, aber andere Sachen neu laden kann. –

+0

Aber du hast davon geredet, den ganzen Staat in Erinnerung zu behalten, damit er nicht verprügelt wird. –