2012-02-26 2 views
3

Этот вопрос основан на previous, но это только FYI.Метод вызова объекта с использованием ASM - Часть 2

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

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

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; 

и для тестирования NOT рабочий вариант иона:

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 ... но поскольку мои знания сборки ограничены, я не могу этого понять.

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

+0

3 параметра передаются в регистры. Я не знаю, куда вступает игра. Вы прочитали раздел управления программой руководства по delphi? –

+0

@ Давид, я просто сделал, кажется, что если я позвоню PUSH EAX, MOV EDX, DWORD PTR AStrValue; MOV ECX, DWORD PTR AIntValue; CALL ACode; он работает отлично, за исключением того, что он вызывает исключение (после выполнения) ..., я понимаю, что строка уже передана по ссылке и целое значение по значению, но я до сих пор не получаю ее ... – ComputerSaysNo

+0

почему вы толкаете EAX? И зачем вам нужно использовать метод asm для вызова метода? –

ответ

4

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

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

Ваш код должен выглядеть следующим образом. В этом случае директива 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; 
+0

Спасибо, Хенрик, только что протестирован, и вы правы, можете ли вы более конкретно сказать о «инструкциях по очистке толчка»? – ComputerSaysNo

+0

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

+0

большое спасибо! – ComputerSaysNo

5

Henrick Hellström правильно со своим answer, и я замечаю, что ваш вопрос помечен Delphi 2010 и, таким образом, относится только к Win32. Тем не менее, вы можете быть заинтересованы, чтобы увидеть, что ситуация будет выглядеть, если вы двигаетесь вперед к Win64 (Delphi> = XE2), поэтому я добавил пример Win64 версию кода 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; 

Есть несколько пояснительных отмечает сделать:

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

2) Вызов конвенции: На Win64 есть только одно соглашение о вызовах. Такие вещи, как register и stdcall, фактически бессмысленны; это все равно, Microsoft's Win64 calling convention. Это, по существу, следующим образом: параметры передаются в 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 инструкции для восстановления CPU в ожидаемом состоянии.

3) Выравнивание и тени пространства Importantl y, каждая функция имеет свое собственное теневое пространство в стеке, которое, как минимум, имеет значение пространства стека QWORD param, даже если нет параметров и независимо от того, действительно ли вызываемая функция действительно затрагивает его. Более того, на сайте каждого вызова функции (в каждом заявлении 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, как указано в Allen Bauer here. Учитывая правильную (неправильную) ситуацию, плохие вещи могут произойти иначе.

+0

+1 спасибо за ваше очень глубокое объяснение, еще один вопрос, на x86 нам нужно переместить aInstance в EAX, однако на x64 (RCX = EAX (?)) AInstance уже в RCX, потому что это первый параметр? – ComputerSaysNo

+0

@Dorin - да, причина в RCX заключается в том, что это первый параметр – PhiS

+0

спасибо, сэр, теперь жизнь имеет смысл! (: – ComputerSaysNo