Chiama il metodo dell'oggetto usando ASM - Parte 2
-
13-11-2019 - |
Domanda
Questa domanda si basa su a precedente, ma questo è solo FYI.
Sono riuscito a farlo funzionare, tuttavia, ho trovato qualcosa che non mi è chiaro, quindi se qualcuno può spiegare il seguente comportamento, sarebbe fantastico.
Ho la seguente classe:
type
TMyObj = class
published
procedure testex(const s: string; const i: integer);
end;
procedure TMyObj.testex(const s: string; const i: integer);
begin
ShowMessage(s + IntToStr(i));
end;
e le seguenti due procedure:
procedure CallObjMethWorking(AMethod: TMethod; const AStrValue: string; const AIntValue: Integer);
begin
asm
PUSH DWORD PTR AIntValue;
PUSH DWORD PTR AStrValue;
CALL AMethod.Code;
end;
end;
procedure CallObjMethNOTWorking(AInstance, ACode: Pointer; const AStrValue: string; const AIntValue: Integer);
begin
asm
MOV EAX, AInstance;
PUSH DWORD PTR AIntValue;
PUSH DWORD PTR AStrValue;
CALL ACode;
end;
end;
Per testare la versione funzionante, è necessario chiamare quanto segue:
procedure ...;
var
LObj: TMyObj;
LMethod: TMethod;
LStrVal: string;
LIntVal: Integer;
begin
LObj := TMyObj.Create;
try
LMethod.Data := Pointer( LObj );
LMethod.Code := LObj.MethodAddress('testex');
LStrVal := 'The year is:' + sLineBreak;
LIntVal := 2012;
CallObjMethWorking(LMethod, LStrVal, LIntVal);
finally
LObj.Free;
end; // tryf
end;
e per testare il NON Versione funzionante:
procedure ...;
var
LObj: TMyObj;
LCode: Pointer;
LData: Pointer;
LStrVal: string;
LIntVal: Integer;
begin
LObj := TMyObj.Create;
try
LData := Pointer( LObj );
LCode := LObj.MethodAddress('testex');
LStrVal := 'The year is:' + sLineBreak;
LIntVal := 2012;
CallObjMethNOTWorking(LData, LCode, LStrVal, LIntVal);
finally
LObj.Free;
end; // tryf
end;
E infine la domanda: perché non lo è Callobjmethnotworking Lavorare, mentre Callobjmethworking è? Immagino che ci sia qualcosa di speciale nel modo in cui il compilatore tratta TMethod ... ma poiché la mia conoscenza dell'assemblea è limitata, non riesco a capirlo.
Apprezzerei molto se qualcuno potesse spiegarmi questo, grazie!
Soluzione
La convenzione di chiamata predefinita a Delphi Win32 è "Register". Il primo parametro viene passato in EAX, il secondo in EDX e il terzo in ECX. Lo stack viene utilizzato solo se ci sono più di tre parametri o se vengono superati i tipi di valore maggiori di 4 byte, ma non è così nel tuo esempio.
La tua prima procedura di callobjmethworking funziona perché il compilatore ha già Posizionato Astrvalue in EDX e Aintvalue in ECX quando si chiamava callobjmethworking. Tuttavia, dal momento che non stai ripulendo le tue due istruzioni di spinta, le cose cattive sono destinate a verificarsi quando la procedura ritorna.
Il tuo codice dovrebbe apparire così. La direttiva STDCall è facoltativa in questo caso, ma potrebbe essere una buona idea usarla per cose come questa per assicurarsi che i tuoi parametri non siano persi perché usi i registri per altri scopi prima di andare in giro per chiamare effettivamente il metodo:
procedure CallObjMeth(AInstance, ACode: Pointer; const AStrValue: string; const AIntValue: Integer); stdcall;
asm
MOV EAX, AInstance;
MOV EDX, DWORD PTR AStrValue;
MOV ECX DWORD PTR AIntValue;
CALL ACode;
end;
Altri suggerimenti
Henrick Hellström ha ragione con il suo Rispondere, e noto che la tua domanda è taggata con Delphi 2010 e quindi riguarda solo Win32. Tuttavia, potresti essere interessato a vedere come sarebbe la situazione se andassi avanti a Win64 (Delphi> = XE2), quindi ho aggiunto una versione di Win64 di esempio al codice di Henrick:
procedure CallObjMeth(AInstance, ACode: Pointer; const AStrValue: string; const AIntValue: Integer); stdcall;
asm
{$IFDEF CPU386}
MOV EAX, AInstance;
MOV EDX, DWORD PTR AStrValue;
MOV ECX, DWORD PTR AIntValue;
{$IFDEF MACOS}
//On MacOSX32 ESP = #######Ch here
SUB ESP, 0Ch
{$ENDIF}
CALL ACode;
{$IFDEF MACOS}
ADD ESP, 0Ch // restoring stack
{$ENDIF}
{$ENDIF}
{$IFDEF CPUX64}{$IFDEF WIN64} // <- see comments
.NOFRAME //Disable stack frame generation
//MOV RCX, AInstance {RCX} //<- not necessary because AInstance already is in RCX
MOV R10, ACode {RDX}
MOV RDX, AStrValue {R8}
MOV R8D, AIntValue {R9D}
SUB RSP, 28h //Set up stack shadow space and align stack: 4*8 bytes for 4 params + 8 bytes bytes for alignment
{$IFNDEF DO_NOT_TEST_STACK_ALIGNMENT}
MOVDQA XMM5, [RSP] //Ensure that RSP is aligned to DQWORD boundary -> exception otherwise
{$ENDIF}
CALL R10 //ACode
ADD RSP, 28h //Restore stack
{$ENDIF}{$ENDIF}
end;
Esistono diverse note esplicative da prendere:
1) ASM
dichiarazione: In Delphi XE2 X64 non c'è miscelazione di codice Pascal e ASM, quindi l'unico modo per scrivere il codice di assemblaggio è in una routine che è composta da un singolo asm..end
Blocco, no begin..end
. Nota che il begin..end
Intorno al tuo codice ASM a 32 bit ha anche un effetto. In particolare, stai forzando la generazione di una cornice dello stack e lascia che il compilatore effettui copie locali dei parametri della funzione. (Se ricorri all'utilizzo dell'assemblaggio in primo luogo, potresti non vorre che il compilatore lo facesse.)
2) Chiamare Convenzione: Su Win64, c'è solo una singola convenzione di chiamata. Cose come register
e stdcall
sono effettivamente insignificanti; E 'tutto lo stesso, WIN64 di Microsoft WIN64 Convention. È essenzialmente questo: i parametri vengono passati RCX
, RDX
, R8
e R9
registri (e/o XMM0-XMM4
, restituire i valori in RAX/XMM0
. I valori più grandi di 64 bit vengono passati tramite riferimento.
Le funzioni chiamate possono usare: RAX, RCX, RDX, R8-R11, ST(0)-ST(7), XMM0-XMM5, YMM0-YMM5, YMM6H-YMM15H
, e deve preservare RBX, RSI, RDI, RBP, R12-R15, XMM6-XMM15
. Se del caso, le funzioni chiamate devono emettere CLD
/EMMS
/VZEROUPPER
Istruzioni per ripristinare la CPU allo stato previsto.
3) Allineamento e spazio ombraÈ importante sottolineare che ogni funzione ha il suo spazio ombra sullo stack, che è almeno 4 anni di spazio dello stack di Qword Param, anche se non ci sono parametri e indipendentemente dal fatto che la funzione chiamata lo tocchi effettivamente. Inoltre, nel sito di ciascuna funzione di funzione (a ciascuno CALL
dichiarazione), RSP
dovrebbe essere allineato a 16 byte (lo stesso per ESP
su macOSX32, btw.). Questo spesso porta a cose come: sub rsp, ##; call $$; add rsp, ##
costrutti in cui ## sarebbe la somma dei parametri (Qword) con cui la funzione deve essere chiamata, oltre a 8 byte opzionali per l'allineamento di RSP
. Si noti che l'allineamento di RSP
al CALL
si traduce il sito RSP = ###8h
All'ingresso della funzione (perché CALL
mette l'indirizzo di ritorno sullo stack), quindi supponendo che nessuno si prenda RSP
Prima di farlo, puoi aspettarti che sia quello.
Nell'esempio fornito, SSE2 MOVDQA
l'istruzione viene utilizzata per testare l'allineamento di RSP
. (XMM5
viene utilizzato come registro di destinazione perché può essere liberamente modificato ma non può contenere dati dei parametri della funzione).
4) ipotesiIl codice qui presuppone che il compilatore non inserisca il codice per modificare RSP
. Potrebbero esserci situazioni in cui ciò potrebbe non essere vero, quindi fai attenzione a fare questo presupposto.
5) Gestione delle eccezioni La gestione delle eccezioni in Win64 è un po 'complicata e dovrebbe essere eseguita correttamente dal compilatore (il codice di esempio sopra non lo fa). Per consentire al compilatore di farlo, idealmente il codice dovrebbe utilizzare le nuove direttive BASM/istruzioni pseudo- .PARAMS
, .PUSHNV
e .SAVENV
come delineato da Allen Bauer qui. Data la situazione giusta (sbagliata), altrimenti potrebbero accadere cose cattive.