Social Icons

^^

segunda-feira, 15 de agosto de 2011

Delphi com assembly (Parte 2)

Passando arrays estáticas como parâmetros
=========================================


Parâmetros de arrays estáticas são passados como ponteiros ao primeiro
elemento do array, independentemente do parâmetro ser passado por valor
ou por referência (como "var" ou como "const").

Dadas as seguintes declarações...

  const
    ARRAY_MAX = 5;

  type
    TArrayOfInt = packed array [0..ARRAY_MAX] of longint;

  var
    a, b: TArrayOfInt;

  procedure InitializeArray(var a: TArrayOfInt);
  var
    i: integer;
  begin
    for i := 0 to ARRAY_MAX do
      a[i] := i;
  end;

... a chamada à procedure InitializeArray em assembler seria:

    // Em Object Pascal:
    //   InitializeArray(a);
    // Em Assembler Inline:
    asm
      mov eax, offset a        // EAX := @a;
      call InitializeArray     // InitializeArray;
    end;

OFFSET é um operador unário assembler que retorna o endereço de um
símbolo. O OFFSET não é aplicável para símbolos locais. Deve-se usar o
opcode LEA (veja abaixo), que é mais "universal".


Arrays estáticas passadas por valor
-----------------------------------


Se o array é passado por valor, é responsabilidade da função chamada
preservar o array. Quando a função precisa mudar os valores de um ou
mais elementos de um array passado por valor, normalmente ela cria uma
cópia local e trabalha com a cópia. O compilador cria uma cópia para nós
no "begin" das procedures e funções Pascal, mas em procedures e funções
em assembler puro temos de fazer isto nós mesmos. Um modo de se fazer
isto seria o seguinte:

  procedure OperateOnArrayPassedByValue(a: TArrayOfInt);
  var
    _a: TArrayOfInt;
  asm
    // Copia os elementos de "a" (parâmetro) em "_a" (cópia local)
    push esi                      // Salva ESI na pilha
    push edi                      // Salva EDI na pilha
    mov esi, eax                  // ESI := EAX; // @a
    lea edi, _a                   // EDI := @_a;
    mov eax, edi                  // EAX := EDI; // @_a
    mov ecx, type TArrayOfInt     // ECX := sizeof(TArrayOfInt);
    rep movsb                     // Move(ESI^, EDI^, ECX);
    pop edi                       // Restaura EDI da pilha
    pop esi                       // Restaura ESI da pilha

    // Aqui vai o resto da função. Trabalharemos sobre o "_a" (a
    // cópia local), cujo primeiro elemento está agora apontado por EAX.
  end;

O que encontramos de novo aqui são os opcods LEA e MOVSB, o prefixo REP
e o operador TYPE, descritos abaixo:


LEA (Load Effective Address)
----------------------------


Move para o primeiro operando o endereço do segundo. Aqui comparamos LEA
com MOV:

   Instrução             Traduzida como         Efeito
  -------------------------------------------------------------------

   lea eax, localvar     lea eax, [ebp-$04]     EAX := @localvar;
                                                EAX := EBP - $04;

   mov eax, localvar     mov eax, [ebp-$04]     EAX := localvar;
                                                EAX := (EBP - $04)^;


MOVSB (MOVe String Byte)
------------------------


Copia o byte apontado por ESI ao local apontado por EDI, e incrementa
ESI e EDI de tal forma que eles apontem para o próximo byte. O trabalho
do MOVSB pode ser descrito como segue:

  ESI^ := EDI^;    // Assume que ESI e EDI são do tipo PChar
  Inc(ESI);
  Inc(EDI);

Notas:

 * MOVSW e MOVSD são as versões Word (16-bit) e DWord (32-bit)
   respectivamente (ESI e EDI são incrementadas de 2 e 4
   respectivamente).

 * Os registradores são decrementados se o Direction Flag é setado.


REP
---


O prefixo REP é usado em operações de string para repetir a operação
de decremento ECX até que ECX seja zero. O trabalho do REP poderia ser
descrito como segue:

  // rep string_instruction

  @@rep:
    string_instruction
    loop @@rep

