antiblock
https://i.imgur.com/aJ17bf7.gif
  • Chatbox

    Did you check out our Discord? https://discord.gg/FFdvMjk9xA
    You don't have permission to chat.
    Load More
Sign in to follow this  
V¡®u§

Delphi com assembly (Parte 3)

1 post in this topic

Objetos são registros

=====================

Do ponto de vista do assembler, um objeto é como um registro, cujos

campos são seus próprios campos mais os campos de seus ancestrais,

mais um ponteiro à VMT (Virtual Methods Table - Tabela de Métodos

Virtuais). Vejamos isto através de um exemplo:

type

TClass1 = class

FieldA: integer;

FieldB: string;

end;

TClass2 = class(TClass1)

FieldC: integer;

end;

No exemplo, TClass2 é de certo modo como um registro com quatro

campos:

TClass2 = record

VMT: pointer; // campo invisível, sempre o primeiro

FieldA: integer; // herdado de TClass1

FieldB: string; // herdado de TClass1

FieldC: integer; // declarado em TClass2

end;

Variáveis objeto são ponteiros

==============================

Uma variável objeto é somente um ponteiro para um objeto, ou seja, um

ponteiro para um registro.

var

a, b: TClass2;

begin

a := TClass2.Create;

b := a; // somente uma declaração de ponteiro

a.Free;

end;

Um construtor aloca memória para uma instância (objeto) de sua classe,

inicializa-a e retorna um ponteiro para a memória alocada. Assim,

após a chamada a TClass.Create a variável "a" aponta para o registro

(o objeto):

+---+ +--------+

| a | ----------> | VMT |

+---+ +--------+

| FieldA |

+--------+

| FieldB |

+--------+

| FieldC |

+--------+

A declaração "b := a" não cria um novo objeto, cópia do primeiro,

mas realmente faz com que ambas as variáveis apontem para o mesmo

objeto:

+---+ +--------+ +---+

| a | ----------> | VMT | <---------- | b |

+---+ +--------+ +---+

| FieldA |

+--------+

| FieldB |

+--------+

| FieldC |

+--------+

Métodos assembler

=================

Os métodos recebem um primeiro parâmetro invisível, chamado Self, que

é um ponteiro para o objeto sobre o qual o método deve operar.

type

TTest = class

FCode: integer;

public

procedure SetCode(NewCode: integer);

end;

procedure TTest.SetCode(NewCode: integer);

begin

FCode := NewCode;

end;

var

a: TTest;

begin

:

a.SetCode(2);

:

end;

O código em Objetc Pascal acima é traduzido para para o Pascal padrão

como segue:

type

TTest = record

VMT: pointer;

FCode: integer;

end;

procedure SetCode(Self: TTest; NewCode: integer);

begin

Self.FCode := NewCode;

end;

var

a: ^TTest;

begin

:

SetCode(a, 2);

:

end;

O exemplo serve para explicar que os métodos recebem o ponteiro Self

como seu primeiro parâmetro, ou seja, eles recebem o ponteiro Self no

registrador EAX e o primeiro parâmetro declarado é passado como um

segundo parâmetro em EDX etc. (o segundo parâmetro declarado é passado

como terceito em ECX e o resto dos parâmetros são passados em pilha).

O método SetCod pode ser escrito em assembler como:

procedure TTest.SetCode(NewCode: integer);

asm

// EAX = Self = endereço da instância TTest

// EDX = parâmetro NewCode

// FCode := NewCode;

mov TTest[eax].FCode, edx // TTest(EAX)^.FCode := EDX;

end;

Como se pode ver, os campos de objeto são acessados da mesma forma que

os campos de registro.

NOTA: Propriedades não são campos e não podem ser acessadas diretamente

a partir do assembler inline.

Eis um exemplo de um método chamando outro método:

procedure TTest.Increment;

asm

// SetCode(Code+1);

mov edx, TTest[eax].FCode // ECX := TTest(EAX)^.FCode;

inc edx

call TTest.SetCode;

end;

Não fixamos o valor de EAX antes de fazer a chamada já que EAX já

contém o valor desejado (Self), assim o método chamado vai operar no

