A pilha cresce para cima ou para baixo?

90

Eu tenho este trecho de código em c:

O resultado é:

Portanto, vejo que de apara a[2], os endereços de memória aumentam em 4 bytes cada. Mas de qpara s, os endereços de memória diminuem em 4 bytes.

Eu me pergunto 2 coisas:

  1. A pilha aumenta ou diminui? (Parece que ambos neste caso)
  2. O que acontece entre os endereços de memória a[2]e q? Por que há uma grande diferença de memória aí? (20 bytes).

Observação: esta não é uma questão de dever de casa. Estou curioso para saber como funciona a pilha. Obrigado por qualquer ajuda.

Spikatrix
fonte
A ordem é arbitrária. A lacuna provavelmente é para armazenar um resultado intermediário, como & q ou & s - olhe para a desmontagem e veja por si mesmo.
Tom Leys
Eu concordo, leia o código de montagem. Se você está fazendo esse tipo de pergunta, é hora de aprender a ler.
Por Johansson
Uma versão de montagem mais simples de responder: stackoverflow.com/questions/664744/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respostas:

74

O comportamento da pilha (crescendo ou diminuindo) depende da interface binária do aplicativo (ABI) e de como a pilha de chamadas (também conhecida como registro de ativação) está organizada.

Ao longo de sua vida, um programa é obrigado a se comunicar com outros programas, como o sistema operacional. ABI determina como um programa pode se comunicar com outro programa.

A pilha para diferentes arquiteturas pode crescer de qualquer maneira, mas para uma arquitetura será consistente. Por favor, verifique este link do wiki. Porém, o crescimento da pilha é decidido pela ABI dessa arquitetura.

Por exemplo, se você pegar o MIPS ABI, a pilha de chamadas é definida conforme abaixo.

Vamos considerar que a função 'fn1' chama 'fn2'. Agora, o frame da pilha visto por 'fn2' é o seguinte:

Agora você pode ver que a pilha cresce para baixo. Portanto, se as variáveis ​​são alocadas ao quadro local da função, os endereços das variáveis ​​na verdade crescem para baixo. O compilador pode decidir sobre a ordem das variáveis ​​para alocação de memória. (No seu caso, pode ser 'q' ou 's' que é a primeira alocação de memória de pilha. Mas, geralmente o compilador faz a alocação de memória de pilha de acordo com a ordem de declaração das variáveis).

Mas no caso dos arrays, a alocação possui apenas um único ponteiro e a memória que precisa ser alocada será na verdade apontada por um único ponteiro. A memória precisa ser contígua para uma matriz. Portanto, embora a pilha cresça para baixo, para os arrays, a pilha aumenta.

Ganesh Gopalasubramanian
fonte
5
Além disso, se você deseja verificar se a pilha cresce para cima ou para baixo. Declare uma variável local na função principal. Imprima o endereço da variável. Chame outra função principal. Declare uma variável local na função. Imprima seu endereço. Com base nos endereços impressos, podemos dizer que a pilha aumenta ou diminui.
Ganesh Gopalasubramanian
obrigado Ganesh, eu tenho uma pequena dúvida: na figura que você desenhou, no terceiro bloco, você quis dizer "registro salvo calleR sendo usado em CALLER" porque quando f1 chama f2, temos que armazenar o endereço f1 (que é o endereço de retorno para registradores f2) e f1 (calleR), não registradores f2 (callee). Direito?
CSawy
43

Na verdade, são duas perguntas. Uma é sobre a maneira como a pilha cresce quando uma função chama outra (quando um novo quadro é alocado) e a outra é sobre como as variáveis ​​são dispostas em um quadro de função específica.

Nenhum dos dois é especificado pelo padrão C, mas as respostas são um pouco diferentes:

  • De que maneira a pilha cresce quando um novo quadro é alocado - se a função f () chamar a função g (), fo ponteiro do quadro será maior ou menor que go ponteiro do quadro? Isso pode acontecer de qualquer maneira - depende do compilador e da arquitetura em particular (pesquise "convenção de chamada"), mas é sempre consistente em uma determinada plataforma (com algumas exceções bizarras, consulte os comentários). Para baixo é mais comum; é o caso em x86, PowerPC, MIPS, SPARC, EE e as SPUs de célula.
  • Como as variáveis ​​locais de uma função são dispostas dentro de sua estrutura de pilha? Isso não é especificado e totalmente imprevisível; o compilador é livre para organizar suas variáveis ​​locais da maneira que quiser para obter o resultado mais eficiente.
