Onde é empurrado?
esp - 4
. Mais precisamente:
esp
é subtraído por 4
- o valor é empurrado para
esp
pop
inverte isso.
A ABI do System V diz ao Linux para rsp
apontar para um local de pilha razoável quando o programa começa a ser executado: Qual é o estado de registro padrão quando o programa é iniciado (asm, linux)? que é o que você normalmente deve usar.
Como você pode empurrar um registro?
Exemplo mínimo de GNU GAS:
.data
/* .long takes 4 bytes each. */
val1:
/* Store bytes 0x 01 00 00 00 here. */
.long 1
val2:
/* 0x 02 00 00 00 */
.long 2
.text
/* Make esp point to the address of val2.
* Unusual, but totally possible. */
mov $val2, %esp
/* eax = 3 */
mov $3, %ea
push %eax
/*
Outcome:
- esp == val1
- val1 == 3
esp was changed to point to val1,
and then val1 was modified.
*/
pop %ebx
/*
Outcome:
- esp == &val2
- ebx == 3
Inverses push: ebx gets the value of val1 (first)
and then esp is increased back to point to val2.
*/
Acima no GitHub com asserções executáveis .
Por que isso é necessário?
É verdade que essas instruções podem ser facilmente implementadas via mov
, add
e sub
.
O motivo de sua existência é que essas combinações de instruções são tão frequentes que a Intel decidiu fornecê-las para nós.
A razão pela qual essas combinações são tão frequentes é que elas tornam mais fácil salvar e restaurar os valores dos registros na memória temporariamente para que eles não sejam substituídos.
Para entender o problema, tente compilar algum código C manualmente.
Uma grande dificuldade é decidir onde cada variável será armazenada.
Idealmente, todas as variáveis caberiam em registradores, que é a memória mais rápida para acessar (atualmente cerca de 100 vezes mais rápido que a RAM).
Mas é claro que podemos facilmente ter mais variáveis do que registradores, especialmente para os argumentos de funções aninhadas, então a única solução é escrever na memória.
Poderíamos escrever em qualquer endereço de memória, mas como as variáveis locais e argumentos de chamadas de função e retornos se encaixam em um bom padrão de pilha, que evita a fragmentação da memória , essa é a melhor maneira de lidar com isso. Compare isso com a insanidade de escrever um alocador de heap.
Então, deixamos que os compiladores otimizem a alocação de registros para nós, já que isso é NP completo e uma das partes mais difíceis de escrever um compilador. Esse problema é chamado de alocação de registro e é isomórfico à coloração do gráfico .
Quando o alocador do compilador é forçado a armazenar coisas na memória em vez de apenas registros, isso é conhecido como derramamento .
Isso se resume a uma única instrução do processador ou é mais complexa?
Tudo o que sabemos com certeza é que a Intel documenta push
uma pop
instrução e uma instrução, portanto, são uma instrução nesse sentido.
Internamente, ele poderia ser expandido para vários microcódigos, um para modificar esp
e outro para fazer o IO de memória, e levar vários ciclos.
Mas também é possível que um único push
seja mais rápido do que uma combinação equivalente de outras instruções, uma vez que é mais específico.
Isso está principalmente sub (der) documentado:
b
,w
,l
, ouq
para denotar o tamanho da memória que está sendo manipulado. Ex:pushl %eax
epopl %eax