mesmo objeto.

NOTAS:

* Métodos virtuais podem ser chamados somente estaticamente, já que

uma referência à classe é necessária na declaração de chamada.

* Métodos sobrecarregados não podem ser distinguidos em assembler

inline.

Construtores assembler

======================

Construtores são métodos muito especiais. Os construtores podem ser

chamados para criar uma instância de uma classe (isto é, para alocar

a memória para o objeto e inicializá-lo), ou simplesmente para

reinicializar um objeto já criado:

a := TTest.Create; // aloca memória

a.Create; // apenas reinicializa um objeto existente

Para distinguir entre estas duas situações, os construtores passam

um segundo parâmetro invisível do tipo byte (ou seja, no registrador

DL) que pode ser positivo ou negativo respectivamente (o compilador

usa 1 e -1 respectivamente).

Se temos de chamar um construtor a partir do código assembler com

DL = $01 (para alocar memória para o objeto), temos de passar uma

referência à classe em EAX. Já que não há nenhum símbolo para acessá-lo

diretamente do assembler, temos que fazer algo similar ao que fizemos

com o tipo de informação dos registros:

var

TTest_TypeInfo: pointer;

:

initialization

TTest_TypeInfo := TTest;

Agora que inicializamos uma variável global com a referência à classe a

partir do nosso código Pascal, podemos usa-la em nosso código assembler:

var

a: TTest;

begin

// a := TTest.Create(2);

asm

mov eax, TTest_TypeInfo

mov dl, 1

mov ecx, 2

call TTest.Create

mov a, eax

end;

:

end;

Chamar um construtor para reinicializar o objeto é mais simples já que

não precisamos de uma referência à classe:

var

a: TTest;

begin

:

// a.Create(2);

asm

mov eax, a

mov dl, -1

mov ecx, 2

call TTest.Create

end;

:

end;

Não temos nada com que nos preocupar se temos que escrever um construtor

assembler já que o Delphi manuseia a alocação para nós na entrada do

construtor e, após isso, o registrador EAX aponta para o objeto, como

acontece com qualquer outro método. O que é relevante é que se o

construtor tem parâmetros, o primeiro parâmetro declarado será

internamente passado como terceito, ou seja, em ECX (ao invés de

segundo, em EDX, como acontece com outros métodos) e o resto dos

parâmetros serão passados em ordem, na pilha.

constructor TTest.Create(NewCode: integer);

asm

// FCode := NewCode

mov TTest[eax].FCode, ecx

end;

__________________

NOTA: Um exemplo com código fonte completo está anexado

Funções API e a convenção de chamada Stdcall

============================================

As funções API são chamadas de forma transparente a partir do assembler

nativo com a declaração CALL. Contudo, devemos levar em consideração que

passar parâmetros para funções API é diferente já que elas normalmente

usam o convenção de chamada Stdcall, ao invés da convenção de chamada

Register, que é a que vimos em várias ocasiões anteriores já que ela é a

convenção padrão.

Na convenção de chamada Stdcall, todos os parâmetros são passados para a

pilha, da direita para a esquerda, ou seja, o último parâmetro (o mais à

direita) é enviado primeiro e o primeiro (o mais à esquerda) é enviado

por último, e, portanto, ele será o primeiro no topo da pilha. Eis um

exemplo de um procedimento que chama uma função API:

procedure HideForm(Handle: THandle);

// Windows.ShowWindow(Handle, SW_HIDE);

asm

push SW_HIDE // push 0 // passa o segundo parâmetro

push Handle // push eax // passa o primeiro parâmetro

call Windows.ShowWindow // chama a API ShowWindow

end;

Se temos que chamar um método que usa a convenção Stdcall, devemos

lembrar que o ponteiro Self é o primeiro parâmetro invisível, logo ele

será passado por último na pilha.

Se temos que escrever funções que usam a convenção Stdcall, não há nada

de especial para nos preocuparmos. O compilador sempre criará uma pilha

e referências aos nomes dos parâmetros serão convertidas em endereços

relativos ao ponteiro base:

function AddAndMultiply(i1, i2, i3: integer): integer; stdcall;