Notas:

* O REP não é um atalho para um código como o acima. Ele trabalha muito
  mais rápido.

* O valor de ECX não é checado no começo do loop (se ECX fosse zero, a
  instrução seria repetida 2^32 vezes, mas geraria um extenso AV antes
  disto, tão logo ESI ou EDI apontassem para uma localição de memória
  inválida).


TYPE
----


O operador TYPE é um operador unário avaliado em tempo de compilação que
retorna o tamanho em bytes de um operando, que deve ser um tipo de dados.
Por exemplo, TYPE WORD retornará 2 e TYPE INTEGER retornará 4.


Acessando os elementos de um array
==================================


Para acessar um elemento a[i] precisamos dos valores "@a[0]" e "i" nos
registradores (como EDX e ECX, por exemplo) para então podermos usar o
endereçamento de memória como segue:

  lea edx, a                      // EDX := @a;
  mov ecx, i                      // ECX := i;
  mov ax, [edx+ecx*type integer]  // AX := EDX[ECX];  // a[i];
       // PWord(EDX + ECX * SizeOf(integer))^

No exemplo, presumimos que os elementos têm 2 bytes (movemos o valor de
a[i] para AX, um registrador de 16 bits), que a array não é agrupada
(cada elemento realmente ocupa 4 bytes, o tamanho de um inteiro, logo
este valor foi usado para calcular a posição do elemento) e que o array
não começa pelo elemento 0, ou seja, não é um array "zero-based". Por
exemplo:

    var a: array [0..N] of word = (1, 2, 3, 6, ...);

    +------ EDX = @a
    |
    v
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+--
  | 1 | 0 |   |   | 2 | 0 |   |   | 3 | 0 |   |   | 6 | 0 |   |   |
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+--
    a[0]             a[1]            a[2]            a[3]
    [edx]          [edx+04]        [edx+08]        [edx+12]

Se o array não é zero-based, temos que ajustar o valor do índice para
torná-lo zero-based antes de endereçarmos o elemento. Exemplos:

  // a[1..100]
  :
  mov ecx, i                      // ECX := i;
  dec ecx                         // Dec(ECX); // Ajusta ECX
  :

  // a[-10..10]
  :
  mov ecx, i                      // ECX := i;
  add ecx, 10                     // Inc(ECX, 10); // Ajusta ECX
  :

A procedure InitializeArray (apresentada acima) pode ser implementada em
assembler do seguinte modo:

  procedure InitializeArray(var a: TArrayOfInt);
  asm                                // EAX = PByte(@a[0]);
    xor ecx, ecx                     // ECX := 0;
  @@loop:
    mov [eax+ecx*type integer], ecx  // PInteger(EAX+ECX*4)^ := ECX;
                                     //  ...or  EAX[ECX] := ECX;
    inc ecx                          // ECX := ECX + 1;
    cmp ecx, ARRAY_MAX               // if ECX <= ARRAY_MAX then
    jle @@loop                       //   goto @@loop;
  end;

Ou assim:

  procedure InitializeArray(var a: TArrayOfInt);
  asm                      // EAX = @a[0];
    xor ecx, ecx           // ECX := 0;
  @@loop:
    mov [eax], ecx         // EAX^ := ECX;
    inc ecx                // Inc(ECX);
    add eax, type integer  // Inc(EAX); // Aponta para o próximo elemento
    cmp ecx, ARRAY_MAX     // if ECX <= ARRAY_MAX then
    jle @@loop             //   goto @@loop;
  end;


Valores de retorno de arrays
============================


As funções que retornam arrays recebem um último parâmetro adicional que
é o ponteiro para a locação de memória onde deveam colocar seu valor de
retorno (a memória é alocada e liberada se necessário por quem acessou).
Por exemplo, consideremos a seguinte função:

  function ReverseArray(const a: TArrayOfInt): TArrayOfInt;
  var
    i: integer;
  begin
    for i := 0 to ARRAY_MAX do
      Result[i] := a[ARRAY_MAX-i];
  end;

