A instrução LEA do x86
Autor: RTFM | Publicado: 2011-09-13T12:26:00-07:00

Um tempo atrás um doido aqui do trampo resolveu "melhorar" um pedaço do código escrevendo ele todo em assembly inline dentro dos modulos em c++ =D para completar, usou métodos pouco convencionais  para executar algumas operações :) Isso rendeu muito papo para almoço e ele ganhou fama de ser maluco, se bem q ele já tinha essa fama :) Uma dos pulos do gato que ele implementou foi usar a instrução "lea" do x86 para multiplicar valores. Normalmente o manual da intel diz q LEA significa "load effective address", um pouco distante do famoso "MUL" :) Mas, devido a uma particularidade do modo de endereçamento dos x86, lea pode ser usada para muitas coisas alem de simplesmente funcionar como uma instrução para lidar com ponteiros. O assembly  do x86 foi criado para "suportar" linguagens de alto-nível como C e Pascal de forma pouco dolorosa para os compiladores e nessas linguagens arrays de inteiros e estruturas pequenas são muito comuns. Vamos ver pra que LEA é utilizada "normalmente" :) Suponha q temos uma estrutura que guarda um valor de cor (RGB ou HSV):struct Color{    int rgb;    int hsv;};Para recuperar o valor do campo HSV escreveríamos algo como:int hsvValue = colors[i].hsv;Aqui colors[] é um array do tipo Color. Supondo que o compilador coloque o endereço base do array em EBX, e que a variavel i esteja em EAX, e que (rgb,hsv) são valores de 32 bits de forma que hsv esteja no offset 4 da estrutura (base+4), então a tradução para assembly poderia tomar a seguinte forma:INTEL: MOV EDX, [EBX + 8*EAX + 4]AT&T:  mov 4(%ebx,%eax,8), %edxIsso vai colocar o valor de "hsv" no registrador EDX. O valor de escala 8 é usado pq cada variável do tipo Color tem 8bytes, ou seja, soma o endereço base do array ao valor de "i" multiplicado pelo tamanho da estrutura então adiciona 4 que é o offset de "hsv". Mas é se quissemos o endereço de "hsv"? Vamos analisar esta expressão usando o operador & para recuperar o endereço de hsv:int * pColor = &color[i].hsv;Neste caso não queremos o conteúdo contido no endereço de memória, queremos o endereço de memória em si. Neste caso vamos usar a instrução LEA:LEA ESI, [EBX + 8*EAX + 4]Pq usar LEA e não MOV? Pq neste caso se você trocar por MOV o que vai acontecer é: o cpu vai efetuar o calculo EBX+8*EAX+4 e vai encontrar um valor, este valor será encarado como um endereço de memória então o cpu vai neste endereço buscar o conteúdo que está armazenado lá. O que claramente não é nossa intenção. Podemos dizer que LEA é uma instrução que executa cálculos envolvendo memória, mas sem acessar estes endereços. De certa forma, é mais uma operação de soma e shift só que com um nome estranho :) Até daria para utilizar MOV, veja esse programinha em C:#include int main(){int * p;int a = 2;p = &a;printf("%d\n",*p);return 0;}Se olharmos o código assembly gerado pelo compilador(no meu caso o gcc) o código será parecido com:main:    // Setup da pilha    pushl   %ebp    movl    %esp, %ebp    andl    $-16, %esp    subl    $32, %esp    movl    $2, 24(%esp)      // Move 2 para a região da pilha onde está o 'a'    leal    24(%esp), %eax    // Carrega eax com o endereço de 'a'    movl    %eax, 28(%esp)    // Move para onde fica 'p'    movl    28(%esp), %eax    // Move o endereço de 'a'(valor de 'p') para eax    movl    (%eax), %edx      // Carrega edx com o valor de 'a'    // Setup para chamar printf    movl    $.LC0, %eax    movl    %edx, 4(%esp)    movl    %eax, (%esp)    call    printf    movl    $0, %eax    leave    retObvio que o código não é otimizado, mas veja que ele usa lea para carregar o endereço de 'a' em 'p', poderíamos ter chegado ao mesmo resultado usando MOV, da seguinte forma:main:    // Setup da pilha    pushl   %ebp    movl    %esp, %ebp    andl    $-16, %esp    subl    $32, %esp    movl    $2, 24(%esp)     // Move 2 para a região da pilha onde está o 'a'    // Faz o que o LEA faz usando um MOV e um ADD    movl    %esp, %eax    addl    $24,  %eax    // O resto é igual! :)    movl    %eax, 28(%esp)    movl    28(%esp), %eax    movl    (%eax), %edx    movl    $.LC0, %eax    movl    %edx, 4(%esp)    movl    %eax, (%esp)    call    printf    movl    $0, %eax    leave    retDá para notar que usamos 2 instruções ao invés de uma, não muito bom :) E esse é o principal sentido de usar LEA, poupar cálculos e resolver o endereço de memória que queremos e depois usar, num loop, por exemplo, onde seria custoso ficar calculando offsets o tempo todo. Se você mudar o LEA para MOV no primeiro caso provavelmente vai quebrar o programa, pq MOV vai tentar acessar o endereço calculado para recuperar o valor. Um outro exemplo é como recuperar o valor de um ponteiro, vejamos, dado o seguinte programinha em C:#include int main(){int * p;int a = 2;p = &a;printf("%d\n",a);return 0;}Esse programa mostra 2 na saída padrão. O código assembly gerado pelo compilador(Gcc) será parecido com:main:    pushl   %ebp    movl    %esp, %ebp    andl    $-16, %esp    subl    $32, %esp    movl    $2, 24(%esp)    leal    24(%esp), %eax    movl    %eax, 28(%esp)    movl    24(%esp), %edx    movl    $.LC0, %eax    movl    %edx, 4(%esp)    movl    %eax, (%esp)    call    printf    movl    $0, %eax    leave    retVamos supor que alguem apagou nosso código em C e vc, por alguma razão maluca qualquer, tem que trocar a linha printf("%d\n",a); para printf("%d\n",*p); fariamos o seguinte:Já carregamos o endereço de 'a' em 'p' com a instrução movl %eax, 28(%esp). Podemos notar que antes de colocar o valor dos parâmetros do printf na pilha o compilador moveu 'a' para edx e o endereço da string para eax. O que vamos fazer é trocar a instrução movl 24(%esp), %edx por:movl 28(%esp), %edxmovl (%edx), %edxJá que 'p' está no endereço (esp+28) nós temos que reaver o que ele contem (o endereço de 'a') e fazemos isso com o movl 28(%esp), %edx ai temos que acessar esse ponteiro movl (%edx), %edx e armazenar em edx o valor contido no endereço que estava armazenado em 'p' (sendo q 'p' contem o endereço de 'a'). Agora que já vimos bastante o LEA, vamos traduzir algumas utilizações do LEA para português:leal (%ebx), %eaxEsta instrução copia o endereço da expressão (%ebx) para eax. Como o "endereço efetivo" é o valor de ebx, essa instrução vai copiar ebx em eax como se fosse um MOV. Nada muito útil :)leal 3(%ebx), %ebxCopia o valor de ebx + 3 para ebx. Não muito útil já que um ADD faria o mesmo, possivelmente mais rápido.leal 3(%ebx), %eaxAqui temos uma utilização mais sensata de LEA, ela vai copiar o valor de ebx somado a 3 em eax. Isso significa que fizemos um MOV + ADD em uma única instrução! Ok, e o que LEA tem com MUL!?!? Se usarmos a capacidade de LEA operar cálculos com endereços sem acessar a memória podemos usar isso para multiplicar por 2,4,8 ou até por 3,5,9, ou até executar essa multiplicação e somar um inteiro. Usando LEA para multiplicar:leal (%eax,%eax,2), %eax   // EAX = EAX * 2 + EAX = EAX*3leal (%eax,%eax,4), %eax   // EAX = EAX * 4 + EAX = EAX*5leal (%eax,%eax,8), %eax   // EAX = EAX * 8 + EAX = EAX*9Outros exemplo:leal 8(,%eax,4), %eax      // Multiplica eax por 4 e adiciona 8No Asm AT&T a sintaxe displacement(base register, offset register, scalar multiplier) é equivalente ao [base register + displacement + offset register * scalar multiplier] na sintaxe da Intel. Um ou ambos os parâmetros numéricos e um dos registradores podem ser omitidos. Sendo que o fator de escala deve ser 1,2,4 ou 8. Ok! Viram! Com LEA vc pode realizar alguns calculos (e sem modificar os flags! isso consta na documentação da Intel). Se você consultar qualquer guia de otimização de código (principalmente para guiar compiladores) verá que LEA pode fornecer métodos bem úteis de realizar operações matemáticas! É isso pessoal! Meu parecer é esse tal gato que meu brother fez não é tanto um gato, é muito mais uma técnica muito boa!!!!