asm // ==> push ebp; mov ebp, esp

// Result := (i1 + i2) * i3;

mov eax, i1 // mov eax, [ebp+8]

add eax, i2 // add eax, [ebp+12]

imul i3 // imul [ebp+16]

end; // ==> pop ebp; ret 12

Este é um exemplo de chamada para a função:

asm

// a := AddAndMultiply(1, 2, 3); // o resultado seria 9

push 3

push 2

push 1

call AddAndMultiply

mov a, eax

end;

Após a entrada na função, a pilha se pareceria com isto:

| |

+----------------+

| Old EBP | [EBP], [ESP]

+----------------+

| Return Address | [EBP+4]

+----------------+

| i1 = 1 | [EBP+8]

+----------------+

| i2 = 2 | [EBP+12]

+----------------+

| i3 = 3 | [EBP+16]

+----------------+

| |

Bibliotecas C/C++ e a convenção de chamada Cdecl

================================================

Algumas vezes precisamos acessar funções em arquivos objeto (.OBJ),

bibliotecas estáticas (.LIB) ou bibliotecas dinâmicas (.DLL) escritas

em C ou C++ e, muito freqüentemente, estas funções usam a convenção de

chamada Cdecl. Ela é muito parecida com a convenção Stdcall, mas a pilha

deve ser limpa por quem a chama, isto é, quem a chama deve invocar os

parâmetros que ela inicia ou- ainda melhor- incrementar o ponteiro da

pilha.

function AddAndMultiply(i1, i2, i3: integer): integer; cdecl;

asm // ==> push ebp; mov ebp, esp

// Result := (i1 + i2) * i3;

mov eax, i1 // mov eax, [ebp+8]

add eax, i2 // add eax, [ebp+12]

imul i3 // imul [ebp+16]

end; // ==> pop ebp; ret

Note-se no comentário da última linha que a função não move o ponteiro

da pilha como ocorreu no exemplo anterior que usou a convenção Stdcall;

logo, quem chamar esta função é que será o responsável por isso. Este é

um exemplo de chamada para esta função:

asm

// a := AddAndMultiply(1, 2, 3); // seria 9

push 3

push 2

push 1

call AddAndMultiply

add esp, 12 // limpa a pilha

mov a, eax

end;

Note-se que se os parâmetros fossem do tipo Byte ao invés de Integer,

moveríamos ainda o ponteiro da pilha de 12 bytes já que cada parâmetro

usaria 32 bits (4 bytes) nos dois casos.

A convenção de chamada Pascal

=============================

Muitos programadores C/C++ preferem a convenção de chamada Pascal ao

invés da Cdecl porque ela é mais compacta e também mais rápida já que a

função chamada limpa a pilha na declaração RET, como ocorre na convenção

Stdcall. A convenção Pascal é como a Stdcall, mas os parâmetros são

passados da esquerda para a direita ao invés da direita para a esquerda,

isto é, o primeiro parâmetro (o mais a esquerda) é passado primeiro e o

último (o mais a direita) é passado por último:

function AddAndMultiply(i1, i2, i3: integer): integer; pascal;

asm // ==> push ebp; mov ebp, esp

// Result := (i1 + i2) * i3;

mov eax, i1 // mov eax, [ebp+16]

add eax, i2 // add eax, [ebp+12]

imul i3 // imul [ebp+8]

end; // ==> pop ebp; ret 12

Note-se como os endereços dos parâmetros são traduzidos de modo

diferente que nos exemplos anteriores.

Este é um exemplo de chamada desta função:

asm

// a := AddAndMultiply(1, 2, 3); // seria 9

push 1

push 2

push 3

call AddAndMultiply

mov a, eax

end;

Após a chamada da função, a pilha se pareceria com isto:

| |

+----------------+

| EBP | [EBP], [ESP]

+----------------+

| Return Address | [EBP+4]

+----------------+

| i3 = 3 | [EBP+8]

+----------------+

| i2 = 2 | [EBP+12]

+----------------+

| i1 = 1 | [EBP+16]

+----------------+

| |

Share this post


Link to post
Share on other sites
antiblock
https://arwen2.global/

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this