antiblock
Cyphriun
  • Chatbox

    You don't have permission to chat.
    Load More
Sign in to follow this  
V¡®u§

Delphi com assembly (Parte 1)

2 posts in this topic

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;

Share this post


Link to post
Share on other sites
antiblock
Rodnia | Alpha & Omega

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