Wie viele Bytes drückt der Push-Befehl auf den Stack, wenn ich die Operandengröße nicht festlege?

Ich kann 4 Byte auf den Stack schieben, indem ich Folgendes tue:

push DWORD 123 

Aber ich habe herausgefunden, dass ich push kann push ohne die Operandengröße anzugeben:

 push 123 

Wie viele Bytes drückt die push statement in diesem Fall auf den Stack? Ist die Anzahl der übertragenen Bytes von der Größe des Operanden abhängig (in meinem Beispiel wird also 1 Byte gedrückt)?

   

Die Anzahl der übertragenen Bytes hängt von der Operandengröße ab

Es hängt nicht vom Wert der Zahl ab. Der technische x86 Begriff für wie viele Bytes push Pushs ist “Operand-Größe”, aber das ist eine separate Sache, ob die Zahl passt in eine imm8 oder nicht.

Siehe auch Sendet jede PUSH-statement ein Vielfaches von 8 Bytes auf x64?

(also in meinem Beispiel wird es 1 Byte drücken)?

Nein, die Größe der Sofortnachricht ist nicht die Operandengröße. Es drückt immer 4 Byte in 32-Bit-Code oder 64 in 64-Bit-Code, es sei denn, Sie tun etwas seltsam.

Empfehlung: Schreiben push 123 immer nur push 123 oder push 0x12345 , um die Standard- push Größe für den Modus zu verwenden, in dem Sie sich befinden, und lassen Sie den Assembler die Codierung wählen. Das ist fast immer was du willst. Wenn das alles ist, was Sie wissen wollten, können Sie jetzt aufhören zu lesen.


Zunächst einmal ist es nützlich zu wissen, welche push Größen in x86-Maschinencode überhaupt möglich sind :

  • Im 16-Bit-Modus können Sie 16 oder (mit Operandengrößen-Präfix bei 386 und höher) 32 Bit drücken.
  • Im 32-Bit-Modus können Sie 32 oder (mit Operandengröße) 16 Bit drücken.
  • Im 64-Bit-Modus können Sie 64 oder (mit Operandengrößen-Präfix) 16 Bit drücken.
    Mit einem REX.W = 0-Präfix können Sie keinen 32-Bit-Push codieren. 1

Es gibt keine anderen Optionen. Der Stapelzeiger wird immer um die Operandengröße des Push 2 dekrementiert. (Es ist also möglich, den Stack durch Drücken von 16 Bits “falsch auszurichten”). pop hat die gleichen Auswahlmöglichkeiten: 16, 32 oder 64, außer 32-Bit-Pop im 64-Bit-Modus.

Dies gilt unabhängig davon, ob Sie ein Register oder ein Unmittelbares imm8 und unabhängig davon, ob das Unmittelbare in ein imm8 Vorzeichenerweiterung imm8 oder ob es ein imm32 (oder imm16 für 16-Bit- imm16 ) benötigt. (Ein 64-Bit- push imm32 Zeichen- push imm32 zu 64-Bit. Es gibt keinen push imm64 , nur mov reg, imm64 )

Drücken push 123 im NASM-Quellcode auf die Operandengröße, die mit dem Modus übereinstimmt, in dem Sie sich befinden. In Ihrem Fall denke ich, dass Sie 32-Bit-Code schreiben, also push 123 ist ein 32-Bit-Push, obwohl es kann (und tut) die push imm8 Codierung verwenden.

Ihr Assembler weiß immer, welche Art von Code er zusammenbaut, da er wissen muss, wann Operandengrößen-Präfixe verwendet werden sollen oder nicht, wenn Sie die Operandengröße erzwingen.

MASM ist das gleiche; Die einzige Sache, die anders sein könnte, ist die Syntax zum Erzwingen einer anderen Operandengröße.

Alles, was Sie in Assembler schreiben, wird zu einer der gültigen Maschinencode-Optionen zusammengestellt (weil die Leute, die den Assembler geschrieben haben, wissen, was codiert werden kann und was nicht), also nein, Sie können kein einzelnes Byte mit einer push statement push . Wenn Sie das wollten, könnten Sie es mit dec esp / mov byte [esp], 123 emulieren


NASM Beispiele:

Ausgabe von nasm -l /dev/stdout , um eine Auflistung zusammen mit der ursprünglichen nasm -l /dev/stdout an das Terminal nasm -l /dev/stdout .

