Social Icons

^^

segunda-feira, 15 de agosto de 2011

Delphi com assembly (Parte 3)

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]
  +----------------+
  |                |

Nenhum comentário:

Postar um comentário

Popular Posts

 

Seguidores

Hora exata:

Total de visualizações de página