3304 registros
0 hoje
9 nesta semana
41 neste mês![]() | 73% | Brasil (47762) |
![]() | 5% | Portugal (3099) |
![]() | 3% | EUA (2141) |
![]() | 0% | Rússia (259) |
![]() | 0% | Holanda (240) |
| Hoje: | 564 |
| Ontem: | 2587 |
| No mês: | 39741 |
| Mês passado: | 25815 |
| Total: | 65556 |
| Recorde: | 3037 |
| No dia: | 04.03.10 |
| Leituras hoje: | 20697 |
| Leituras Total: | 282249 |
| Bots hoje: | 153 |
| Dados desde: | 16.02.2010 |
|
Sáb 25 Abr 2009 14:10 |
|
Todos programas fazem uso intensivo da pilha em tempo de execução. Quando se programa usando uma linguagem de alto nível, este aspecto passa batido e a gente nem toma conhecimento do assunto. Um programador assembly, no entanto, precisa ficar esperto porque a pilha é uma das ferramentas mais importantes que ele tem à sua disposição. Saber trabalhar com a pilha é uma enorme vantagem, apesar de não ser indispensável. Em todo caso, sempre é bom ter uma noçãozinha da coisa. Características e vantagens da pilhaA pilha é basicamente uma área de dwords (área de dados de 32 bits) existente na memória em tempo de execução, na qual o aplicativo pode armazenar dados temporariamente. Possui certas características e vantagens reais em relação a outros tipos de armazenamento na memória (seção de dados e áreas de memória em tempo de execução). São elas:
A pilha pode ser usada para:
Registrador ESP, o ponteiro da pilhaO registrador ESP (acrônimo de "extended stack pointer") contém o topo da pilha. Este é o ponto usado pelas instruções que utilizam a pilha (PUSH, POP, CALL e RET). Adiante falaremos mais sobre o assunto. Normalmente o programador faz o registrador EBP (acrônimo de "extended base pointer") apontar para um determinado lugar da pilha para que seus dados possam ser lidos ou escritos usando um endereçamento com base indexada. Por exemplo, na instrução MOV EAX,[EBP+8h], o registrador EBP é usado como um índice para uma área da pilha e esta instrução irá transferir da pilha para o registrador EAX um dword situado 8 bytes adiante. A origem do uso do registrador EBP associado à pilha é da época dos sistemas de 16 bits, que tinham toda aquela complicação com segmentos e outros que tais. Nos sistemas de 32 bits não é necessário manter esta associação e o registrador EBP pode ser utilizado como um registrador de uso geral. Apenas por hábito ele continua sendo usado para endereçar determinadas áreas da pilha, principalmente para acessar parâmetros passados para funções e rotinas de callback e para endereçar dados locais. Armazenando e retirando dados da pilhaA pilha pode ser imaginada como uma pilha de pratos. Isto funciona na base de "último a entrar, primeiro a sair". O último prato colocado na pilha usando uma instrução PUSH será o primeiro a ser retirado com uma instrução POP (se não for assim, a pilha cai Voltando ao computador. Suponha que o valor de ESP seja 64FE3Ch e que você tenha as seguintes instruções no seu código fonte: PUSH 2 PUSH [hWnd] PUSH ADDR STRING Após estas três instruções, ESP estaria com o valor 64FE30h (12 bytes ou 3 dwords a menos) e a pilha teria o seguinte aspecto:
ESP está aqui -> 64FE30h endereço de STRING
64FE34h valor de hWnd
64FE38h número 2
64FE3Ch
Observe que cada instrução PUSH diminui o valor de ESP em 4 bytes. Observe também que, uma vez que ESP aponta para o último dword PUSHado para a pilha, o próximo PUSH vai escrever em ESP-4h. Isto é feito pelo processador, que reduz o ESP em quatro e depois escreve o dword no endereço que ESP contém. Agora vamos ver como se comporta o POP. Usando os mesmos valores da pilha, usaremos as seguintes instruções: POP EAX POP EBX POP ECX Despois destas três instruções a pilha terá o seguinte aspecto:
64FE30h endereço de STRING -> EAX
64FE34h valor de hWnd -> EBX
64FE38h número 2 -> ECX
ESP está aqui -> 64FE3Ch
A primeira coisa a ser observada é que, após estas três instruções, o ESP está de volta em 64FE3Ch. Isto significa que o equilíbrio de ESP foi restaurado. Este é um conceito muito importante (veja logo abaixo). O registrador EAX agora contém o endereço de STRING, o EBX contém o valor de hWnd e o ECX contém o número 2. Percebe-se que os dados armazenados na pilha foram retirados pelo POP na ordem inversa em que foram colocados. Observe também que os dados da pilha continuam presentes! Isto acontece por que a instrução POP não escreve na pilha. Ela apenas lê os dados da pilha e os transfere para a segunda parte da instrução (chamada de "operando"). Preservando valores de registradores em funçõesProgramas escritos em Assembly são rápidos porque usam os registradores exaustivamente, só que isto muitas vezes exige que os valores dos registradores sejam preservados para uso futuro. Por exemplo, imagine que um manipulador de arquivo (handle) esteja em EDI e que, após alguns cálculos com a ajuda do EDI, você tenha que fechar o manipulador. Para preservá-lo pode-se fazer o seguinte: PUSH EDI ;salva o manipulador de arquivo CALL CALCULA ;faz alguns cálculos (usando EDI) POP EDI ;recupera o manipulador de arquivo CALL CLOSE_FILEHANDLE ;fecha o manipulador contido em EDI Uma outra alternativa é preservar o EDI dentro do procedimento CALCULA: CALL CALCULA ;faz alguns cálculos (salva EDI) CALL CLOSE_FILEHANDLE ;fecha o manipulador contido em EDI CALCULA: PUSH EDI ;salva o manipulador de arquivo . . ;código usando EDI . POP EDI ;recupera o manipulador de arquivo RET Outra razão para um registrador ser preservado é quando uma função em particular é chamada externamente (por outra função no mesmo programa, por outro programa ou pelo sistema). Na maioria dos casos deve-se garantir que EBP, EBX, EDI e ESI sejam preservados. Programas em C ou Delphi que chamam rotinas em Assembly e procedimentos callback chamados pelo próprio Windows com certeza exigem esta preservação. Um exemplo de procedimento callback é um procedimento de uma janela que é usada pelo sistema para passar informações para uma janela de um aplicativo. Nestas circunstâncias é necessário garantir os valores dos registradores usando, por exemplo: PUSH EBP,EBX,EDI,ESI . . ;seu código vai aqui . POP ESI,EDI,EBX,EBP É óbvio que, se estes registradores não forem modificados pelo código, alguns PUSH e POP não são necessários. Mesmo assim, é uma boa prática garantir a preservação dos seus valores - o seguro morreu de velho. Note que os POP estão na ordem inversa dos PUSH - isto é para respeitar o "último a entrar, primeiro a sair" da pilha. Observe também que os registradores estão em ordem alfabética. É um pequeno truque para não esquecer nenhum deles. Caso você esteja trabalhando com o GoAsm, a declaração USES preserva e restaura automaticamente todos os registradores. Preservando dados da memóriaDa mesma forma que é possível preservadar valores de registradores usando a pilha, pode-se também preservar dados da memória. Suponha que você tenha calculado cuidadosamente o número de widgets e quer escrever os detalhes dos widgets na tela além de gravá-los em arquivo. Você pode usar o seguinte código: PUSH [NRODE_WIDGETS] ;guardar número de widgets L2: CALL REPORT_WIDGET ;escrever detalhes do widget na tela DEC D[NRODE_WIDGETS] ;decrementar o número de widgets JNZ L2 ;continuar com o próximo enquanto não for zero POP [NRODE_WIDGETS] ;restaurar o número de widgets CALL WRITETO_FILE ;e gravar em arquivo Transferindo dados sem usar registradoresSuponha que você queira transferir o número de widgets para um outro marcador (label) de memória. Você poderia usar: MOV EAX,[NRODE_WIDGETS] MOV [COPIADE_NRODE_WIDGETS],EAX Igualmente eficiente seria: PUSH [NRODE_WIDGETS] POP [COPIADE_NRODE_WIDGETS] Como esta segunda opção não faz uso do registrador EAX, este registrador não perderia seu valor e poderia ser utilizado para outra finalidade. Revertendo a ordem de dadosVocê pode tirar vantagem da característica "último a entrar, primeiro a sair" da pilha para inverter a ordem de dados. Um exemplo muito prático é escrever na tela um valor decimal. Neste exemplo, EAX contém o valor que deve ser escrito e EDI contém a posição de memória do buffer que abrigará a string com os algarismos: XOR EDX,EDX ;zera edx XOR ECX,ECX ;zera ecx (usado como contador) MOV EBX,10 ;ebx guarda sempre o valor 10 L2: DIV EBX ;divide edx:eax por 10 - quociente em eax, resto em edx PUSH EDX ;põe resultado na pilha INC ECX ;conta quantos foram feitos XOR EDX,EDX ;zera edx CMP EAX,EDX ;vê se há mais para ser feito JNZ L2 ;sim L3: ;agora reverter a ordem dos dígitos POP EAX ;pega o próximo da pilha ADD AL,48 ;converte para número ascii STOSB ;escreve número ascii no buffer LOOP L3 ;continua enquanto ecx for diferente de zero Vamos analisar este código. Imagine que o valor em EAX seja 123 decimal. A primeira divisão por dez põe 12 em EAX e 3 em EDX. 3 é colocado na pilha. A segunda divisão por dez põe 1 em EAX e 2 em EDX. 2 é colocado na pilha. A terceira divisão por dez põe zero em EAX e 1 em EDX. 1 é colocado na pilha. O resultado de CMP EAX,EDX então é zero e a execução do código é desviada para o marcador L3. ECX está com 3 porque contou o número de dígitos. Agora cada um deles é retirado da pilha e adicionado a 48. Para 1, 2 e 3 obtemos respectivamente 49, 50 e 51. Estes valores são transferidos para o buffer e correspondem aos caracteres ascii "1", "2", e "3". Como foram colocados na pilha na ordem inversa (321) e foram retirados novamente na ordem inversa (123), já estão na sequência desejada e prontos para, mais tarde, serem escritos na tela. Como CALL e RET usam a pilhaA instrução CALL é muito usada em programação. É utilizada para desviar a execução para um procedimento (ou "função") em particular. Quando o procedimento termina, a execução continua logo após a linha da chamada. Chamando procedimentos ajuda a manter o código fonte limpo e mais fácil de entender. Por exemplo: 401020: MOV EAX,EDX 401022: CALL CALCULA_CUSTOS 401027: MOV [CUSTOS],EAX ;põe resultado da chamada na memória Não há dúvida de que o procedimento CALCULA_CUSTOS deve realizar um trabalho extenso, porém, neste ponto do código, não há a necessidade de se preocupar com isso. Usando calls também ajuda a manter a modularidade do código, ou seja, o procedimento CALCULA_CUSTOS também pode ser usado por outros programas. Se quiser, pode considerá-lo como um "objeto". A programação orientada a objeto é basicamente isto. Como é que o processador sabe onde continuar o processamento depois de uma chamada? Muito simples: ele coloca o endereço de retorno na pilha! Vamos dar uma olhada na pilha no momento em que acontece uma chamada. Imagine que o valor de ESP seja 64FE3Ch e que o código fonte seja o mostrado acima. Após a primeira instrução, é claro que ESP ainda está em 64FE3Ch e a pilha não foi modificada por que ela não é afetada pela instrução MOV. Mas, quando a instrução CALL CALCULA_CUSTOS é executada, o processador PUSHa para a pilha o endereço de retorno 401027h. Bem, no procedimento CALCULA_CUSTOS existe uma instrução RET (retornar ao chamador), por exemplo: CALCULA_CUSTOS:
;um monte de código aqui
RET ;retornar ao chamador
A instrução RET causa um POP para EIP. Em outras palavras, seja o que for que estiver em [ESP] é atribuído a EIP (o ponteiro de instruções) e depois ESP (o ponteiro da pilha) é incrementado em 4 bytes. Vamos observar o que acontece com a pilha antes, durante e depois destas instruções. Note como o equilíbrio do ESP é restaurado: Antes da Chamada Durante a Chamada Depois da Chamada 64FE30h 64FE30h 64FE30h 64FE34h 64FE34h 64FE34h 64FE38h ESP -> 64FE38h 401027h 64FE38h 401027h ESP -> 64FE3Ch 64FE3Ch ESP -> 64FE3Ch A importância do equilíbrio da pilhaVimos como um procedimento pode ser chamado e o endereço de retorno é mantido na pilha. Acontece que, com frequência, procedimentos chamam outros procedimentos que chamam outros procedimentos... e assim por diante. Podemos ter, por exemplo: CALCULA_CUSTOS: CALL CALCULA_CUSTOFIXO RET ;retorna ao chamador CALCULA_CUSTOFIXO: ;uma porção de código CALL GET_CUSTOVARIAVEL CALL AJUSTEPARA_DEPRECIACAO ADD ESP,4 ;tira o equilíbrio de ESP RET Neste exemplo, a tarefa é dividida em vários componentes. Imagine que o procedimento CALCULA_CUSTOFIXO adicione 4 a ESP por engano. Se isto acontecer, quando a instrução RET for executada, o ponteiro de instruções EIP estará carregado com um valor errado e o programa vai dar pau. Enquanto um procedimento estiver sendo executado é comum que o ESP seja deslocado (por exemplo, quando é preciso abrir um espaço na pilha), mas é de vital importância assegurar que o equilíbrio da pilha seja restaurado assim que o procedimento chegar no fim. O equilíbrio da pilha também é importante ao retornar para o Windows, mesmo num programinha minúsculo. O aplicativo Windows mais simples possível, que não faz absolutamente nada, é o seguinte: START:
RET
onde START é a entrada do aplicativo. Na realidade, o Windows normalmente chama seu aplicativo através da Kernel32.dll, de modo que um simples RET termina o programa alegremente sem maiores problemas por que esta e outras DLLs da API cuidam que a pilha se mantenha equilibrada. Usando a pilha para passar parâmetrosAs APIs do Windows esperam receber parâmetros através da pilha. Portanto, quando chamamos uma API, é necessário PUSHar os parâmetros necessários para que estes possam ser resgatados pela API. PUSH 1,[hButton] CALL EnableWindow ;habilitar botão Inicialmente colocamos o valor 1 (flag ENABLE, habilitar) na pilha, seguido pelo manipulador da janela que quermos habilitar. O Windows usa a convenção de chamada padrão "C" para suas APIs de modo que, ao retornar da API, a pilha estará novamente em equilíbrio. A convenção também significa que EBP, EBX, ESI e EDI sempre são restaurados pela API. Outro aspecto da convenção é que os parâmetros são sempre PUSHados da direita para a esquerda (ou do último para o primeiro). As especificações para a função EnableWindow no Windows Software Development Kit são: WINAPI EnableWindow( HWND hWnd, BOOL bEnable ); Para traduzir para Assembly, é preciso ler do fim para o começo. A coisa fica um pouco mais fácil se usarmos a instrução INVOKE ao invés de CALL. Neste caso, a ordem dos parâmetros é a mesma do SDK: INVOKE EnableWindow, [hButton], 1 FinalmentesUFA!!! Foi muito pra cabeça? Espero que não. Entender o funcionamento da pilha, a meu ver, é essencial para produzir programas de qualidade. Não posso deixar de agradecer Jeremy Gordon pelos seus excelentes artigos sobre a pilha. O presente texto é (praticamente) apenas a tradução de Understand the stack (part 1) do referido autor. |
| Última atualização ( Seg, 27.04.2009 23:34 ) |