Leicht bearbeitet, um Opcode- und Präfix-Bytes von den Operanden zu trennen. (Anders als objdump -drwC -Mintel Disassemblierungsformat keine Leerzeichen zwischen Bytes im Maschinencode-Hexdump).

  68 80000000 push 128 6A 80 push -128 ;; signed imm8 is -128 to +127 6A 7B push byte 123 6A 7B push dword 123 ;; still optimized to the imm8 encoding 68 7B000000 push strict dword 123 6A 80 push strict byte 0x80 ;; will decode as push -128 ****************** warning: signed byte value exceeds bounds [-w+number-overflow] 

dword ist normalerweise eine Operandengröße, während strict dword Anforderung ist, dass der Assembler es nicht für eine kleinere Codierung optimiert.

Alle vorhergehenden statementen sind 32-Bit-Push-Vorgänge (oder 64-Bit im 64-Bit-Modus mit demselben Maschinencode). Alle folgenden statementen sind 16-Bit- 0x660x66 , unabhängig davon, in welchem ​​Modus Sie sie zusammenstellen. (Wenn sie im 16-Bit-Modus zusammengestellt werden, haben sie kein 0x66 Operanden-Präfix)

  66 6A 7B push word 123 66 68 8000 push word 128 66 68 7B00 push strict word 123 

dword scheint NASM die Überschreibungen von byte und dword so zu behandeln, als dword sie auf die Größe des Unmittelbaren dword , aber das word gilt für die Operandengröße des Befehls. Mit o32 push 12 im 64-Bit-Modus wird auch keine Warnung o32 push 12 . push eax tut das allerdings: “error: statement wird im 64-bit Modus nicht unterstützt”.

Beachten Sie, dass push imm8 in allen Modi als 6A ib codiert ist. Ohne Präfix Operandengröße ist die Operandengröße die Größe des Modus. (zB dekodiert 6A FF im Langmodus als 64-Bit Operandengröße mit einem Operanden von -1 , dekrementiert RSP um 8 und macht einen 8-Byte Speicher).


Das Präfix der Adressgröße betrifft nur den expliziten Adressierungsmodus, der für Push mit einer Speicherquelle verwendet wird, z. B. im 64-Bit-Modus: push qword [rsi] (keine Präfixe) vs. push qword [esi] (Präfix der Adressgröße für 32 -Bit Adressierungsmodus). push dword [rsi] ist nicht codierbar, weil nichts die Operandengröße 32-Bit in 64-Bit-Code 1 machen kann . push qword [esi] rsp auf 32-bit ab. Anscheinend ist “Stack Address Width” eine andere Sache, wahrscheinlich in einem Segmentdeskriptor gesetzt. (Es ist immer 64 in 64-Bit-Code auf einem normalen Betriebssystem, ich denke sogar für Linux x32 ABI : ILP32 im langen Modus.)


Wann würdest du jemals 16 Bits drücken wollen? Wenn Sie aus performancesgründen in asm schreiben, dann wahrscheinlich nie . In meinem Code-Golf adler32 brauchte ein schmaler Push -> Wide Pop weniger Bytes Code als Shift / OR, um zwei 16b Integer zu einem 32b Wert zu kombinieren.

Oder vielleicht möchten Sie in einem Exploit für 64-Bit-Code einige Daten lückenlos auf den Stack übertragen. Sie können nicht einfach push imm32 , da dieses Zeichen oder push imm32 auf 64-Bit erweitert wird. Sie könnten dies in 16-Bit-Blöcken mit mehreren 16-Bit-Push-statementen tun. Aber immer noch wahrscheinlich effizienter mov rax, imm64 / push rax (10B + 1B = 11B für eine 8B imm Nutzlast). Oder push 0xDEADBEEF / mov dword [rsp+4], 0xDEADC0DE (5B + 8B = 13B und benötigt kein Register). vier 16-Bit-Pushs würden 16B dauern.


