Question

Cette question est basée sur un précédent, mais c'est juste pour info.

J'ai réussi à le faire fonctionner, cependant, j'ai trouvé quelque chose qui ne m'est pas clair, donc si quelqu'un peut expliquer le comportement suivant, ce serait génial.

J'ai la classe suivante:

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;

et les deux procédures suivantes:

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;

Afin de tester la version de travail, il faut appeler ce qui suit:

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;

et pour tester le NE PAS Version de travail:

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;

Et enfin la question: pourquoi pas Callobjmethnotworking travailler, tandis que Callobjmethworking est? Je suppose qu'il y a quelque chose de spécial dans la façon dont le compilateur traite TMethod ... mais comme mes connaissances de l'assemblage sont limitées, je ne peux pas la comprendre.

J'apprécierais beaucoup si quelqu'un pouvait m'expliquer cela, merci!

Était-ce utile?

La solution

La convention d'appel par défaut dans Delphi Win32 est "Registre". Le premier paramètre est passé dans EAX, le second dans EDX et le troisième dans ECX. La pile n'est utilisée que s'il y a plus de trois paramètres, ou si des types de valeur supérieurs à 4 octets sont passés, mais ce n'est pas le cas dans votre exemple.

Votre première procédure d'appel de callobjmeth fonctionne parce que le compilateur a déjà placé Astvalue dans Edx et AintValue dans ECX lorsque Callobjmethworking a été appelé. Cependant, comme vous ne nettoyez pas vos deux instructions de poussée, les mauvaises choses se produisent lorsque la procédure revient.

Votre code devrait ressembler à ceci. La directive STDCALL est facultative dans ce cas, mais il pourrait être une bonne idée de l'utiliser pour des choses comme celle-ci pour vous assurer que vos paramètres ne sont pas perdus parce que vous utilisez les registres à d'autres fins avant de vous déplacer pour appeler réellement la méthode:

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;

Autres conseils

Henrick Hellström a raison avec son réponse, et je remarque que votre question est marquée avec Delphi 2010 et ne concerne donc que Win32. Cependant, vous pourriez être intéressé de voir à quoi ressemblerait la situation si vous avancez à Win64 (Delphi> = XE2), j'ai donc ajouté un exemple de version Win64 au code d'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;

Il y a plusieurs notes explicatives à prendre:

1) ASM déclaration: Dans Delphi Xe2 x64, il n'y a pas de mélange de code Pascal et ASM, donc la seule façon d'écrire du code d'assemblage est dans une routine qui est composée d'un seul asm..end bloc, non begin..end. Notez que le begin..end Autour de votre code ASM 32 bits a également un effet. Plus précisément, vous forçant la génération d'un cadre de pile et laissez le compilateur faire des copies locales des paramètres de fonction. (Si vous recourez à l'utilisation de l'assemblage en premier lieu, vous ne voudriez peut-être pas que le compilateur le fasse.)

2) Appeler la convention: Sur Win64, il n'y a qu'une seule convention d'appel. Des choses comme register et stdcall sont effectivement dénués de sens; c'est tout de même, Convention d'appel Win64 de Microsoft. C'est essentiellement ceci: les paramètres sont passés dans RCX, RDX, R8 et R9 registres (et / ou XMM0-XMM4, Valeurs de retour dans RAX/XMM0. Des valeurs supérieures à 64 bits sont passées par référence.

Les fonctions appelées peuvent utiliser: RAX, RCX, RDX, R8-R11, ST(0)-ST(7), XMM0-XMM5, YMM0-YMM5, YMM6H-YMM15H, et doit conserver RBX, RSI, RDI, RBP, R12-R15, XMM6-XMM15. Le cas échéant, les fonctions appelées doivent émettre CLD/EMMS/VZEROUPPER Instructions pour restaurer le CPU à l'état attendu.

3) Alignement et espace d'ombreSurtout, chaque fonction a son propre espace d'ombre sur la pile, qui est au moins 4 Qword Param d'espace de pile, même s'il n'y a pas de paramètres et indépendamment du fait que la fonction appelée le touche réellement. De plus, sur le site de chaque appel de fonction (à chacun CALL déclaration), RSP devrait être aligné de 16 octets (même pour ESP sur macOSX32, btw.). Cela conduit souvent à des choses comme: sub rsp, ##; call $$; add rsp, ## Constructions dans lesquelles ## serait la somme des paramètres (qword) avec lesquels la fonction doit être appelée, plus 8 octets facultatifs pour l'alignement de RSP. Notez que l'alignement de RSP au CALL Le site entraîne RSP = ###8h Lors de l'entrée de la fonction (parce que CALL met l'adresse de retour sur la pile), alors en supposant que personne ne gâche avec RSP Avant de le faire, vous pouvez vous attendre à ce que ce soit cela.

Dans l'exemple fourni, le SSE2 MOVDQA L'instruction est utilisée pour tester l'alignement de RSP. (XMM5 est utilisé comme registre de destination car il peut être librement modifié mais ne peut contenir aucune donnée de paramètre de fonction).

4) HypothèsesLe code ici suppose que le compilateur n'inserte pas le code pour changer RSP. Il peut y avoir des situations dans lesquelles cela peut ne pas être vrai, alors méfiez-vous de faire cette hypothèse.

5) Gestion des exceptions La gestion des exceptions dans Win64 est un peu compliquée et doit être correctement effectuée par le compilateur (l'exemple de code ci-dessus ne le fait pas). Pour permettre au compilateur de le faire, idéalement, votre code devrait utiliser les directives BASM / pseudo-instructions BASM .PARAMS, .PUSHNV et .SAVENV comme indiqué par Allen Bauer ici. Compte tenu de la bonne (mauvaise) situation, de mauvaises choses pourraient se produire autrement.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top