Redescobrindo Assembly: Entendendo a "Stack" em sua forma mais primitiva (em Assembly) - EximiaCo.Tech
Conhecer assembly, geralmente, não é fundamental para o dia a dia de um programador. Entretanto, entender como assembly funciona ajuda a valorizar determinadas características de linguagens de programação de nível mais alto e, até mesmo, deixar mais confortável cenários mais complexos de depuração ou otimização.
Um dos conceitos centrais de programação em assembly que ajudam a entender o comportamento do código, em uma linguagem como Java ou C#, é a stack.
Trata-se de uma área contínua de memória que nossos programas utilizam para manipular dados, principalmente primitivos, e é intensamente utilizada na "comunicação" durante chamadas e retornos de funções.
Fundamentos para Performance: Entendendo a Stack (e StackOverflowException)
Os dados são "empilhados" na stack usando, em assembly, a instrução push e são desempilhados usando a instrução pop. O endereço de memória correspondente ao "topo" da stack é mantindo no registrador esp.
O topo da pilha "cresce negativamente", de high-memory para low-memory. Assim, sempre que um valor é empilhado na stack, o valor de esp é decrescido. De maneira análoga, sempre que um valor é "desempilhado" da stack, o valor de esp é acrescido.
Qual a relação entre a Stack e a execução de funções?
De maneira geral, sempre que uma função é chamada, os dados necessários para sua execução são dispostos na stack obedecendo uma determinada convenção. Voltando ao código do post anterior, podemos perceber no "header" da função addInAsm a convenção que deveria ser respeitada (cdecl).
extern "C" int addInAsm(int a, int b);
int result = addInAsm(a, b);
std::cout << a << " + " << b << " = " << result << std::endl;
O código em assembly que escrevemos utiliza "empiricamente" o que está acordado na convenção para acessar os dados.
addInAsm proc
; Initialize a stack frame pointer
; load the paramaters
mov  eax, [ebp + 8]    ; eax = a
mov  ecx, [ebp + 12]   ; ecx = b
; restore the stack frame
addInAsm endp
Segundo as convenções, a stack foi atualizada, na chamada, para conter, os parâmetros da função e o endereço de retorno para quando a função encerrar.
Perceba que códigos em assembly não empregam sistemas sofisticados de tipos. Ou seja, não há abstrações com relação a valores em memória – tudo são bytes que ganham significado conforme a intenção do código e são acessados através dos deslocamentos impostos pelo "tamanho" de cada dado.
O endereço de retorno é capturado pela instrução ret, diretamente da stack, para saber onde está a próxima instrução da função chamadora (main) a ser procesada.
Qual a relação entre a Stack e a parâmetros "byref"?
Quando passamos parâmetros "por referência", mandamos na Stack, no lugar dos valores, os endereços de memória correspondendo as variáveis que desejamos atualizar.
extern "C" void addMul(int a, int b, int* sum, int* prod);
addMul(a, b, &sum, &prod);
"The sum of "
<< a <<
<< b <<
<< sum << " and the product is " << prod
No exemplo, os parâmetros prod e sum são passados como referência, mas a estrutura na stack é praticamente inalterada.
addMul proc
mov ecx,[ebp+8]                     ;ecx = 'a'
mov eax, ecx                        ;eax = 'a'
mov edx,[ebp+12]                    ;edx = 'b'
imul ecx,edx                        ;edx = 'a' * 'b'
mov ebx,[ebp+20]                    ;ebx = 'prod'
mov [ebx],ecx                       ;save product
add eax, edx                        ;eax = 'a' + 'b'
mov ebx,[ebp+16]                    ;ebx = 'sum'
mov [ebx],eax                       ;save sum
addMul endp
A mudança na stack fica apenas nas duas novas posições necessárias para acomodar os novos valores.
No código em assembly, repare que utilizemos os "endereços" contidos nos parâmetros e não seus valores diretamente (como fazemos para as variáveis a e b).
O que acontece se a memória destinada para a Stack for esgotada?
O resultado depende do ambiente operacional onde estamos trabalhando. Em C#, por exemplo, uma StackOverflowException irá ser disparada e o programa se encerrará.
Importante indicar que o "esgotamento" da Stack geralmente é causado por execuções recursivas em demasia. Afinal, a stack é utilizada para fazer "o caminho de volta" na execução de diversas funções.
Eventualmente, podemos escrever código que ajuda o compilador a entender que "não será necessário" voltar para a função quando houver um retorno.
Alguns compiladores conseguem identificar esses cenários e não criar um registro na stack para cada chamada.
A stack é um dos conceitos fundamentais para execução de programas de computador em qualquer ambiente moderno. Em seu formato mais "bruto" trata-se apenas de um espaço contínuo de memória que é atualizado seguindo algumas convenções muito simples.
O conhecimento sobre como a Stack é manipulada, em seu estado mais fundamental, ajuda programadores a apreciar o bom trabalho dos compiladores e entender oportunidades de otimização.
Saber assembly não é fundamental. Entretanto, é importante que todos saibamos que programas modernos, mesmo os escritos em linguagens mais sofisticadas, em sua execução utilizam uma implementação primitiva de Stack - na prática, uma área contínua de memória utilizada na "comunicação" que ocorre na chamada e no retorno das funções.
Conhecer como a "stack" é utilizada, escrevendo algum código em assembly, ajuda a entender como a dinâmica de execução das aplicações modernas acontece. Eventualmente, ajuda a estruturar programas para que eles sejam mais eficientes.