Crashworks
fonte
7
"é sempre consistente dentro de uma determinada plataforma" - não garantido. Eu vi uma plataforma sem memória virtual, onde a pilha foi estendida dinamicamente. Na verdade, novos blocos de pilha foram malocalizados, o que significa que você iria "descer" um bloco de pilha por um tempo e, de repente, "para os lados" em um bloco diferente. "Sideways" pode significar um endereço maior ou menor, inteiramente por causa do sorteio.
Steve Jessop
2
Para detalhes adicionais ao item 2 - um compilador pode ser capaz de decidir que uma variável nunca precisa estar na memória (mantendo-a em um registro durante a vida da variável), e / ou se o tempo de vida de duas ou mais variáveis ​​não t se sobrepor, o compilador pode decidir usar a mesma memória para mais de uma variável.
Michael Burr
2
Acho que S / 390 (IBM zSeries) tem uma ABI onde os call frames são vinculados em vez de crescer em uma pilha.
efemiente
2
Correto em S / 390. Uma chamada é "BALR", registro de ramificação e link. O valor de retorno é colocado em um registro em vez de colocado em uma pilha. A função de retorno é um desvio para o conteúdo desse registro. Conforme a pilha fica mais profunda, o espaço é alocado na pilha e eles são encadeados. É aqui que o equivalente MVS de "/ bin / true" obtém seu nome: "IEFBR14". A primeira versão continha uma única instrução: "BR 14", que se ramificava para o conteúdo do registrador 14 que continha o endereço de retorno.
janeiro
1
E alguns compiladores em processadores PIC fazem a análise de todo o programa e localizações fixas alocadas para cada variável automática de função; a pilha real é pequena e não pode ser acessada pelo software; é apenas para endereços de retorno.
janeiro
13

A direção que as pilhas crescem é específica da arquitetura. Dito isso, meu entendimento é que apenas algumas poucas arquiteturas de hardware têm pilhas que crescem.

A direção em que uma pilha cresce é independente do layout de um objeto individual. Portanto, embora a pilha possa diminuir, os arrays não irão (ou seja, & array [n] será sempre <& array [n + 1]);

R Samuel Klatchko
fonte
4

Não há nada no padrão que determine como as coisas são organizadas na pilha. Na verdade, você poderia construir um compilador em conformidade que não armazenasse elementos da matriz em elementos contíguos na pilha, desde que tivesse a inteligência para ainda fazer aritmética de elementos da matriz corretamente (para que soubesse, por exemplo, que um 1 era 1K de distância de um [0] e poderia ajustar para isso).

A razão pela qual você pode estar obtendo resultados diferentes é porque, embora a pilha possa diminuir para adicionar "objetos" a ela, a matriz é um único "objeto" e pode ter elementos de matriz ascendentes na ordem oposta. Mas não é seguro confiar nesse comportamento, já que a direção pode mudar e as variáveis ​​podem ser trocadas por uma variedade de razões, incluindo, mas não se limitando a:

  • otimização.
  • alinhamento.
  • os caprichos da pessoa que faz parte do gerenciamento de pilha do compilador.

Veja aqui o meu excelente tratado sobre a direção da pilha :-)

Em resposta às suas perguntas específicas:

  1. A pilha aumenta ou diminui?
    Não importa em absoluto (em termos de padrão), mas, desde que você perguntou, pode aumentar ou diminuir na memória, dependendo da implementação.
  2. O que acontece entre os endereços de memória a [2] eq? Por que há uma grande diferença de memória aí? (20 bytes)?
    Não importa de forma alguma (em termos de padrão). Veja acima para possíveis razões.
paxdiablo
fonte
Eu vi você vincular que a maioria das arquiteturas de CPU adota a maneira "crescer para baixo", você sabe se há alguma vantagem em fazer isso?
baye
Não faço ideia, realmente. É possível que alguém tenha pensado que o código vai para cima de 0, portanto, a pilha deve ir para baixo a partir de highmem, de modo a minimizar a possibilidade de interseção. Mas algumas CPUs iniciam especificamente a execução do código em locais diferentes de zero, então esse pode não ser o caso. Como a maioria das coisas, talvez tenha sido feito dessa forma simplesmente porque foi a primeira maneira que alguém pensou em fazer :-)
paxdiablo
@lzprgmr: Existem algumas pequenas vantagens em ter certos tipos de alocação de heap executados em ordem crescente e, historicamente, é comum que a pilha e o heap fiquem em extremidades opostas de um espaço de endereçamento comum. Desde que o uso combinado de estática + heap + pilha não exceda a memória disponível, não é necessário se preocupar com a quantidade exata de memória usada pelo programa.
supercat
3

