Вызов метода объекта с использованием ASM - часть 2 - PullRequest
3 голосов
/ 26 февраля 2012

Этот вопрос основан на предыдущем , но это только к вашему сведению.

Мне удалось заставить его работать, однако я нашел кое-что, что мне не понятно, поэтому, если кто-нибудь сможет объяснить следующее поведение, это было бы здорово.

У меня есть следующий класс:

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;

и следующие две процедуры:

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;

Чтобы протестировать рабочую версию, необходимо позвонить по следующему номеру:

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;

и для проверки НЕ рабочей версии:

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;

И, наконец, вопрос: почему CallObjMethNOTWorking не работает, а CallObjMethWorking работает? Я предполагаю, что в том, как компилятор обрабатывает TMethod, есть что-то особенное ... но поскольку мои знания по сборке ограничены, я не могу этого понять.

Я был бы очень признателен, если бы кто-то мог мне это объяснить, спасибо!

Ответы [ 2 ]

5 голосов
/ 27 февраля 2012

Хенрик Хеллстрем прав в своем ответе , и я замечаю, что ваш вопрос помечен Delphi 2010 и, таким образом, касается только Win32.Однако вам может быть интересно посмотреть, как будет выглядеть ситуация, если вы перейдете к Win64 (Delphi> = XE2), поэтому я добавил пример версии Win64 в код Хенрика:

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;

Необходимо сделать несколько пояснительных замечаний:

1) ASM оператор : в Delphi XE2 x64 нет смешивания кода на языке pascal и asm, поэтому единственный способ написатьассемблерный код находится в подпрограмме, которая состоит из одного блока asm..end, а не begin..end.Обратите внимание, что begin..end вокруг вашего 32-битного кода ASM также имеет эффект.В частности, вы заставляете генерацию стекового фрейма и позволяете компилятору делать локальные копии параметров функции.(Если вы прибегаете к использованию ассемблера, вы, возможно, не захотите, чтобы компилятор делал это.)

2) Соглашение о вызовах : В Win64 существует только один вызовусловность.Такие вещи, как register и stdcall, фактически бессмысленны;это все то же самое, Соглашение Microsoft о вызовах Win64 .По сути, это так: параметры передаются в регистры RCX, RDX, R8 и R9 (и / или XMM0-XMM4, возвращаемые значения в RAX/XMM0. Через 64-битные значения передаются по ссылке.

Вызываемые функции могут использовать: RAX, RCX, RDX, R8-R11, ST(0)-ST(7), XMM0-XMM5, YMM0-YMM5, YMM6H-YMM15H и должны сохранять RBX, RSI, RDI, RBP, R12-R15, XMM6-XMM15. При необходимости вызываемые функции должны выдавать CLD / EMMS / VZEROUPPER инструкции для восстановления ожидаемого ЦПstate.

3) Выравнивание и теневое пространство Важно, что каждая функция имеет свое собственное теневое пространство в стеке, которое равно как минимум 4 параметрам QWORD стекового пространства, даже если их нетparams и независимо от того, касается ли вызываемая функция этого.Более того, на месте каждого вызова функции (в каждом операторе CALL) ожидается, что RSP будет выровнен на 16 байт (то же самое для ESP в MacOSX32, кстати).Это часто приводит к таким вещам, как: sub rsp, ##; call $$; add rsp, ## конструкции, в которых ## будет суммой параметров (QWORD), с которыми должна вызываться функция, плюс дополнительные 8 байтов для выравнивания RSP.Обратите внимание, что выравнивание RSP на сайте CALL приводит к RSP = ###8h при входе в функцию (поскольку CALL помещает адрес возврата в стек), поэтому, если никто не помешает с RSP, прежде чем вы это сделаете, вы можетеожидайте, что это так.

В приведенном примере инструкция SSE2 MOVDQA используется для проверки выравнивания RSP.(XMM5 используется как регистр назначения, потому что он может быть свободно изменен, но не может содержать никаких данных параметров функции).

4) Допущения Код здесь предполагает, что компилятор не вставляет кодизменить RSP.Могут быть ситуации, в которых это может быть неверно, поэтому остерегайтесь делать это предположение.

5) Обработка исключений Обработка исключений в Win64 немного сложна и должна быть правильно выполненакомпилятор (пример кода выше не делает этого).Чтобы компилятор мог это сделать, в идеале ваш код должен использовать новые директивы / псевдоинструкции BASM .PARAMS, .PUSHNV и .SAVENV, обозначенные здесь Алленом Бауэром .При правильной (неправильной) ситуации в противном случае могут произойти плохие вещи.

4 голосов
/ 26 февраля 2012

Соглашение о вызовах по умолчанию в Delphi Win32 - "регистрация". Первый параметр передается в EAX, второй в EDX и третий в ECX. Стек используется только в том случае, если имеется более трех параметров или если переданы типы значений размером более 4 байтов, но это не так в вашем примере.

Ваша первая процедура CallObjMethWorking работает, потому что компилятор уже уже поместил aStrValue в EDX и aIntValue в ECX при вызове CallObjMethWorking. Тем не менее, поскольку вы не очищаете свои две инструкции толчка, при возврате процедуры обязательно произойдет что-то плохое.

Ваш код должен выглядеть следующим образом. В этом случае директива stdcall является необязательной, но было бы неплохо использовать ее для подобных вещей, чтобы убедиться, что ваши параметры не потеряны, потому что вы используете регистры для других целей, прежде чем приступить к вызову метода:

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;
...