A função recebe dois parâmetros:

  1) EAX = endereço do primeiro elemento da array "a"
  2) EDX = endereço do primeiro elemento de Result

A função pode ser reescrita em assembler como segue:

  function ReverseArray(const a: TArrayOfInt): TArrayOfInt;
  asm                                // EAX = @a[0]; EDX = @Result[0];
    push ebx                         // Save EBX
    mov ebx, eax                     // EBX := EAX;
    xor ecx, ecx                     // ECX := 0;
  @@loop:
    mov eax, ARRAY_MAX
    sub eax, ecx                     // EAX := ARRAY_MAX-ECX;
    mov eax, [ebx+eax*type integer]  // EAX := EBX[EAX];
    mov [edx+ecx*type integer], eax  // EDX[ECX] := EAX;
    inc ecx                          // ECX := ECX + 1;
    cmp ecx, ARRAY_MAX               // if ECX <= ARRAY_MAX then
    jle @@loop                       //   goto @@loop;
    pop ebx                          // Restore EBX
  end;

Bem, isto é tudo por enquanto. No próximo artigo veremos como trabalhar
com registros.


Passando records como parâmetros
================================


Como arrays estáticos, records são internamente passados como ponteiros
para os dados, independentemente se o parâmetro é passado por valor ou
por referência (também como "var" ou como "const").

Dada as seguintes declarações...

  type
    TRecord = record
      Id: integer;
      Name: string;
    end;

  var
    a, b: TRecord;

  procedure InitRecord(var r: TRecord; Id: integer; const Name: string);
  begin
    r.Id := Id;
    r.Name := Name;
  end;

...uma chamada para a procedure InitRecord em assembler seria assim:

    // Em Object Pascal:
    //   InitRecord(a, n, s);
    // Em Inline Assembler:
    asm
      lea eax, a               // EAX := @a; // 1st parameter in EAX
      mov edx, n               // EDX := n;  // 2nd parameter in EDX
      mov ecx, s               // ECX := s;  // 3rd parameter in ECX
      call InitRecord          // InitRecord;
    end;


Acessando os campos de um record
================================


Campos de records estão localizados em um certo offset de um endereço do
record (o endereço do primeiro campo). No exemplo, assumindo que nós
temos o endereço do record do tipo TRecord no registrador EAX, o campo
Id está localizado em [EAX+0] (ou simplesmente [EAX]), e o campo Name
está localizado em [EAX+4], mas normalmente nós não escrevemos código
usando números explicitamente. Ao invés disto, para produzir código
auto-explicável e de fácil manutenção, nós temos cinco alternativas:

  mov edx, [eax + TRecord.Name]
  mov edx, (TRecord PTR [eax]).Name
  mov edx, (TRecord [eax]).Name
  mov edx, TRecord[eax].Name
  mov edx, [eax].TRecord.Name

As cinco sentenças anteriores seriam montadas como:

  mov edx, [eax + 4]

No lugar de um registrador (como EAX), as sintaxes também se aplicam
para nome de variáveis locais ou globais.

Você pode deduzir da primeira sintaxe que em inline assembler a
expressão RecordType.Field é avaliada em tempo de compilação como
uma constante representando o offset no qual o campo está localizado
no RecorType. Por exemplo, a seguinte sentença é válida:

  mov ecx, TRecord.Name   // mov ecx, 4

Voltando ao assunto, a procedure InitRecord (apresentada acima) pode ser
implementada em assembler desta forma:

  procedure InitRecord(var r: TRecord; Id: integer; const Name: string);
  asm // EAX = @r;  EDX = Id;  ECX = @Name[1]
    mov (TRecord PTR [eax]).Id, edx    // EAX^.Id := EDX;  // Id
    // _LStrAsg(@EAX^.Name, @Name) --> EAX^.Name := Name
    lea eax, (TRecord PTR [eax]).Name  // EAX := @(EAX^.Name);
    mov edx, ecx                       // EDX := @Name[1];
    call System.@LStrAsg               // _LStrAsg(EAX, EDX)
  end;

