Qual é a função das instruções push / pop usadas em registros no assembly x86?

94

Quando leio sobre assembler, frequentemente encontro pessoas escrevendo que empurram um certo registro do processador e o pop novamente mais tarde para restaurar seu estado anterior.

  • Como você pode empurrar um registro? Onde é empurrado? Por que isso é necessário?
  • Isso se resume a uma única instrução do processador ou é mais complexa?
Emblema Ars
fonte
3
Aviso: todas as respostas atuais são fornecidas na sintaxe de montagem da Intel; push-pop no AT & T sintaxe, por exemplo, usa um pós-fix como b, w, l, ou qpara denotar o tamanho da memória que está sendo manipulado. Ex: pushl %eaxepopl %eax
Hawken
5
@hawken Na maioria dos montadores capazes de engolir a sintaxe AT&T (notavelmente gás), o pós-fixado do tamanho pode ser omitido se o tamanho do operando puder ser deduzido do tamanho do operando. Este é o caso dos exemplos que você deu, pois %eaxsempre tem tamanho de 32 bits.
Gunther Piez

Respostas:

147

empurrar um valor (não necessariamente armazenado em um registro) significa gravá-lo na pilha.

popping significa restaurar o que quer que esteja no topo da pilha em um registrador. Essas são instruções básicas:

push 0xdeadbeef      ; push a value to the stack
pop eax              ; eax is now 0xdeadbeef

; swap contents of registers
push eax
mov eax, ebx
pop ebx
Linus Kleen
fonte
3
O operando explícito para push e pop é r/m, não apenas registrar, então você pode push dword [esi]. Ou até mesmo pop dword [esp]para carregar e armazenar o mesmo valor de volta no mesmo endereço. ( github.com/HJLebbink/asm-dude/wiki/POP ). Só menciono isso porque você diz "não necessariamente um registro".
Peter Cordes
Você também pode popem uma área da memória:pop [0xdeadbeef]
SS Anne
Olá, qual é a diferença entre push / pop e pushq / popq? Estou em macos / intel
SteakOverflow
42

Aqui está como você empurra um registro. Presumo que estamos falando sobre x86.

push ebx
push eax

Ele é colocado na pilha. O valor do ESPregistro é decrementado para o tamanho do valor enviado conforme a pilha diminui em sistemas x86.

É preciso preservar os valores. O uso geral é

push eax           ;   preserve the value of eax
call some_method   ;   some method is called which will put return value in eax
mov  edx, eax      ;    move the return value to edx
pop  eax           ;    restore original eax

A pushé uma única instrução em x86, que faz duas coisas internamente.

  1. Armazene o valor enviado no endereço atual do ESPregistro.
  2. Diminua o ESPregistro para o tamanho do valor enviado.
Madhur Ahuja
fonte
7
1. e 2. devem ser reorganizados
vavan
@vavan acaba de enviar uma solicitação para que seja consertado
jgh fun-run
38

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 rspapontar 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, adde 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 pushuma popinstruçã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 espe outro para fazer o IO de memória, e levar vários ciclos.

Mas também é possível que um único pushseja 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:

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fonte
4
Você não precisa adivinhar como push/ popdecodificar em uops. Graças aos contadores de desempenho, testes experimentais são possíveis, e Agner Fog fez isso e publicou tabelas de instruções . Pentium-M e CPUs posteriores possuem uop único push/ popgraças ao mecanismo de pilha (veja o pdf microarch de Agner). Isso inclui CPUs AMD recentes, graças ao acordo de compartilhamento de patentes Intel / AMD.
Peter Cordes
@PeterCordes incrível! Então, os contadores de desempenho são documentados pela Intel para contar micro-operações?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Além disso, as variáveis ​​locais derramadas de regs normalmente ainda estarão quentes no cache L1 se alguma delas estiver realmente sendo usada. Mas a leitura de um registrador é efetivamente gratuita, latência zero. Portanto, é infinitamente mais rápido que o cache L1, dependendo de como você deseja definir os termos. Para locais somente leitura transferidos para a pilha, o custo principal é apenas uops de carga extra (às vezes operandos de memória, às vezes com movcargas separadas ). Para variáveis ​​não constantes derramadas, as viagens de ida e volta de encaminhamento à loja são muito mais latentes (um extra de ~ 5c em comparação ao encaminhamento direto e as instruções da loja não são baratas).
Peter Cordes
Sim, há contadores para uops totais em alguns estágios de pipeline diferentes (emitir / executar / retirar), então você pode contar o domínio fundido ou o domínio não fundido. Veja esta resposta por exemplo. Se eu estivesse reescrevendo essa resposta agora, usaria o ocperf.pyscript de wrapper para obter nomes simbólicos fáceis para os contadores.
Peter Cordes
22

Os registros de push e popping estão nos bastidores equivalentes a este:

push reg   <= same as =>      sub  $8,%rsp        # subtract 8 from rsp
                              mov  reg,(%rsp)     # store, using rsp as the address

pop  reg    <= same as=>      mov  (%rsp),reg     # load, using rsp as the address
                              add  $8,%rsp        # add 8 to the rsp

Observe que esta é a sintaxe At & t x86-64.

Usado como um par, permite salvar um registro na pilha e restaurá-lo posteriormente. Existem outros usos também.

gowrath
fonte
4
Sim, essas sequências emulam corretamente o push / pop. (exceto push / pop não afeta os sinalizadores).
Peter Cordes
2
É melhor usar em lea rsp, [rsp±8]vez de add/ subpara emular melhor o efeito de push/ popnos sinalizadores.
Ruslan
12

Quase todas as CPUs usam pilha. A pilha do programa é a técnica LIFO com gerenciamento suportado por hardware.

Pilha é a quantidade de memória de programa (RAM) normalmente alocada no topo do heap de memória da CPU e cresce (na instrução PUSH, o ponteiro da pilha é diminuído) na direção oposta. Um termo padrão para inserir na pilha é PUSH e para remover da pilha é POP .

A pilha é gerenciada por meio do registro de CPU pretendido pela pilha, também chamado de ponteiro de pilha, então quando a CPU executa POP ou PUSH, o ponteiro de pilha carrega / armazena um registro ou constante na memória de pilha e o ponteiro de pilha diminui automaticamente xou aumenta de acordo com o número de palavras empurradas ou colocado na (da) pilha.

Através das instruções do montador, podemos armazenar para empilhar:

  1. Registros da CPU e também constantes.
  2. Retornar endereços para funções ou procedimentos
  3. Funções / procedimentos de entrada / saída de variáveis
  4. Variáveis ​​locais de funções / procedimentos.
GJ.
fonte