antiblock
Cyphriun
  • 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 2)

1 post in this topic

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;
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 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;
// PWord(EDX + ECX * SizeOf(integer))^

No exemplo, presumimos que os elementos têm 2 bytes (movemos o valor de
a 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 := 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.

Share this post


Link to post
Share on other sites
antiblock
Cyphriun

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