Na entrada da procedure, nós temos EAX apontando para o registro
(primeiro parâmetro), EDX contendo o Id (segundo parâmetro), EDX
apontando para o dado da string Name (terceiro parâmetro). Atribuir
um inteiro é bem simples, mas atribuir uma string é um pouco mais
complicado.

  Se a string destino não é uma string vazia então
  begin
    Decremente a contagem de referência da string destino;
    Se a contagem de referência da string destino chegou a zero então
      Libere a string destino;
  end;

  Se a String Origem não for uma string vazia então
    Incremente a contagem de referência da String origem;
  Designe origem para o destino;

A procedure _LStrAsg (da Unit System) implementa esta lógica para nós. A
procedure recebe dois parâmetros: o primeiro (em EAX) é a string destino
passada por referência e o segundo (em EDX) é a string origem passada
por valor (o que é passado na verdade é o ponteiro, visto que strings
são ponteiros para os caracteres de fato). Então, no nosso caso, EAX
deveria ser o endereço de uma variável string que será atribuída (isto é
@r.Name), enquanto EDX deveria ser o valor a ser atribuído:

    EAX  --> r.Name --> r.Name[1]   ==>   EAX = @r.Name
    EDX  --> Name[1]                ==>   EDX = @Name[1]

    Ref.: "-->" significa "aponta para"

Então, preparamos EAX e EDX e então chamamos _LStrAsg:

    lea eax, (TRecord PTR [eax]).Name  // EAX := @(EAX^.Name);
    mov edx, ecx                       // EDX := @Name[1];
    call System.@LStrAsg               // _LStrAsg(EAX, EDX)


Funções de baixo nível para trabalhar com records
=================================================


Como arrays estáticos, se o record é passado por valor, é
responsabilidade da função chamada preservar o record. Quando uma função
precisa trocar o valor de um ou mais campos do record passado por valor,
normalmente ela cria uma cópia local e trabalha com a cópia. O
compilador cria uma cópia para nós no "begin" das funções Pascal, mas
nas funções puramente assembler temos que fazê-lo nós mesmos. Um jeito
de fazer isto é como mostrado na parte III com arrays estáticos. Aqui
está outro modo:

  procedure OperateOnRecordPassedByValue(r: TRecord);
  var
    _r: TRecord;
  asm
    // Copia os elementos de "r" (parâmetros) em "_r" (cópia local)
    // Move(r, _r, sizeof(TRecord));

    lea edx, _r                   // EDX := @_r;
    mov ecx, type TRecord         // ECX := sizeof(TRecord);
    call Move                     // Move(EAX^, EDX^, ECX);

    lea eax, _r                   // EAX := @_r;
    mov edx, TRecord_TypeInfo     // EDX := TRecord_TypeInfo;
    call System.@AddRefRecord     // System._AddRefRecord(EAX,EDX);

    lea eax, _r                   // EAX := @_r; // optional

    // Aqui vai o resto da função. Nós trabalharemos no
    // record "_r" (a cópia local), agora apontada por EAX.
  end;

Desta vez nós chamamos a procedure Move ao invés de copiarmos os dados
com REP MOVSB. Deste modo, nós escrevemos menos código.

IMPORTANTE: Copiar os valores da memória apenas funciona com records que
não contém campos do tipo reference-counted tais como strings, arrays
dinâmicos ou variantes do tipo string ou arrays dinâmicos.

Se nós tivermos um ou mais campos string, ou campos de algum outro tipo
reference-counted, depois de copiar os valores de memória, nós temos que
incrementar seus respectivos contadores de referência. A procedure
_AddRefRecord (da Unit System) realiza isto. Ela possui dois parâmetros:
um ponteiro para o record (em EAX) e um ponteiro para informação do tipo
de dado para o record gerado pelo compilador (em EDX).

A informação de tipo para o record é basicamente uma estrutura de dados
que contém as posições e tipos de campos reference-counted do registro.
As procedures que trabalham com records declaradas na Unit System,
(_InitializeRecord, _AddRefRecord, _CopyRecord, e _FinalizeRecord)
requerem um ponteiro para a informação do tipo de dado como seu último
parâmetro.

