Social Icons

^^

segunda-feira, 11 de janeiro de 2010

Assembler nativo em Delphi - Records

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 estão anexados.

Nenhum comentário:

Postar um comentário

Popular Posts

- Arquivo -

 

Seguidores

Hora exata:

Total de visualizações de página