Fußnoten :

  1. Tatsächlich wird REX.W = 0 ignoriert und ändert die Operandengröße nicht von ihrem Standard-64-Bit. NASM, YASM und GAS montieren alle push r12 zu 41 54 , nicht 49 54 . GNU objdjump denkt 49 54 ist ungewöhnlich, und dekodiert es als 49 54 rex.WB push r12 . (Beide führen dasselbe aus). Microsoft stimmt dem ebenfalls zu und verwendet in einigen Windows-DLLs einen push rbx REX als Padding auf push rbx .

    Intel sagt nur, dass 32-Bit-Pushs im Long-Modus “nicht kodierbar” sind (NE in der Tabelle). Ich verstehe nicht, warum W = 1 nicht die Standardcodierung für push / pop wenn ein REX-Präfix benötigt wird, aber anscheinend ist die Wahl willkürlich.

    Fun-fact: Nur Stack-Befehle und einige andere setzen standardmäßig die 64-Bit-Operandengröße im 64-Bit-Modus . add rax, rdx im Maschinencode add rax, rdx benötigt ein REX-Präfix (mit gesetztem W-Bit). Sonst würde es als add eax, edx . Aber Sie können die Operandengröße nicht mit einem REX.W=0 verringern, wenn sie standardmäßig auf 64-Bit gesetzt ist, sondern nur erhöhen, wenn sie auf 32 REX.W=0 .

    http://wiki.osdev.org/X86-64_Instruction_Encoding#REX_prefix listet die statementen auf, die standardmäßig 64-Bit im 64-Bit-Modus verwenden. Beachten Sie, dass jrcxz nicht streng in diese Liste gehört, da das von ihm überprüfte Register (cx / ecx / rcx) von der Adressgröße und nicht von der Operandengröße bestimmt wird, so dass es auf 32-Bit (aber nicht auf 16-Bit) überschrieben werden kann. Bit) im 64-Bit-Modus. loop ist gleich.

    Es ist merkwürdig, dass Intels Handbuch mit dem Referenzhandbuch für push (HTML-Auszug: http://felixcloutier.com/x86/PUSH.html ) zeigt, was bei einem 32-Bit Operand-Push im 64-Bit-Modus passieren würde (der einzige Fall) wo Stack-Adressbreite 64 sein kann, so verwendet es rsp ). Vielleicht ist es irgendwie mit einigen nicht standardmäßigen Einstellungen in dem Code-Segment-Deskriptor erreichbar, so dass Sie es nicht in normalem 64-Bit-Code ausführen können, der unter einem normalen Betriebssystem ausgeführt wird. Oder wahrscheinlicher ist es ein Versehen, und das würde passieren, wenn es kodierbar wäre, aber das ist es nicht.

  2. Außer Segment-Register sind 16-Bit, aber eine normale push fs wird immer noch den Stack-pointers um die Stack-Breite (Operand-Größe) dekrementieren. Intel dokumentiert, dass neuere Intel-CPUs in diesem Fall nur einen 16-Bit-Speicher bereitstellen und den Rest der 32- oder 64-Bit-Version unverändert belassen.

    x86 hat offiziell keine Stapelbreite , die in der Hardware erzwungen wird. Es ist ein Software- / Aufrufkonvention-Begriff, zB werden char und short Argumente, die auf dem Stapel in irgendwelchen Aufrufkonventionen übergeben werden, auf 4B oder 8B aufgefüllt, so dass der Stapel ausgerichtet bleibt. (Moderne 32- und 64-Bit-Aufrufkonventionen wie das x86-32-System-V-psABI, das von Linux verwendet wird, halten den Stapel 16B vor functionsaufrufen ausgerichtet, obwohl ein Arg- “Schlitz” auf dem Stapel immer noch nur 4B ist). Wie auch immer, “Stapelbreite” ist nur eine Programmierkonvention für jede Architektur.

    Die nächste Sache in der x86-ISA zu einer “Stapelbreite” ist die Standard-Operandengröße von push / pop . Aber Sie können den Stack-Pointer beliebig manipulieren, zB sub esp,1 . Sie können, aber nicht aus performancesgründen: P

Die “Stapelbreite” in einem Computer, die die kleinste Menge von Daten ist, die auf den Stapel geschoben werden kann, ist als die Registergröße des processors definiert. Dies bedeutet, dass bei einem processor mit 16-Bit-Registern die Stack-Breite 2 Byte beträgt. Wenn der processor über 32-Bit-Register verfügt, beträgt die Stapelbreite 4 Byte. Wenn der processor über 64-Bit-Register verfügt, beträgt die Stapelbreite 8 Byte.

Seien Sie nicht verwirrt, wenn Sie moderne x86 / x86_64-Systeme verwenden. Wenn das System in einem 32-Bit-Modus läuft, beträgt die Stapelbreite und Registergröße 32 Bit oder 4 Byte. Wenn Sie in den 64-Bit-Modus wechseln, ändert sich die Größe von Register und Stack.