Mas, onde estão os dados? Bem, infelizmente, não há um símbolo para
acessar sua localização diretamente. Nós temos que conseguir seu
endereço através de uma chamada para a função TypeInfo, mas não há uma
função que nós possamos chamar através do código assembler porque não é
uma função verdadeira, e sim uma função interna que o compilador resolve
em tempo de compilação.

Um possível contorno é inicializar uma variável global, chamando a
função TypeInfo de nosso código Pascal:

  var
    TRecord_TypeInfo: pointer;

  :

  initialization
    TRecord_TypeInfo := TypeInfo(TRecord);

E então podemos usá-la como:

  procedure OperateOnRecordPassedByValue(r: TRecord);
  var
    _r: TRecord;
  asm
    // Copiar os elementos de "r" (parâmetro) para "_r" (cópia local)
    // Move(_r, r, sizeof(TRecord));
    lea edx, _r                   // EDX := @_r;
    mov ecx, TYPE TRecord         // ECX := sizeof(TRecord);
    call Move                     // Move(EAX^, EDX^, ECX);
    // System._AddRefRecord(@_r, TypeInfo(TRecord));
    lea eax, _r                   // EAX := @_r;
    mov edx, TRecord_TypeInfo     // EDX := TypeInfo(TRecord);
    call System.@AddRefRecord     // System._AddRefRecord(EAX, EDX);

    lea eax, _r                   // EAX := @_r; // opcional

    // Aqui vai o resto da função. Nós trabalharemos no
    // record "_r" (a cópia local), agora apontada em EAX.

    // Nós temos que finalizar a cópia local antes de retornarmos
    // System._FinalizeRecord(@_r, TypeInfo(TRecord));
    lea eax, _r                   // EAX := @_r;
    mov edx, TRecord_TypeInfo     // EDX := TypeInfo(TRecord);
    call System.@FinalizeRecord   // System._FinalizeRecord(EAX, EDX);
  end;


Note que antes da função retornar, nós temos que fazer a chamada a
_FinalizeRecord para destruir o record local (por exemplo, isto
decrementará a contagem de referência de strings apontadas por campos
string).

Chamar Move e então _AddRefRecord é um jeito válido de copiar records
se e apenas se o record de destino tenha sido inicializado (depois de
chamar _AddRefRecord, o record é inicializado). Se o record de destino
já estiver inicializado, então nós temos que chamar _CopyRecord ao invés
disto.

Por Exemplo:

  procedure proc(const r: TRecord);
  var
    _r: TRecord;
  begin
    // _r := r;
    asm
      mov edx, eax                // EDX := @r;
      lea eax, _r                 // EAX := @_r;
      mov ecx, TRecord_TypeInfo   // ECX := TypeInfo(TRecord);
      call System.@CopyRecord     // System._CopyRecord(EAX, EDX, ECX);
    end;
  end;

Note que como isto é uma função Pascal normal (não uma função Assembler
completa), o compilador automaticamente gera código para inicializar
e finalizar a variável record local (no "begin" e "end" da procedure
respectivamente).

A combinação Move mais _AddRefRecord é idêntica em efeito a
_InitializeRecord mais _CopyRecord:

  procedure OperateOnRecordPassedByValue(r: TRecord);
  var
    _r: TRecord;
  asm
    // Copiar os elementos de "r" (parâmetro) para "_r" (cópia local)
    // Move(_r, r, sizeof(TRecord));
    // System._InitializeRecord(@_r, TypeInfo(TRecord));
    push eax                      // Push(EAX); // @r
    lea eax, _r                   // EAX := @_r;
    mov edx, TRecord_TypeInfo     // EDX := TypeInfo(TRecord);
    call System.@InitializeRecord // System._InitializeRecord(EAX, EDX);
    // _r := r;
    lea eax, _r                   // EAX := @_r;
    pop edx                       // EDX := Pop(); // @r
    mov ecx, TRecord_TypeInfo     // EDX := TypeInfo(TRecord);
    call System.@CopyRecord       // System._CopyRecord(EAX, EDX, ECX);

    lea eax, _r                   // EAX := @_r; // optional

    // Aqui vai o resto da função. Nós trabalharemos no
    // record "_r" (a cópia local), agora apontada em EAX.

    // Nós temos que finalizar a cópia local antes de retornarmos
    // System._FinalizeRecord(@_r, TypeInfo(TRecord));
    lea eax, _r                   // EAX := @_r;
    mov edx, TRecord_TypeInfo     // EDX := TypeInfo(TRecord);
    call System.@FinalizeRecord   // System._FinalizeRecord(EAX, EDX);
  end;