Em um x86, a "alocação" de memória de um frame de pilha consiste simplesmente em subtrair o número necessário de bytes do ponteiro de pilha (acredito que outras arquiteturas são semelhantes). Nesse sentido, acho que a pilha cresce "para baixo", na medida em que os endereços ficam progressivamente menores conforme você chama mais profundamente na pilha (mas sempre imagino a memória começando com 0 no canto superior esquerdo e ficando maiores endereços conforme você se move para a direita e empacotar, então na minha imagem mental a pilha cresce ...). A ordem das variáveis ​​declaradas pode não ter qualquer relação com seus endereços - acredito que o padrão permite que o compilador as reordene, desde que não cause efeitos colaterais (alguém, por favor, me corrija se eu estiver errado) . Eles'

A lacuna em torno da matriz pode ser algum tipo de preenchimento, mas é misteriosa para mim.

rmeador
fonte
1
Na verdade, eu sei que o compilador pode reordená-los, porque também é livre para não alocá-los. Ele pode simplesmente colocá-los em registradores e não usar nenhum espaço de pilha.
rmeador
Ele não pode colocá-los nos registros se você referenciar seus endereços.
florim
bom ponto, não tinha considerado isso. mas ainda é suficiente como prova de que o compilador pode reordená-los, já que sabemos que ele pode fazer isso pelo menos algumas vezes :)
rmeador
1

Em primeiro lugar, são 8 bytes de espaço não utilizado na memória (não é 12, lembre-se de que a pilha cresce para baixo, então o espaço que não é alocado é de 604 para 597). e porque?. Porque cada tipo de dado ocupa espaço na memória a partir do endereço divisível por seu tamanho. No nosso caso, o array de 3 inteiros ocupa 12 bytes de espaço de memória e 604 não é divisível por 12. Portanto, ele deixa espaços vazios até encontrar um endereço de memória que é divisível por 12, é 596.

Portanto, o espaço de memória alocado para o array é de 596 a 584. Mas como a alocação do array está em continuação, o primeiro elemento do array começa no endereço 584 e não no 596.

Prateek Khurana
fonte
1

O compilador é livre para alocar variáveis ​​locais (automáticas) em qualquer lugar no frame da pilha local, você não pode inferir com segurança a direção de crescimento da pilha puramente disso. Você pode inferir a direção de crescimento da pilha comparando os endereços de frames de pilha aninhados, ou seja, comparando o endereço de uma variável local dentro do frame de pilha de uma função em comparação com seu receptor:

matja
fonte
5
Tenho certeza de que é um comportamento indefinido subtrair ponteiros para objetos de pilha diferentes - ponteiros que não fazem parte do mesmo objeto não são comparáveis. Obviamente, ele não irá travar em nenhuma arquitetura "normal".
Steve Jessop
@SteveJessop Existe alguma maneira de corrigir isso para obter a direção da pilha de forma programática?
xxks-kkk
@ xxks-kkk: em princípio não, porque uma implementação em C não precisa ter uma "direção de pilha". Por exemplo, não violaria o padrão ter uma convenção de chamada em que um bloco de pilha é alocado antecipadamente e, em seguida, alguma rotina de alocação de memória interna pseudo-aleatória é usada para saltar dentro dele. Na prática, ele realmente funciona conforme a descrição de matja.
Steve Jessop
0

Eu não acho que seja determinístico assim. A matriz a parece "crescer" porque essa memória deve ser alocada de forma contígua. No entanto, uma vez que q e s não estão relacionados entre si, o compilador apenas coloca cada um deles em um local de memória livre arbitrário dentro da pilha, provavelmente aquele que se encaixa melhor em um tamanho inteiro.

O que aconteceu entre a [2] eq é que o espaço ao redor da localização de q não era grande o suficiente (ou seja, não era maior que 12 bytes) para alocar um array de 3 inteiros.

