V¡®u§ 849 Posted January 7, 2012 Este artigo irá introduzir os conceitos de assembler em linha (inline assembler) no Delphi. O artigo dará uma noção básica do assunto mas não pretende oferecer, em hipótese alguma, detalhes da programação assembler que, por si só, precisariam de um livro inteiro ou mais... Por que e Quando ================ Se você der uma olhada no código fonte da RTL e da VCL, você encontrará declarações assembler inline em vários pontos. Por que a Borland optou por escrever partes do código da RTL e da VCL em assembler? A resposta é bem simples: para alcançar velocidade na execução. Nós sabemos que o compilador produz código rápido mas um compilador jamais será tão bom quanto um programador assembler profissional. Agora, se o assembler é tão bom, por que não foi utilizado em toda a RTL e VCL? A resposta é igualmente simples: porque na programação de mais alto nível, é mais fácil codificar, depurar, ler e manter o código, de modo que o sacrifício em velocidade fica compensado pelas conveniências decorrentes. Isso ajuda a explicar quando o assembler deve ser utilizado. Para ser curto, além do acesso ao sistema em baixo nível, o assembler inline deve ser utilizado quando a diferença na velocidade de execução justifica o trabalho adicional da codificação em assembler. Por exemplo, na unidade Math.pas, há muito assembler, basicamente para chamadas de sistema em baixo nível (para acesso às funções do coprocessador); em System.pas, SysUtils.pas e Classes.pas há também diversos blocos em assembler, desta vez para priorizar velocidade; no é estranho já que essas podem ser consideradas as unidades centrais da RTL e VCL. Em geral, procedimentos e funções que tendem a ser chamadas de forma repetida por um programa devem ser altamente otimizadas, mas codificação em assembler deve ser evitada tanto quanto possível. Se desejamos ganhos em velocidade, antes de optar por assembler devemos otimizar o algoritmo propriamente dito; depois, otimizamos o código Pascal. Se optarmos por assembler, o código Pascal otimizado pode servir como documentação e pode ser utilizado como "código de contigência" no caso de problemas com a manutenção do código assembler. Os Registradores da CPU ======================= Os registradores da CPU são como variáveis predefinidas residindo na CPU e, por vezes, têm tarefas especiais. Eles não têm tipo e podem ser vistos como inteiros de 32 bits com ou sem sinal ou como ponteiros, dependendo da situação. Como estão na própria CPU, é muito mais rápido acessar valores contidos nos registradores do que na memória, fazendo dos registros ideais para fazer cache de valores. Como variáveis, os registradores também possuem nomes. Os nomes daqueles que usaremos são EAX, EBX, ECX, EDX, ESI, EDI, EBP e ESP. Cada registrador tem uma particularidade que o distingue dos demais: - Para algumas instruções, a CPU foi otimizada para utilizar o registrador EAX (também conhecido como acumulador) ou ao menos os opcodes são menores. EAX é usado nas multiplicações e as divisões, intructions de string, instruções de I/O, instruções de ajuste ASCII e decimal, e em algumas instruções especiais (como CDQ, LAHF, SAHF e XLAT). - EBX é um registrador de uso geral, e é usado implicitamente por XLAT. - ECX (também conhecido como contador) tem emprego especial nas instruções LOOP, de rotação e deslocamento de bits e de manipulação de literais. - EDX é utilizado para armazenar os 32 bits mais altos do resultado de uma multiplicação ou os os 32 bits mais altos do dividendo e do resto de uma divisão. - ESI e EDI (conhecidos como índice de origem (source index) e índice de destino ("destination index") respectivamente) são como ponteiros utilizados em instruções envolvendo strings. - EBP (conhecido como ponteiro base) é normalmente usado para endereçar valores na pilha (parâmetros e variáveis locais). - ESP (conhecido como ponteiro da pilha) é utilizado para controlar a pilha. é alterado automaticamente por instruções como PUSH, POP, CALL e RET. Os registradores EBX, ESI, EDI, EBP e ESP devem ser preservados, o que significa que antes de usá-los, devemos salvar seus valores em algum lugar (normalmente na pilha ou outro registradores) e, quando terminarmos de usá-los, devemos restaurar seus valores originais (essas operações implicam no uso de instruções e perda de algum tempo) de modo que o uso desses registradores será feito somente quando justificável ou quando houver uma necessidade inevitável. Provavelmente você percebeu que os nomes dos registradores iniciam com a letra "E". O "E" representa "Extended", estendido. Nos tempos do Intel 80286, os registradores tinham 16 bits e eram chamados AX, BX, CX, etc. Esses registradores ainda existem e são exatamente os 16 bits menos significativos dos registradores EAX, EBX, ECX, etc., respectivamente. A propósito disso, os registradores AX, BX, CX e DX são divididos em dois registradores de 8 bits. AL, BL, CL e DL são os bytes menos significativos de AX, BX, CX e DX respectivamente, enquanto AH, BH, CH e DH são os bytes mais significativos de AX, BX, CX e DX respectivamente. Por exemplo, se o valor de EAX é $7AFD503C, então o valor de AX é $503C, o valor de AH é $50 e o valor de AL é $3C: 7A FD 50 3C AH AL /----/ AX /------------/ EAX Se, na situação acima, armazenarmos o valor $99 em AH, então EAX passaria a ter o valor $7AFD993C. Existe um registrador especial, o registrador de indicadores (flags), que armazena indicadores binários alterados por instruções matemáticas e lógicas ou explicitamente por código, e que são normalmente usados em instruções de desvio condicional. O indicador carry também é usado em algumas instruções de rotação e o indicador de direção é utilizado em instruções envolvendo literais. Esse registrador não é acessível por nome como os demais registradores; mas pode ser copiado e restaurado através da pilha, utilizando PUSHF e POPF respectivamente, e pode também ser copiado e restaurado parcialmente através do registrador AH, utilizando LAHF e SAHF respectivamente. Instruções Assembler ==================== Instruções assembler são dispostas em blocos asm..end blocks e têm a seguinte forma: [identificador:] [prefixo] opcode [operando1 [, operando2 [, ...]]] Onde opcode é o nome da instrução como MOV, ADD, PUSH, etc. Instruções podem ser separadas por ponto e vírgula, quebras de linhas ou comentários. A propósito, comentários são no formato do Object Pascal, isto é, o ponto e vírgula não é considerado o início de um comentário até o final da linha, como no assembler tradicional. A seguir, um exemplo de bloco asm..end com vários dos possíveis tipos de instruções e separadores de comentários: asm xchg ebx, edx; add eax, [ebx]; {ponto e vírgula separa declaração} // quebra de linha separa declaração mov ebx, p sub eax, [ebx] (*comentário separa declaração*) mov ebx, edx end; A convenção é utilizar quebras d elinhas para separação: asm xchg ebx, edx add eax, [ebx] mov ebx, p sub eax, [ebx] mov ebx, edx end; No código da VCL, você verá que os opcodes e nomes de registradores são escritos em maiúsculas e que instruções são indentadas em uma tabulação (normalmente equivalente a oito caracteres), mas utilizaremos outra convenção neste artigo. Blocos asm..end podem ocorrer em qualquer ponto do código fonte onde uma declaração Pascal ordinária puder aparecer; além disso, é possível termos rotinas 100% assembler se, ao invés de "begin", utilizarmos "asm": procedure teste; asm // declarações assembler end; Note que as duas implementações abaixo não são equivalentes: function f(parâmetros): tipo; begin asm // declarações assembler end; end; function f(parâmetros): tipo; asm // declarações assembler end; A razão disso é que o compilador realiza certas otimizações quando implementamos rotinas inteiramente em assembler, sem utilizar um bloco begin..end. As etiquetas devem ser declaradas em uma seção Label, como em qualquer código Object Pascal, a menos que foram prefixadas por "@@": function ENumeroMagico(x: integer): boolean; asm cmp eax, NumeroMagico je @@Bingo xor eax, eax ret @@Bingo: mov eax, 1 end; As etiquetas prefixadas por "@@" são locais ao bloco asm..end em que são usadas. Isto gerará um erro da compilaçao: begin .... asm .... @@destino: .... end; .... asm .... jnz @@destino // Error .... end; .... end; Para corrigi-lo, necessitamos usar uma etiqueta convencional, local ao procedimento ou à função: label destino; begin .... asm .... destino: .... end; .... asm .... jnz destino // Correto .... end; .... end; Operandos ========= Certas vezes, um ou mais operandos são implícitos. Por exemplo, a instrução CDQ (Converta Dword para Qword) parece não utilizar operando algum; entretanto, essa instrução utiliza EDX e EAX: o bit mais alto de EAX, o bit de sinal, é copiado para EDX de forma que, EDX:EAX passa a representar o inteiro em EAX convertido para Int64, onde EAX carrega os 32 bits menos significativos e EDX os 32 bits mais significativos. Para a maioria das instruções, os operandos são registradores. Por exemplo: mov eax, ecx copia o valor de ECX para EAX. Operandos podem conter valores imediatos: mov eax, 5 mov eax, 2 + 3 // expressão constante, resolvida na compilação mov al, 'A' // o código ASCII de 'A' é $41 (65) mov eax, 'ABC' // equivalente a MOV EAX, $00414243 Operandos também podem conter referências de memória: mov [ebx], eax // EBX^ := EAX; Referências de memória aparecem de várias formas: mov eax, [$000FFFC] // Endereço absoluto mov eax, [ebx] // Registrador mov eax, [ebp-12] // Registrador mais/menos deslocamento // constante mov eax, [ebp+ebx] // Registrador mais deslocamento em registro mov eax, [ebp+ebx+8] // Registrador mais deslocamento em registro // mais/menos deslocamento constante mov eax, [ebp+ebx*4] // Registrador mais deslocamento em registro // multiplicado por constante mov eax, [ebp+ebx*4+8] // Registrador mais deslocamento em registro // multiplicado por constante, mais/menos // deslocamento constante Os identificadores usuais do Pascal são traduzidos para uma das formas: mov eax, parâmetro // mov eax, [ebp + deslocamento_constante] mov eax, varlocal // mov eax, [ebp - deslocamento_constante] mov eax, varglobal // mov eax, [endereço_absoulto] call procname // chama endereço absoluto Primeiro Exemplo ================ Agora que estamos prontos para aprender alguns opcodes, vamos aos exemplos. Podemos começar com uma função simples: function f(x: integer; y: integer): integer; // f(x,y) = (-x-y+5)*7 { begin Result := (-x - y + 5) * 7; end; } asm // os parâmetros são passados em EAX (x) e EDX (y); neg eax // EAX := -EAX; // EAX = -x sub eax, edx // EAX := EAX - EDX; // EAX = -x-y add eax, 5 // EAX := EAX + 5; // EAX = -x-y+5 imul 7 // EAX := EAX * 7; // EAX = (-x-y+5)*7 end; Os três primeiros parâmetros (da esquerda para a direita) são passados em EAX, EDX e ECX. Para métodos, o primeiro parâmetro é Self (passado em EAX) e o primeiro parâmetro explicitamente declarado é, de fato, o segundo parâmetro (passado em EDX) e o segundo parâmetro explícito é de fato o terceiro parâmetro (passado em ECX). O valor de retorno deve ser armazenado em EAX para valores ordinais de 32 bits (AX e AL devem ser utilizados para retornar valores de 16 e 8 bits respectivamente). Os comentários explicam os opcodes de forma clara mas, para IMUL, temos que acrescentar duas explicações: * IMUL considera os operandos (EAX e 7 no exemplo) como inteiros com sinal (devemos utilizar MUL quando os operandos não possuírem sinal). * O resultado da multiplicação é um inteiro de 64 bits sendo que os 32 bits mais significativos do resultado são armazenados em EDX. Multiplicações são relativamente caras em termos de tempo de CPU e, por vezes, é mais fácil substitui-las por deslocamentos de bits (quando a multiplicação ou divisão operarem com potências de dois), somas e subtrações. Por exemplo: a * 7 = a * (8 - 1) = a * 8 - a = a * 2^3 - a a * 7 = a shl 3 - a Ao invés de IMUL 7, podemos fazer o seguinte: mov ecx, eax // ECX := EAX; // ECX = -x-y+5 shl eax, 3 // EAX := EAX shl 3; // EAX = (-x-y+5)*8 sub eax, ecx // EAX := EAX - ECX; // EAX = (-x-y+5)*8 - (-x-y+5) // EAX = (-x-y+5)*7 Vejamos outro exemplo: function resto(x: integer; y: integer): integer; // Retorna o resto de x dividido por y { begin Result := x mod y; end; } asm // os parâmetros são passados em EAX (x) e EDX (y); mov ecx, edx // ECX := EDX; // EDX = y cdq // EDX:EAX := Int64(EAX); // EAX = x idiv ecx // divisão inteira com sinal em 32 bits: // EAX := Int64(EDX:EAX) div integer(ECX); // EDX := Int64(EDX:EAX) mod integer(ECX); mov eax, edx // Result := EDX; // resto end; A Pilha ======= Quando um programa é carregado, ele receve uma pilha, que é uma região de memória utilizada como uma estrutura LIFO, "Last In, First Out" (último a chegar, primeiro a sair), controlada pelo registrador ESP que aponta para o topo dessa pilha. ESP inicia apontando para o final da região de modo que, cada vez que empilhamos um novo valor de 32 bits, o registrador ESP é decrementado em 4 (bytes) e o valor é armazenado no local apontado por ESP. | | +-----------+ | | +-----------+ | $01234567 | <- ESP +-----------+ | | PUSH $89ABCDEF // SUB ESP,4; MOV [ESP],$89ABCDEF | | +-----------+ | $89ABCDEF | <- ESP +-----------+ | $01234567 | +-----------+ | | De forma análoga, quando retiramos um valor de 32 bits da pilha, o valor é recuperado do local apontado por ESP e ESP é incrementado em 4 (bytes). POP EAX // MOV EAX,[ESP]; ADD ESP,4 | | +-----------+ +-----------+ | $89ABCDEF | EAX | $89ABCDEF | +-----------+ +-----------+ | $01234567 | <- ESP +-----------+ | | A pilha é utilizada para armazenar endereços de retorno de rotinas, parâmetros, variáveis locais e resultados intermediários. No exemplo a seguir, utilizamos a pilha para salvar o valor de um registrador para uso posterior: function IntDiv(x: integer; y: integer; r: pinteger = NIL): integer; // Retorna o quociente inteiro x / y e o resto em r { begin Result := x div y; if r <> NIL then r^ := x mod y; end; } asm // os parâmetros são passados em EAX (x), EDX (y) e ECX ® push ecx // Salve ECX ® para uso posterior mov ecx, edx // ECX := EDX; // ECX = y cdq // EDX:EAX := Int64(EAX); // EAX = x idiv ecx // divisão inteira com sinal em 32 bits: // EAX := Int64(EDX:EAX) div integer(ECX); // EDX := Int64(EDX:EAX) mod integer(ECX); pop ecx // Restaura ECX (ECX := r) cmp ecx, 0 // if ECX = NIL then jz @@end // goto @@end; mov [ecx], edx // ECX^ := EDX; // resto @@end: // identificador local (precedido por "@@") end; Note que, para cada PUSH que executamos, temos que executar um POP correspondente de modo que ESP fique inalterado (ESP é um dos registradores que temos que preservar). A instrução CMP subtrai o segundo operador do primeiro (ECX-0 nesse caso), como a instrução SUB, mas o resultado não é armazenado em lugar algum, ainda que o indicador de Zero (Zero flag) seja marcado (ligado) ou limpo (desligado) dependendo do resultado ser zero ou não, como em qualquer instrução lógica ou matemática (com a exceção de certos casos). Podemos então tirar vantagem desse fato e, ao invés de escrevermos cmp ecx, 0 podemos escrever or ecx, ecx // ECX := ECX or ECX; O resultado de ECX Or ECX é o próprio ECX; portanto, o valor armazenado em ECX é o mesmo de antes, e como dissemos anteriormente o indicador de Zero será marcado se o resultado for zero (isto é, se ECX era zero). A instrução JZ, "Jump if Zero" (Desvie se Zero), desvia (salta) para o identificador indicado como operando se o valor do indicador de Zero estiver marcado (ligado) ou continua normalmente com o fluxo de execução se o indicador de Zero estiver desmarcado (desligado). Passando Parâmetros para a Pilha -------------------------------- Voltemos para a pilha. Dissemos que os três primeiros parâmetros de uma rotina são passados em EAX, EDX e ECX; mas, o que acontece quando temos mais de três parâmetros? Parâmetros adicionais são passados na pilha, da esquerda para a direita, de forma que o último parâmetro será sempre o primeiro da pilha. Suponha que temos a seguinte função function Soma(a, b, c, d, e: integer): integer; begin Result := a + b + c + d + e; end; e queremos fazer a chamada Sum(1,2,3,4,5); Em assembler, faríamos da seguinte forma: mov eax, 1 mov edx, 2 mov ecx, 3 push 4 push 5 call Sum A instrução CALL empilha o endereço de retorno na pilha e salta para (inicia a execução) da função. A instrução RET (RETorna) gerada pelo compilador quando o final de uma função é alcançado desempilha esse endereço da pilha e salta para ele para continuar a execução a partir desse ponto. Note que quando empilhamos parâmetros na pilha mas não os desempilhamos. Isso acontece pois limpar a pilha é responsabilidade da função chamada e não da função que chama (exceto na convenção de chamada CDECL). Para limpar os parâmetros, a instrução RET é utilizada com um operando que indica o número de bytes que ESP deve ser incrementado (8 nesse caso já que ESP foi decrementado em 4 bytes para cada parâmetro empilhado). O compilador fica encarregado dessa tarefa portanto não temos com que nos preocupar; mas, se você utilizar a janela de depuração da CPU e encontrar uma instrução RET $08, agora você já sabe do que se trata. Na entrada para Soma, a pilha estaria, em teoria, da seguinte forma: | | +-----------+ | Ret_Addr | <- ESP +-----------+ | $00000005 | (parâmetro e) +-----------+ | $00000004 | (parâmetro d) +-----------+ | | Quando uma função tem parâmetros na pilha (ou variáveis locais), o compilador gera algumas instruções chamadas de "stack frame", quadro da pilha. Na entrada da função (em "asm"), EBP é empilhado de modo a ser preservado e ESP é atribuído a ele; e, antes de deixar a função, (em "end"), o valor original de EBP é desempilhado: function Soma(a, b, c, d, e: integer): integer; asm // push ebp; mov ebp, esp; .... end; // pop ebp; ret 8; Assim, quando entramos em Soma, a pilha estaria de fato da seguinte forma: | | +-----------+ | Orig. EBP | <- EBP, ESP +-----------+ | Ret_Addr | +-----------+ | $00000005 | <- EBP+8 (parâmetro e) +-----------+ | $00000004 | <- EBP+12 (parâmetro d) +-----------+ | | Em [EBP] encontramos o valor original de EBP que foi empilhado para ser preservado quando da construção do quadro de pilha; em [EBP+4] encontramos o endereço de retorno da rotina; em [EBP+8] encontramos o último parâmetro (o último parâmetro é empilhado por último e, por isso, é o primeiro da pilha). O parâmetro seguinte (da direita para a esquerda) fica em [EBP+12], e assim por diante se houvesse outros parâmetros. Agora vamos escrever a rotina Soma em assembler: function Soma(a, b, c, d, e: integer): integer; { begin Result := a + b + c + d + e; end; } asm add eax, b add eax, c add eax, d add eax, e end; Note que no bloco asm..end nós utilizamos "b", "c", "d" e "e" ao invés de "EDX", "ECX", "[EBP+12]" e "[EBP+8]" respectivamente. Nós podemos fazer assim já que o compilador fará as substituições adequadas. Variáveis Locais na Pilha ------------------------- Se nossa função assembler inline tiver variáveis locais, o compilador criará espaço para essas variáveis na pilha, movendo o ponteiro da pilha de modo que o quadro da pilha para uma função com duas variáveis locais inteiras seria: push ebp mov ebp, esp sub esp, 8 // Desloca ESP como se empilhássemos 8 bytes ... add esp, 8 // Desloca ESP como se desempilhássemos 8 bytes pop ebp Para o propósito do exemplo, aqui vai uma variação da rotina Soma acima, utilizando duas variáveis locais: function SomaL(a, b, c, d, e: integer): integer; var f, g: integer; { begin f := b + c; g := d + e; Result := a + f + g; end; } asm // push ebp; mov ebp, esp; sub esp, 8; add edx, ecx mov f, edx // b + c mov edx, d add edx, e mov g, edx // d + e add eax, f add eax, g end; // add esp, 8; pop ebp; ret 8 Nessa função, a pilha teria o seguinte aspecto: | | +-----------+ | var. g | <- EBP-8, ESP +-----------+ | var. f | <- EBP-4 +-----------+ | Orig. EBP | <- EBP +-----------+ | Ret_Addr | +-----------+ | Param e | <- EBP+8 +-----------+ | Param d | <- EBP+12 +-----------+ | | O Que Vem Agora? ================ Na continuação deste artigo, aprenderemos mais instruções e veremos como passar e retornar outros tipos de parâmetros, como trabalhar com arrays, como acessar campos de registros e objetos, como chamar métodos e mais. Nesse capítulo iremos aprender algumas novas instruções assembler e o básico da manipulação de strings ANSI, também chamadas de strings longas. Novos opcodes ============= Abaixo os opcodes introduzidos neste atrigo: * JL (Jump if Lower, desvie se menor): A descrição mais adequada levaria muito tempo para ser explicada, então vamos dizer que JL salta (desvia) para o label especificado desde que na operação CMP (ou SUB) anterior o primeiro operando seja menor que o segundo numa comparação com sinal: // if signed(op1) < signed(op2) then goto @@label; cmp op1, op2 jl @@label JG (Jump if Greater, desvie se maior), JLE (Jump if Lower or Equal, desvie se menor ou igual) e JGE (Jump if Greater or Equal, desvie se maior ou igual) completa a família de desvios condicionais para comparações com sinal. * JA (Jump if Above, desvie se maior): salta (desvia) para o label especificado desde que na operação CMP (ou SUB) anterior o primeiro operando seja maior que o segundo numa comparação sem sinal: // if unsigned(op1) > unsigned(op2) then goto @@label; cmp op1, op2 ja @@label JB (Jump if Below, desvie se menor), JBE (Jump if Below or Equal, desvie se menor ou igual) e JAE (Jump if Above or Equal, desvie se maior ou igual) completam a família de desvios condicionais para comparações sem sinais. * LOOP: Decrementa ECX e, se não for zero, desvia para o label indicado. LOOP @@label é o equivalente mais curto e rápido de: dec ecx // ECX := ECX - 1; jnz @@label // if ECX <> 0 then goto @@label Examplo: xor eax, eax // EAX := EAX xor EAX; // EAX := 0; mov ecx, 5 // ECX := 5; @@label: add eax, ecx // EAX := EAX + ECX; // Executado 5 vezes loop @@label // Dec(ECX); if ECX <> 0 then goto @@label; // EAX seria 15 (5+4+3+2+1) Trabalhando com strings ANSI ============================ Uma variável string é representada por um ponteiro de 32 bits. Se a string é vazia (''), então o ponteiro é nil (zero), caso contrário, esse ponteiro aponta para o primeiro caractere dessa string. O tamanho da string e a contagem de referência são dois inteiros em deslocamentos negativos a partir do primeiro byte da string: +-----------+ | s: string |-------------------+ +-----------+ | V --+-----------+-----------+-----------+---+---+---+---+---+---+---+-- | allocSiz | refCnt | length | H | e | l | l | o | ! | #0| --+-----------+-----------+-----------+---+---+---+---+---+---+---+-- (longint) (longint) (longint) \-----------------v-----------------/ StrRec record const skew = sizeof(StrRec); // 12 Quando passamos uma string como um parâmetro para uma função, o que de fato é passado é o ponteiro de 32 bits. Os valores string são um pouco mais complicados de explicar. A rotina que chamou a rotina que retorna a string deve passar- como último e invisível parâmetro da chamada, um tipo PString-o endereço de uma variável string que receberá o resultado da função. d := Uppercase(s); // Internamente convertido para: Uppercase(s, @d); Se o resultado da função é usado em uma expressão ao invés de ser atribuído diretamente à variável, a rotina que chama deve utilizar uma variável temporária incializada com nil (string vazia). O compilador faz tudo isso automaticamente no nosso código Object Pascal mas, se temos que fazer isso por conta própria se optarmos por escrever código assembler que chame rotinas que retornam strings. Para algumas tarefas, não podemos chamar as clássicas funções de string diretamente. Por exemplo, a função Length não é o nome de uma função de verdade,. é uma construção interna do próprio compilador e o compilador gera o código para a função apropriada, dependendo do parâmetro ser uma string ou um array dinâmico. Em assembler, ao invés de Lenght, teríamos que usar a função LStrLen (declarada na unidade System) para obter o tamanho da string. Existem mais coisas que deveríamos saber a respeito das strings mas o que temos já é suficiente para um primeiro exemplo. Versão Assembler de Uppercase ============================= Eis a declaração da função: function AsmUpperCase(const s: string): string; O parâmetro "s" será passado em EAX e o endereço de "Result" será passado como o segundo parâmetro, ou seja, em EDX. Basicamente a função deve fazer: 1) Obter o comprimento da string a converter 2) Alocar memória para a string convertida 3) Copiar os caracteres um a um, convertidos para maiúsculas 1) Obter o comprimento da string a converter -------------------------------------------- Faremos isso através de uma chamada a System.@LStrLen. A função espera a string em EAX (ela já está lá) e o resultado será colocado em EAX; então, temos que salvar o valor de EAX (o parâmetro "s") em algum lugar antes de chamar a função de modo que "s" não seja perdido. Podemos salvar numa variável local "src". Já que funções são livres para utilizar os registradores EAX, ECX e EDX, presumimos que o valor em EDX ("@Result") poderia também ser destruído após uma chamada a System.@LStrLen, de modo que é útil salvar esse valor numa variável local, por exemplo, "psrc". O resultado da chamada a System.@LStrLen, deixado em EAX, servirá como parâmetro da chamada a System.@LStrSetLength (para alocar memória para o conteúdo da string de resultado), como contador dos bytes a copiar, de modo que esse valor também deve ser salvo, por exemplo, na variável "n": var pdst: Pointer; // Endereço da string resultado src: PChar; // String de origem n: Integer; // Comprimento da string de origem asm // O endereço da string de resultado é passado em EDX. // Salvamos esse valor na variável pdst: mov pdst, edx // pdst := EDX; // Salvamos EAX (s) na variável local (src) mov src, eax // src := EAX; // n := Length(s); call System.@LStrLen // EAX := LStrLen(EAX); mov n, eax // n := EAX; 2) Alocar memória para a string convertida ------------------------------------------ A alocação é realizada através de uma chamada a System.@LStrSetLength. O procedimento espera dois parâmetros: o endereço da string (que salvamos em "pdst") e o comprimento da string (que está em EAX). // SetLength(pdst^, n); // Alocar a string de resultado mov edx, eax // EDX := n; // Segundo parâmetro p/LStrSetLength mov eax, pdst // EAX := pdst; // Primeiro parâmetro p/LStrSetLength call System.@LStrSetLength // LStrSetLength(EAX, EDX); 3) Copiar os caracteres um a um, convertidos para maiúsculas ------------------------------------------------------------ Se o comprimento da string era zero, já terminamos: // if n = 0 then exit; mov ecx, n // ECX := n; test ecx, ecx // Fazer and de ECX com ECX para definir flags // (ECX inalterado) jz @@end // Ir para @@end se o flag zero está marcado (ECX=0) Não sendo esse o caso, devemos copiar os caracteres de uma string para a outra, convertendo-os para maiúsculas conforme necessário. Nós vamos utilizar ESI e EDX para apontar para os caracteres da string de origem e destino respectivamente, AL para carregar os caracteres da string de origem e realizar a mudança antes de armazená-los na string de destino e ECX para controlar a instrução de LOOP que contará os caracteres. Já que ESI é um registro que tem que ser preservado, devemos salvar seu valor para restaurá-lo mais tarde. Decidi salvar ESI colocando-o na pilha. push esi // Salve ESI na pilha // Inicializar ESI e EDX mov eax, pdst // EAX := pdst; // Endereço da string de resultado mov esi, src // ESI := src; // String de origem mov edx, [eax] // EDX := pdst^; // String de resultado @@cycle: mov al, [esi] // AL := ESI^; // if Shortint(AL) < Shortint(Ord('a')) then goto @@nochange cmp al, 'a' jl @@nochange // AL in ['a'..#127] // if Byte(AL) > Byte(Ord('a')) then goto @@nochange cmp al, 'z' ja @@nochange // AL in ['a'..'z'] sub al, 'a'-'A' // Dec(AL, Ord('a')-Ord('A')); @@nochange: mov [edx], al // EDX^ := AL; inc esi // Inc(ESI); inc edx // Inc(EDX); loop @@cycle // Dec(ECX); if ECX <> 0 then goto cycle pop esi // Restaurar ESI da pilha @@end: end; 1 ɓʀuɳѳ' ИИ#92 reacted to this Share this post Link to post Share on other sites
Xandy 46 Posted November 12, 2018 E deixar os direitos de autor? Isto é só copy/past. Fonte: https://pt.scribd.com/document/115934223/Untitled Share this post Link to post Share on other sites