V¡®u§ 849 Posted January 7, 2012 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