javanix
fonte
em caso afirmativo, por que q, s, a não possuem memória contingente? (Ex: Endereço de q: 2293612 Endereço de s: 2293608 Endereço de a: 2293604)
Vejo uma "lacuna" entre s e a
Porque s e a não foram alocados juntos - os únicos ponteiros que precisam ser contíguos são aqueles na matriz. A outra memória pode ser alocada em qualquer lugar.
javanix
0

Minha pilha parece se estender para endereços com números menores.

Pode ser diferente em outro computador, ou mesmo no meu próprio computador, se eu usar uma chamada de compilador diferente. ... ou o compilador deve escolher não usar pilha (tudo embutido (funções e variáveis ​​se eu não pegar o endereço delas)).

$ / usr / bin / gcc -Wall -Wextra -std = c89 -pedantic stack.c
$ ./a.out
nível 4: x está em 0x7fff7781190c
nível 3: x está em 0x7fff778118ec
nível 2: x está em 0x7fff778118cc
nível 1: x está em 0x7fff778118ac
nível 0: x está em 0x7fff7781188c
pmg
fonte
0

A pilha diminui (em x86). No entanto, a pilha é alocada em um bloco quando a função é carregada e você não tem uma garantia da ordem em que os itens estarão na pilha.

Nesse caso, ele alocou espaço para dois ints e uma matriz de três int na pilha. Ele também alocou 12 bytes adicionais após a matriz, então tem a seguinte aparência:

a [12 bytes]
preenchimento (?) [12 bytes]
s [4 bytes]
q [4 bytes]

Por alguma razão, seu compilador decidiu que precisava alocar 32 bytes para esta função, e possivelmente mais. Isso é opaco para você como um programador C, você não sabe por quê.

Se você quiser saber o porquê, compile o código para a linguagem assembly, acredito que seja -S no gcc e / S no compilador C da MS. Se você olhar as instruções de abertura dessa função, verá o ponteiro da pilha antigo sendo salvo e, em seguida, 32 (ou outra coisa!) Sendo subtraído dele. A partir daí, você pode ver como o código acessa aquele bloco de memória de 32 bytes e descobrir o que seu compilador está fazendo. No final da função, você pode ver o ponteiro da pilha sendo restaurado.

Aric TenEyck
fonte
0

Depende do seu sistema operacional e do seu compilador.

David R Tribble
fonte
Não sei por que minha resposta foi rejeitada. Realmente depende do seu sistema operacional e compilador. Em alguns sistemas, a pilha cresce para baixo, mas em outros, para cima. E em alguns sistemas, não há uma pilha de quadros push-down real, mas sim simulada com uma área reservada de memória ou conjunto de registros.
David R Tribble
3
Provavelmente porque afirmações de uma única frase não são boas respostas.
Lightness Races in Orbit
0

A pilha diminui. Portanto, f (g (h ())), a pilha alocada para h começará no endereço menor que g e g's serão menores que f's. Mas as variáveis ​​dentro da pilha devem seguir a especificação C,

http://c0x.coding-guidelines.com/6.5.8.html

1206 Se os objetos apontados são membros do mesmo objeto agregado, ponteiros para membros da estrutura declarados posteriormente são comparados com ponteiros para membros declarados anteriormente na estrutura e ponteiros para elementos da matriz com valores de subscrito maiores são comparados com ponteiros para elementos do mesmo array com valores de subscrito mais baixos.

& a [0] <& a [1], deve sempre ser verdadeiro, independentemente de como 'a' é alocado

user1187902
fonte
Na maioria das máquinas, a pilha cresce para baixo - exceto para aquelas onde ela cresce para cima.
Jonathan Leffler
0

cresce para baixo e isso é devido ao padrão de ordem de bytes little endian quando se trata do conjunto de dados na memória.

Uma maneira de ver isso é que a pilha CRESCE para cima se você olhar para a memória de 0 do topo e máximo de baixo.

A razão para a pilha crescer para baixo é ser capaz de remover a referência da perspectiva da pilha ou do ponteiro da base.

Lembre-se de que a desreferenciação de qualquer tipo aumenta do endereço mais baixo para o mais alto. Como a pilha cresce para baixo (endereço do mais alto para o mais baixo), isso permite que você trate a pilha como memória dinâmica.

Esse é um dos motivos pelos quais tantas linguagens de programação e script usam uma máquina virtual baseada em pilha em vez de uma baseada em registro.

Nergal
fonte
The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.Muito bom raciocínio
user3405291
0

Depende da arquitetura. Para verificar seu próprio sistema, use este código de GeeksForGeeks :

kurdtpage
fonte