A segunda parte deste artigo não é essencial para os programadores em Assembly porém contém informações muito interessantes. Por ser um assunto extenso, o artigo foi dividido em duas partes. Neste segundo bloco abordaremos os seguintes tópicos:
Todos os tópicos são muito interessantes, portanto, vamos ao trabalho. |
||||||||||||||||||||
O valor em ESP é um endereço virtual. Se, por exemplo, no início, for 64FE3Ch, não estará se referindo a um endereço de memória existente na memória física real. Para obter o endereço físico da memória, o sistema precisa converter (ou "mapear") 64FE3Ch de acordo com seus próprios registros internos. Por exemplo, este endereço pode muito bem corresponder a 2FE3Ch na memória física real. Portanto, um endereço virtual é apenas uma representação conveniente de uma posição na memória. Costuma-se dizer que cada aplicação roda no seu próprio espaço virtual de endereços. Na teoria, toda a extensão de endereços de 32 bits (zero a 4 Gb) está disponível para cada uma das aplicações. Na prática, a coisa muda de figura, mas continua sendo verdade que cada aplicação que esteja rodando no sistema pode usar a mesma extensão de endereços virtuais. Não ocorrem conflitos porque o sistema sabe o tempo todo qual aplicação está endereçando memória. Portanto, pode indicar às aplicações o local correto na memória física. Deste modo, é possível que várias aplicações apresentem simultaneamente o mesmo valor em ESP porém cada um destes valores estará apontando para um local diferente da memória física. |
||||||||||||||||||||
O Windows aloca uma área de pilha específica para o thread principal quando este é carregado. O próprio sistema faz uso deste thread e da sua área de pilha antes de chamar o endereço de entrada do programa. Você pode ver isto no debugger. Inicie seu programa, deixe chegar no endereço de entrada e observe o valor de ESP. Agora abra uma janela de inspeção para o valor de ESP. Talvez você imagine estar na base da área de memória, só que não é este o caso. Se você rolar a janela de inspeção para a base da memória (role para o maior endereço) você verá que já houve muita atividade na pilha durante a preparação do sistema para chamar o endereço de entrada do programa. É interessante que o último valor da pilha, antes da aplicação ser chamada, é um endereço de retorno na Kernel32.dll. Isto indica que uma função da Kernel32.dll chamou a aplicação. Devido à existência deste endereço de retorno é possível usar um simples RET para terminar o processo, ao invés de chamar ExitProcess. É claro que isto só funciona se a pilha estiver em equilíbrio de modo que a execução do código continue na função chamadora da Kernel32.dll. Um pouco mais adiante, podemos ver na pilha o nome do arquivo da aplicação e, mais adiante ainda, podemos observar o endereço do manipulador de exceções que o próprio sistema alocou para o thread principal da aplicação. Todas estas coisas mostram que a área de pilha da aplicação (assim como seu thread) é utilizada pelo sistema para preparar a chamada a esta aplicação. |
||||||||||||||||||||
No Windows, quando alguma memória é reservada para o uso de uma aplicação, uma quantidade de endereços virtuais são alocados pelo sistema. Esta alocação preserva estes endereços para que a aplicação possa utilizá-los. Se a aplicação precisar de mais memória, os mesmos endereços não podem ser reutilizados. Nenhuma memória física é utilizada enquanto a memória não tiver sido consignada. Neste ponto, os endereços virtuais que foram alocados são mapeados para a área ou áreas da memória física que estejam disponíveis para o sistema. Obviamente, para que este processo funcione, o sistema precisa saber do tamanho máximo de memória contígua que deverá ser consignada. Esta passa a ser a extensão de endereços alocados. O mesmo se aplica quando alguma memória é reservada para o uso da pilha. No início de uma aplicação, o sistema precisa saber quanta memória alocará para a pilha e quanto deverá consignar na primeira instância. Estas duas quantidades estão referenciadas no arquivo PE em +48h e +4Ch no cabeçalho opcional. Como veremos abaixo, referem-se não somente ao thread principal da aplicação mas também a novos threads criados pela aplicação. A maioria dos linkers utilizam, respectivamente, 1Mb e 4 Kb (o tamanho normal de página) para estes valores. Com o GoLink você pode alterar estes valores default usando, respectivamente, /stacksize e /stackinit (veja o manual do GoLink para saber como usá-los). |
||||||||||||||||||||
O sistema percebe se uma aplicação estiver tentando ler ou escrever além da área de pilha consignada usando manipulação de exceção. Considerando que a tentativa ocorra dentro da área permitida da pilha, mais memória será consignada de acordo com a necessidade. Mesmo que ocorra uma tentativa de aumentar a pilha além da área alocada, o sistema NT (mas não o W9x) tentará alocar mais memória, o que só não ocorre se os endereços virtuais requeridos tenham sido alocados para outras áreas de memória. |
||||||||||||||||||||
A pilha não é considerada como apropriada para manter grandes quantidades de dados e este enfoque é reforçado pelo Windows através do seu mecanismo de exceção. No W9x, a área de pilha usável permitida situa-se entre o ESP corrente e o limite da próxima página mais o tamanho da página. Por exemplo, se ESP for 64FE3Ch, então o limite da próxima página será 64F000h e o tamanho da página extra (que geralmente é fixada em 4K pelo sistema) nos leva para 64E000h:
Desta forma, se ESP for 64FE3Ch, a instrução MOV D[ESP-1E40h],0 causará uma exceção porque o ponto atual da pilha que está sendo endereçado é 64DFFCh, uma área não disponível porque ainda não foi consignada pelo sistema. Também não é possível contornar o problema movendo ESP. No W9x, o sistema permite que o ESP seja movido apenas até ao limite da próxima página + o tamanho da página menos quatro bytes. Por exemplo, se ESP for 64FE3Ch, só será permitida uma única instrução para mover ESP em 1E38h (em decimal isto corresponde a 7836 bytes). Isto significa que a instrução SUB ESP,1E38h faz com que ESP se torne 64E004h e isto é permitido. Mas a instrução SUB ESP,1E3Ch causará uma exceção. A diferença de 4 bytes na posição que dispara a exceção sugere que existam dois tipos de proteção. Pelo acima exposto pode parecer que o tamanho dos dados que podem ser colocados na pilha esteja limitado a 4K, porém isto não é verdade. Existem duas maneiras de se evitar estas exceções e, desta forma, usar a pilha para uma quantidade maior de dados. A primeira forma é mover e usar o ESP incrementalmente. Isto assegurará que o sistema consigne memória progressivamente, como desejado. O seguinte código cria com segurança uma área de 40K bytes na pilha: MOV ECX,10 L0: SUB ESP,1000h MOV D[ESP],0 LOOP L0 Aqui se obriga o sistema a consignar 10 blocos de 4K de memória de pilha. O ESP acaba ficando no topo desta área de pilha. Este processo não é particularmente rápido porque o sistema precisa consignar memória dez vezes. Um método mais rápido é instruir o sistema a consignar uma quantidade maior que a usual de memória para a pilha quando a aplicação for carregada. Com o GoLink é possível fazê-lo usando /stackinit. Por exemplo, /stackinit 0A000 garantirá que 40K de memória seja consignada para a pilha no início. Você vai poder mover o ESP com segurança usando a instrução SUB ESP,0A000h e terá um espaço de 40K de memória para brincar. |
||||||||||||||||||||
Com as devidas precauções, a pilha pode ser usada para armazenar um fluxo razoável de dados. Os pontos que devem ser lembrados são:
|
||||||||||||||||||||
Cada thread do seu aplicativo possui seus próprios registradores e pilha. Isto quer dizer que, quando o sistema delegar tempo de processamento ao thread, ele entrará no contexto de registradores deste thread. O contexto contém todos os valores dos registradores existentes no momento em que, da última vez, o tempo de processamento foi tirado do thread. Como os registradores incluem o ESP, seu valor também será corretamente trocado de modo que a área de memória física correta será usada pelo thread como sua pilha. O resultado é que thread pode se apoiar no fato de que pode usar sua pilha como uma área particular da memória que recebe interferências de outros threads. Você pode observar isto no debugger. Será possível ver que o ESP sempre muda substancialmente quando a execução troca de thread. Quando um thread é iniciado, sua área de pilha é alocada. Como exemplo prático, verificou-se que o thread principal de um aplicativo rodava a partir de 64FE3Ch (para baixo) e, quando um novo thread era feito, sua pilha rodava a partir de 75FF9Ch (para baixo). Num outro teste, quando seis threads novos foram feitos, suas pilhas foram iniciadas respectivamente em 19DEF9Ch, 1AFFF9Ch, 1C1FF9Ch, 1D3FF9Ch, 1E5FF9Ch e 1F7FF9Ch. Aqui você pode notar que o sistema está separando o endereço virtual de cada área de pilha com 128Kb a mais do que o default de 1Mb. Provavelmente isto tenha ocorrido para abrir espaço para o uso da pilha pelo sistema e também alguma folga. Alterando a alocação do tamanho da pilha para 200000h (2Mb) através do uso de /stacksize e depois criando seis threads novos teve como resultado a separação das áreas de pilha com 128Kb a mais que os 2 Mb. |
||||||||||||||||||||
Um quadro de pilha é uma área particular da pilha que contém um endereço de retorno de uma função e dados usados por esta função, sem o risco de sobre-escrita porque o valor de ESP foi decrementado. Os dados mantidos num quadro da pilha são denominados "dados locais". Isto porque são usados apenas dentro do referido quadro de pilha e não está previsto que sejam endereçados pelo programa de forma feral. Vejamos este exemplo simples: PROCEDURE1: SUB ESP,20h ;faz espaço na pilha para dados locais ; ;usa a área de dados locais CALL PROCEDURE2 ; ;retorna de PROCEDURE2 ; ;continua a usar dados locais ADD ESP,20h ;restaura o equilíbrio de ESP RET e PROCEDURE2: PUSH EAX,EBX,ECX ; ;faz vários cálculos POP ECX,EBX,EAX RET Aqui o quadro da pilha é criado usando a instrução SUB ESP,20h. Isto diminui o valor de ESP em 32 bytes, criando espaço na pilha para 8 dwords. Agora, como o ESP foi mudado, qualquer coisa que ocorrer na PROCEDURE2 nunca vai sobre-escrever estes 8 dwords. Vamos conferir isto visualmente imaginando que ESP contenha 64FE38h no início da PROCEDURE1:
|
||||||||||||||||||||
Observação: isto é automatizado no GoAsm usando FRAME..ENDF e no MASM usando PROC..ENDP. Uma vez que ESP aponta para o topo da área de dados locais, é possível endereçar os dados usando ESP. Assim, no exemplo acima, o primeiro dado local dword estaria disponível em [ESP] imediatamente após o SUB ESP,20h. Porém, usando ESP para gerenciar os dados locais na pilha pode ser complicado porque ESP mudará a cada CALL ou PUSH dentro do procedimento. Por esta razão costuma-se usar o registrador EBP e não o ESP. Atribui-se um valor ao EBP logo no início do frame de pilha, na base dos dados locais, e o valor não é alterado enquanto a execução não abandonar o quadro. Desta forma, pode-se ter certeza de que os dados locais possam sempre ser endereçados usando um deslocamento (offset) de EBP. Agora, o código para um quadro de pilha típico passa a ter a seguinte aparência:
QuadroPilhaTipico:
PUSH EBP ;salva o valor de ebp que será alterado }
MOV EBP,ESP ;põe valor atual do ponteiro de pilha em ebp } "prólogo"
SUB ESP,0Ch ;make space for local data }
; ;PONTO "X"
;
; ;código dentro do procedimento
;
MOV ESP,EBP ;restaura o ponteiro da pilha }
POP EBP ;restaura o valor de ebp } "epílogo"
RET ;retorna ao chamador ajustando o ponteiro }
da pilha }
Aqui mudamos o ponteiro da pilha em 12 bytes. No ponto "X", a pilha em relação a EBP tem o seguinte aspecto:
Agora, por todo o quadro de pilha, seja o que for que acontecer a ESP, os dados locais estarão acessíveis em [EBP-4H], [EBP-8h] e [EBP-0Ch]. Observe como o equilíbrio de ESP é restaurado automaticamente pelo uso de MOV ESP,EBP pouco antes de retornar ao chamador. Não é obrigatório usar EBP para este fim, qualquer registrador é adequado. Acontece que o EBP é tradicionalmente usado para este fim e seu código será mais facilmente entendido por outros programadores. |
||||||||||||||||||||
Já vimos como passar parâmetros para outros procedimentos usando a pilha. Agora vamos analisar como usar parâmetros passados para procedimentos no seu próprio código. Basicamente, estes parâmetros estão mais em baixo na pilha para que não sejam sobre-escritos em circunstâncias normais. Por esta razão não há necessidade nenhuma de salvá-los ou resgatá-los. Depois de entrar num procedimento, ESP estará apontando para o endereço de retorno deste procedimento (inserido pelo CALL). Por isto, os parâmetros estarão em [ESP+4h], [ESP+8h], [ESP+0Ch] e assim por diante, dependendo de quantos parâmetros existirem. Mas pode ser difícil localizar onde exatamente estejam os parâmetros usando ESP porque o valor deste registrador mudará no próximo PUSH ou CALL. Mais uma vez o EBP pode ser usado para apontar os parâmetros. Se o código prólogo for PUSH EBP ;salva o valor de ebp que será alterado } MOV EBP,ESP ;põe valor do ponteiro da pilha atual em ebp } "prólogo" SUB ESP,0Ch ;cria espaço para dados locais } quando ESP for passado para EBP, ele terá 4 bytes a menos do que tinha no início da chamada (devido ao primeiro PUSH EBP). Portanto, os parâmetros agora podem ser acessados usando [EBP+8h], [EBP+0Ch], [EBP+10h] e asiim por diante, dependendo do número de parâmeros existentes. |
||||||||||||||||||||
As duas técnicas utilizadas (criando espaço para dados locais e endereçando parâmetros) são requeridas em procedimentos de callback do Windows. O procedimento callback mais comum em programas Windows é o procedimento janela. É para este procedimento que o Windows envia "mensagens" e o Windows espera uma resposta correta. O que acontece neste caso é que o Windows chama o procedimento janela usando o thread do próprio programa. Isto geralmente ocorre enquanto o programa estiver num loop de mensagem esperando um retorno da API GetMessage ou então executando a API DispatchMessage. Por sorte você pode usar FRAME..ENDF no GoAsm para obter os parâmetros enviados por janelas e também endereçá-los por nome. Você também pode criar com facilidade áreas de dados locais endereçáveis por nome. E você pode preservar registradores e restaurar o equilíbrio da pilha automaticamente. Veja o manual do GoAsm para uma descrição completa ou retorne para a parte 1 deste texto. |
||||||||||||||||||||
Belíssimo texto, né não? Se agora você ainda não sabe o que é e como funciona a pilha... 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 2) do referido autor. |
| Localizador | ||
|
| Localizador || @ Info NumaBoa > oicìliS > Assembly > Textos > A Pilha II Créditos: vovó Vicki webdesign sobMedida by vickiSoft - /informatica/oiciliS/assembler/textos/stack2.php (04.08.03) versão 1.0 de 04.08.03 Licença Creative Commons 1998-2006 Aldeia NumaBoa Exceto onde especificamente declarado, todo material deste site é disponibilizado de acordo com a Licença Creative Commons. | ||