Entendendo o quadro da pilha da chamada de função em C / C ++?

19

Estou tentando entender como os quadros de pilha são criados e quais variáveis ​​(parâmetros) são empurradas para empilhar em que ordem? Alguns resultados de pesquisa mostraram que o compilador C / C ++ decide com base nas operações executadas em uma função. Por exemplo, se a função deveria incrementar apenas um valor int passado por 1 (semelhante ao operador ++) e retorná-lo, colocaria todos os parâmetros da função e variáveis ​​locais nos registradores.

Eu estou querendo saber quais registros são usados ​​para retornados ou passar por parâmetros de valor. Como as referências são retornadas? Como o compilador escolhe entre eax, ebx, ecx e edx?

O que eu preciso saber para entender como registros, pilha e referências de pilha são usados, construídos e destruídos durante chamadas de função?

Gana
fonte
isso é bastante difícil de ler (parede de texto). Você se importaria de editar sua postagem para uma forma melhor?
Gnat
1
Essa questão me parece bastante ampla. Além disso, isso não será muito específico da plataforma?
Kazark
Pergunta também foi questionado sobre SO: stackoverflow.com/questions/16088040/...
Wayne Conrad
Veja também a minha resposta no SO
Basile Starynkevitch

Respostas:

11

Além do que Dirk disse, um uso importante dos quadros de pilha é salvar os valores anteriores dos registradores para que possam ser restaurados após uma chamada de função. Portanto, mesmo nos processadores em que os registros são usados ​​para passar parâmetros, retornar um valor e salvar o endereço de retorno, os valores desses registros são salvos na pilha antes de uma chamada de função, para que possam ser restaurados após a chamada. Isso permite que uma função chame outra sem sobrescrever seus próprios parâmetros ou esquecer seu próprio endereço de retorno.

Portanto, chamar uma função B da função A em um sistema "genérico" típico pode envolver as seguintes etapas:

  • função A:
    • empurre espaço para o valor de retorno
    • empurrar parâmetros
    • empurre o endereço de retorno
  • pule para a função B
  • função B:
    • pressione o endereço do quadro de pilha anterior
    • empurre os valores dos registradores que essa função usa (para que possam ser restaurados)
    • empurre espaço para variáveis ​​locais
    • faça o cálculo necessário
    • restaurar os registros
    • restaurar o quadro de pilha anterior
    • armazena o resultado da função
    • pule para o endereço de retorno
  • função A:
    • pop os parâmetros
    • pop o valor de retorno

Isso não é de forma alguma a única maneira pela qual as chamadas de função podem funcionar (e eu posso ter uma ou duas etapas desordenadas), mas você deve ter uma idéia de como a pilha é usada para permitir que o processador lide com chamadas de função aninhadas.

Caleb
fonte
O que "push" significa exatamente aqui? Não tenho ideia do que fazer com isso.
Tomáš Zato - Restabelece Monica
2
@ TomášZato pushe popsão as duas operações fundamentais em uma pilha. Uma pilha é uma estrutura de último a entrar, primeiro a sair, como uma pilha de livros. Quando você pushcoloca um novo objeto no topo da pilha; quando você popestá tirando um objeto do topo da pilha. Você não tem permissão para inserir ou remover objetos no meio, você só pode operar na parte superior da pilha. Você pode ler mais sobre pilhas em geral e a pilha de programas em particular na Wikipedia.
Caleb Caleb
11

Isso depende da convenção de chamada que está sendo usada. Quem define a convenção de chamada pode tomar essa decisão da maneira que desejar.

Na convenção de chamada mais comum no x86, os registradores não são usados ​​para passar parâmetros; os parâmetros são enviados para a pilha começando com o parâmetro mais à direita. O valor de retorno é colocado em eax e pode usar edx se precisar de espaço extra. Referências e ponteiros são retornados na forma de um endereço em eax.

Dirk Holsopple
fonte
5

Se você entender muito bem a pilha, entenderá como a memória funciona no programa e se entenderá como a memória funciona no programa, entenderá como o armazenamento de funções no programa e se entenderá como o armazenamento de funções no programa entenderá como funciona a função recursiva e se você entende como a função recursiva funciona, entenderá como o compilador funciona e, se entender como o compilador funciona, sua mente funcionará como compilador e você depurará qualquer programa com muita facilidade.

Deixe-me explicar como a pilha funciona:

Primeiro você precisa saber como as funções armazenam na pilha:

Os valores de alocação de memória dinâmica de armazenamento de heap. Valores automáticos de alocação e exclusão de armazenamento de pilha.

insira a descrição da imagem aqui

Vamos entender com o exemplo:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(4)

Agora entenda partes deste programa:

insira a descrição da imagem aqui

Agora vamos ver o que é pilha e o que são partes da pilha:

insira a descrição da imagem aqui

Alocação da pilha:

Lembre-se de uma coisa: se alguma função obtiver "retorno", não importa que tenha carregado todas as suas variáveis ​​locais ou qualquer coisa que ela retorne imediatamente da pilha, será o quadro da pilha. Significa que quando qualquer função recursiva obtém a condição base e colocamos retorno após a condição base, para que a condição base não espere para carregar variáveis ​​locais que estão localizadas na parte “else” do programa, ela retornará imediatamente o quadro atual da pilha e agora se um quadro retornar próximo quadro está no registro de ativação. Veja isso na prática:

insira a descrição da imagem aqui

Desalocação do bloco:

Portanto, agora, sempre que uma função encontrada retorna a instrução, ela exclui o quadro atual da pilha.

enquanto retornar do valor da pilha retornará na ordem inversa da ordem em que foram alocados na pilha.

insira a descrição da imagem aqui

Essas são descrições muito curtas e, se você quiser saber mais sobre pilha e recursão dupla, leia dois posts deste blog:

Mais sobre a pilha passo a passo

Mais sobre recursão dupla passo a passo com pilha

user5904928
fonte
3

O que você está procurando é chamado Application Binary Interface - ABI.

Há uma especificação para cada compilador que especifica a ABI.

Cada plataforma geralmente especifica e ABI para oferecer suporte à interoperabilidade entre compiladores. Por exemplo, as convenções de chamada do x86 explicitam as convenções de chamada típicas do x86 e x86-64. Eu esperaria um documento mais oficial do que a Wikipedia, no entanto.

Bill Door
fonte