Como _AddRefRecord, a procedure _InitializeRecord é apenas destinada
para ser usada com records não inicializados.


Retornando valores de records
=============================


Retornar valores de records é exatamente o mesmo que retornar valores de
array estático. Funções que retornam records recebem um último parâmetro
adicional que é o ponteiro para a localização em memória onde o valor de
retorno deve ser armazenado, isto é, o valor do último parâmetro é
@Result. A memória para o record de resultado deveria ser alocada,
inicializada e liberada pelo chamador (não é de responsabilidade da
função chamada). Por exemplo, vamos considerar a seguinte função:

  function MakeRecord(Id: integer; const Name: string): TRecord;
  begin
    Result.Id := Id;
    Result.Name := Name;
  end;

A função é declarada para receber dois parâmetros e retornar um record,
mas internamente é como uma procedure com três parâmetros:

  1) EAX = O Id para o novo record
  2) EDX = O nome para o novo record
  3) ECX = O endereço do record de resultado (@Result)

A função pode ser reescrita em assembler como segue :

  function MakeRecord(Id: integer; const Name: string): TRecord;
  asm // EAX = Id;  EDX = @Name[1];  ECX = @Result
    mov (TRecord PTR [ecx]).Id, eax    // ECX^.Id := EAX;  // Id
                                       // (@Result)^.Id := EAX;
                                       // Result.Id := EAX;
    // Result.Name := Name;
    // System.@LStrAsg(@(Result.Name), @Name[1])
    // System.@LStrAsg(@(ECX^.Name), @Name[1])
    lea eax, (TRecord PTR [ecx]).Name  // EAX := @(ECX^.Name);
    call System.@LStrAsg               // _LStrAsg(EAX, EDX)
  end;

  NOTA: Nós não designamos o valor EDX antes de chamar _LStrAsg
        porque EDX já contém o valor desejado (passado como parâmetro)


Chamando funções que retornam records
=====================================


Considere o seguinte código:

  a := MakeRecord(n, s);

Alguém seria tentado a pensar que o compilador traduz como:

  asm
    mov eax, n
    mov edx, s
    lea ecx, a   // ECX := @a;  // @Result
    call MakeRecord
  end;

Mas as coisas não acontecem deste jeito, ao menos no Delphi 5. O
compilador aloca e inicializa uma variável local que armazena o
resultado e então copia o resultado do record para o record de destino.
Nós não temos apenas ineficiência realizando uma cópia que seria
desnecessária se usássemos um código como o acima, mas- como nós temos
visto acima- a cópia por si mesma não é tão inocente como uma chamada
para a procedure Move (_CopyRecord checa a informação de tipo de dado
em runtime para localizar os campos que requerem tratamento especial).
É claro, a variável local invisível é primeiro inicializada e
eventualmente finalizada. Este modo é extremamente ineficiente. Se
você precisa de velocidade, chame funções record-returning usando
assembler como mostrado acima, passando diretamente o endereço da
variável que irá guardar o resultado como o último parâmetro (@Result).

Bem, é isto por enquanto. Na próxima parte, veremos algumas coisas
básicas sobre o trabalho com objetos.

__________________

NOTA: O Código fonte e a aplicação DEMO estaram anexados no último post desse artigo.

Nenhum comentário:

Postar um comentário

Popular Posts

 

Seguidores

Hora exata:

Total de visualizações de página