V¡®u§ 849 Posted January 7, 2012 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. 1 Big_Z reacted to this Share this post Link to post Share on other sites