스택이 위로 또는 아래로 증가합니까?


90

이 코드는 c에 있습니다.

int q = 10;
int s = 5;
int a[3];

printf("Address of a: %d\n",    (int)a);
printf("Address of a[1]: %d\n", (int)&a[1]);
printf("Address of a[2]: %d\n", (int)&a[2]);
printf("Address of q: %d\n",    (int)&q);
printf("Address of s: %d\n",    (int)&s);

출력은 다음과 같습니다.

Address of a: 2293584
Address of a[1]: 2293588
Address of a[2]: 2293592
Address of q: 2293612
Address of s: 2293608

따라서에서 a으로 a[2]메모리 주소가 각각 4 바이트 씩 증가 한다는 것을 알 수 있습니다 . 그러나에서 q까지 s메모리 주소는 4 바이트 감소합니다.

두 가지가 궁금합니다.

  1. 스택이 증가하거나 감소합니까? (이 경우에는 둘 다처럼 보입니다)
  2. a[2]q메모리 주소 사이에는 어떤 일이 발생 합니까? 왜 거기에 큰 메모리 차이가 있습니까? (20 바이트).

참고 : 이것은 숙제 질문이 아닙니다. 스택이 어떻게 작동하는지 궁금합니다. 도움을 주셔서 감사합니다.


순서는 임의적입니다. 그 차이는 아마도 & q 또는 & s와 같은 중간 결과를 저장하는 것입니다. 분해를보고 직접 확인하십시오.
Tom Leys

동의합니다. 어셈블리 코드를 읽으십시오. 이런 종류의 질문을하고 있다면 그것을 읽는 법을 배워야 할 때입니다.
Per Johansson

답변:


74

스택의 동작 (성장 또는 감소)은 ABI (애플리케이션 바이너리 인터페이스) 및 호출 스택 (활성화 레코드라고도 함)이 구성되는 방식에 따라 다릅니다.

평생 동안 프로그램은 OS와 같은 다른 프로그램과 통신해야합니다. ABI는 프로그램이 다른 프로그램과 통신하는 방법을 결정합니다.

다른 아키텍처의 스택은 어느 쪽이든 성장할 수 있지만 아키텍처의 경우 일관성이 있습니다. 위키 링크를 확인 하십시오. 그러나 스택의 성장은 해당 아키텍처의 ABI에 의해 결정됩니다.

예를 들어 MIPS ABI를 사용하는 경우 호출 스택은 다음과 같이 정의됩니다.

함수 'fn1'이 'fn2'를 호출한다고 가정 해 보겠습니다. 이제 'fn2'에 표시된 스택 프레임은 다음과 같습니다.

direction of     |                                 |
  growth of      +---------------------------------+ 
   stack         | Parameters passed by fn1(caller)|
from higher addr.|                                 |
to lower addr.   | Direction of growth is opposite |
      |          |   to direction of stack growth  |
      |          +---------------------------------+ <-- SP on entry to fn2
      |          | Return address from fn2(callee) | 
      V          +---------------------------------+ 
                 | Callee saved registers being    | 
                 |   used in the callee function   | 
                 +---------------------------------+
                 | Local variables of fn2          |
                 |(Direction of growth of frame is |
                 | same as direction of growth of  |
                 |            stack)               |
                 +---------------------------------+ 
                 | Arguments to functions called   |
                 | by fn2                          |
                 +---------------------------------+ <- Current SP after stack 
                                                        frame is allocated

이제 스택이 아래쪽으로 커지는 것을 볼 수 있습니다. 따라서 변수가 함수의 로컬 프레임에 할당되면 변수의 주소는 실제로 아래쪽으로 커집니다. 컴파일러는 메모리 할당을위한 변수의 순서를 결정할 수 있습니다. (귀하의 경우에는 처음 할당 된 스택 메모리 인 'q'또는 's'일 수 있습니다. 그러나 일반적으로 컴파일러는 변수 선언 순서에 따라 스택 메모리 할당을 수행합니다.)

그러나 배열의 경우 할당에는 단일 포인터 만 있으며 할당해야하는 메모리는 실제로 단일 포인터에 의해 가리 킵니다. 메모리는 배열에 대해 연속적이어야합니다. 따라서 스택이 아래쪽으로 커지더라도 어레이의 경우 스택이 커집니다.


5
또한 스택이 위로 또는 아래로 증가하는지 확인하려면. 주 함수에서 지역 변수를 선언하십시오. 변수의 주소를 인쇄하십시오. main에서 다른 함수를 호출하십시오. 함수에서 지역 변수를 선언하십시오. 주소를 정자로 기입하십시오. 인쇄 된 주소를 기반으로 스택이 증가하거나 감소한다고 말할 수 있습니다.
Ganesh Gopalasubramanian

감사합니다 Ganesh, 저는 작은 질문이 있습니다 : u 그린 그림에서, 세 번째 블록에서 u는 "calleR saved register being used in CALLER"를 의미합니까? f1이 f2를 호출 할 때 f1 주소를 저장해야하기 때문입니다. f2) 및 f1 (calleR)의 경우 f2 (callee) 레지스터가 아닙니다. 권리?
CSawy

43

이것은 실제로 두 가지 질문입니다. 하나는 한 함수가 다른 함수를 호출 할 때 스택이 커지는 방식 (새 프레임이 할당 될 때)에 관한 것이고 다른 하나는 변수가 특정 함수의 프레임에 배치되는 방식에 관한 것입니다.

둘 다 C 표준에 의해 지정되지 않지만 답변은 약간 다릅니다.

  • 새 프레임이 할당 될 때 스택은 어느 방향으로 커지는가? f () 함수가 g () 함수를 호출하면 f의 프레임 포인터가의 프레임 포인터보다 크거나 작을 g까요? 이것은 어느 쪽이든 갈 수 있습니다.-특정 컴파일러와 아키텍처 ( "호출 규칙"조회)에 따라 다르지만 항상 주어진 플랫폼 내에서 일관성이 있습니다 (몇 가지 기이 한 예외는 주석을 참조하십시오). 아래쪽이 더 일반적입니다. x86, PowerPC, MIPS, SPARC, EE 및 Cell SPU의 경우입니다.
  • 함수의 지역 변수는 스택 프레임 안에 어떻게 배치됩니까? 이것은 지정되지 않았고 완전히 예측할 수 없습니다. 컴파일러는 지역 변수를 자유롭게 배열 할 수 있지만 가장 효율적인 결과를 얻고 자합니다.

7
"주어진 플랫폼 내에서 항상 일관성이 있습니다."-보장되지 않습니다. 가상 메모리가없는 플랫폼을 보았는데 스택이 동적으로 확장되었습니다. 새로운 스택 블록은 실제로 malloced되었습니다. 즉, 잠시 동안 한 스택 블록을 "아래로"이동 한 다음 갑자기 다른 블록으로 "옆으로"이동합니다. "사이드 웨이"는 더 큰 또는 더 적은 주소를 의미 할 수 있으며 전적으로 추첨의 운에 달려 있습니다.
Steve Jessop

2
항목 2에 대한 추가 세부 정보-컴파일러는 변수가 메모리에있을 필요가 없는지 (변수의 수명 동안 레지스터에 유지) 및 / 또는 둘 이상의 변수의 수명이 그렇지 않은지 결정할 수 있습니다. 겹치면 컴파일러는 둘 이상의 변수에 대해 동일한 메모리를 사용하기로 결정할 수 있습니다.
Michael Burr

2
S / 390 (IBM zSeries)에는 스택에서 성장하는 대신 호출 프레임이 연결된 ABI가 있다고 생각합니다.
ephemient

2
S / 390에서 수정되었습니다. 호출은 "BALR", 분기 및 링크 레지스터입니다. 반환 값은 스택에 푸시되지 않고 레지스터에 입력됩니다. 반환 함수는 해당 레지스터의 내용에 대한 분기입니다. 스택이 깊어짐에 따라 공간이 힙에 할당되고 함께 연결됩니다. 여기서 "/ bin / true"에 해당하는 MVS의 이름은 "IEFBR14"입니다. 첫 번째 버전에는 반환 주소를 포함하는 레지스터 14의 내용으로 분기되는 "BR 14"라는 단일 명령이 있습니다.
janm

1
그리고 PIC 프로세서의 일부 컴파일러는 전체 프로그램 분석을 수행하고 각 함수의 자동 변수에 대해 고정 위치를 할당합니다. 실제 스택은 작으며 소프트웨어에서 액세스 할 수 없습니다. 반송 주소에만 해당됩니다.
janm

13

스택이 성장하는 방향은 아키텍처에 따라 다릅니다. 즉, 내 이해는 극소수의 하드웨어 아키텍처에만 스택이 커지는 것입니다.

스택이 커지는 방향은 개별 개체의 레이아웃과 무관합니다. 따라서 스택이 커질 수 있지만 어레이는 성장하지 않습니다 (예 : & array [n]는 항상 <& array [n + 1]).


4

스택에서 사물을 구성하는 방법을 지시하는 표준은 없습니다. 사실, 스택의 연속 요소에 배열 요소를 전혀 저장하지 않은 준수 컴파일러를 빌드 할 수 있습니다. 단, 배열 요소 산술을 제대로 수행 할 수있는 현명한 기능이 있다면 (예를 들어 1 이 a [0]에서 1K 떨어져 있고 조정할 수 있습니다.)

다른 결과를 얻을 수있는 이유는 스택이 확장되어 "개체"를 추가 할 수 있지만 배열은 단일 "개체"이며 반대 순서로 오름차순 배열 요소를 가질 수 있기 때문입니다. 그러나 방향이 변경 될 수 있고 변수가 다음을 포함하되 이에 국한되지 않는 다양한 이유로 교체 될 수 있으므로 해당 동작에 의존하는 것은 안전하지 않습니다.

  • 최적화.
  • 조정.
  • 컴파일러의 스택 관리 부분 인 사람의 변덕.

스택 방향에 대한 훌륭한 논문 은 여기 를 참조 하십시오 :-)

특정 질문에 대한 답변 :

  1. 스택이 증가하거나 감소합니까?
    (표준 측면에서) 전혀 중요하지 않지만 요청 했으므로 구현에 따라 메모리에서 증가 하거나 감소 할 수 있습니다 .
  2. a [2]와 q 메모리 주소 사이에는 어떤 일이 발생합니까? 왜 거기에 큰 메모리 차이가 있습니까? (20 바이트)?
    (표준 측면에서) 전혀 중요하지 않습니다. 가능한 이유는 위를 참조하십시오.

대부분의 CPU 아키텍처가 "성장"방식을 채택한다는 링크를 보았습니다. 그렇게하는 것이 어떤 이점이 있는지 알고 있습니까?
Baiyan Huang

정말 모르겠어요. 그것은이다 가능한 스택이 교차의 가능성을 최소화 시키도록, Highmem라는부터 아래로 가야한다, 그래서 사람의 생각 코드가 0에서 위쪽으로가는 것이다. 그러나 일부 CPU는 특히 0이 아닌 위치에서 코드 실행을 시작하므로 그렇지 않을 수 있습니다. 그것이 첫 번째 방법 누군가가 :-) 그것을 할 생각 이었기 때문에 대부분의 것들과 마찬가지로, 어쩌면 단순히 그런 식으로 이루어졌다
paxdiablo

@lzprgmr : 특정 종류의 힙 할당을 오름차순으로 수행하면 약간의 이점이 있으며, 스택과 힙이 공통 주소 지정 공간의 반대쪽 끝에 위치하는 것은 역사적으로 일반적이었습니다. 결합 된 정적 + 힙 + 스택 사용량이 사용 가능한 메모리를 초과하지 않는다면 프로그램이 정확히 얼마나 많은 스택 메모리를 사용하는지 걱정할 필요가 없습니다.
supercat

3

x86에서 스택 프레임의 메모리 "할당"은 단순히 스택 포인터에서 필요한 바이트 수를 빼는 것으로 구성됩니다 (다른 아키텍처도 유사하다고 생각합니다). 이런 의미에서 스택이 "아래로"성장한 것 같습니다. 스택에 더 깊게 호출할수록 주소가 점점 작아지는 것입니다. 오른쪽으로 감싸고, 그래서 내 정신 이미지에서 스택이 커집니다 ...). 선언되는 변수의 순서는 주소에 아무런 영향을 미치지 않을 수 있습니다. 부작용을 일으키지 않는 한 표준에 따라 컴파일러가 그것들을 재정렬 할 수 있다고 믿습니다 (내가 틀렸다면 누군가 나를 수정하십시오) . 그들'

배열 주변의 간격은 일종의 패딩 일 수 있지만 나에게는 신비 스럽습니다.


1
사실, 컴파일러가 그것들을 재정렬 할 수 있다는 것을 알고 있습니다. 왜냐하면 그것들을 전혀 할당하지 않아도되기 때문입니다. 레지스터에 넣을 수 있으며 스택 공간을 전혀 사용하지 않습니다.
rmeador

주소를 참조하면 레지스터에 넣을 수 없습니다.
florin

좋은 지적은 그것을 고려하지 않았습니다. 그러나 그것은 컴파일러가 그것들을 재정렬 할 수 있다는 증거로 여전히 충분합니다. 우리는 그것이 적어도 어느 정도는 그것을 할 수 있다는 것을 알고 있기 때문입니다. :)
rmeador

1

우선, 메모리에서 8 바이트의 사용되지 않은 공간 (12가 아닌 스택이 아래로 커짐을 기억하므로 할당되지 않은 공간은 604에서 597까지입니다). 그리고 왜?. 모든 데이터 유형은 크기로 나눌 수있는 주소에서 시작하여 메모리 공간을 차지하기 때문입니다. 우리의 경우 3 개의 정수 배열은 12 바이트의 메모리 공간을 사용하고 604는 12로 나눌 수 없습니다. 따라서 12로 나눌 수있는 메모리 주소를 만날 때까지 빈 공간을 남깁니다. 이는 596입니다.

따라서 배열에 할당 된 메모리 공간은 596에서 584까지입니다. 그러나 배열 할당이 계속됨에 따라 배열의 첫 번째 요소는 596이 아닌 584 주소에서 시작됩니다.


1

컴파일러는 로컬 스택 프레임의 어느 위치 에나 로컬 (자동) 변수를 자유롭게 할당 할 수 있습니다. 순전히 스택 성장 방향을 안정적으로 추론 할 수는 없습니다. 중첩 된 스택 프레임의 주소를 비교하여 스택 성장 방향을 추론 할 수 있습니다. 즉, 호출 수신자와 함수의 스택 프레임 내부에있는 지역 변수의 주소를 비교합니다.

#include <stdio.h>
int f(int *x)
{
  int a;
  return x == NULL ? f(&a) : &a - x;
}

int main(void)
{
  printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up");
  return 0;
}

5
다른 스택 객체에 대한 포인터를 빼는 것은 정의되지 않은 동작이라고 확신합니다. 동일한 객체의 일부가 아닌 포인터는 비교할 수 없습니다. "일반적인"아키텍처에서는 충돌하지 않을 것이 분명합니다.
Steve Jessop

@SteveJessop 프로그래밍 방식으로 스택의 방향을 얻기 위해이 문제를 해결할 수있는 방법이 있습니까?
xxks-kkk

@ xxks-kkk : 원칙적으로 아니요. C 구현에는 "스택 방향"이 필요하지 않기 때문입니다. 예를 들어, 스택 블록이 미리 할당 된 다음 일부 의사 랜덤 내부 메모리 할당 루틴을 사용하여 내부로 이동하는 호출 규칙을 갖는 것은 표준을 위반하지 않습니다. 실제로 matja가 설명하는대로 실제로 작동합니다.
Steve Jessop

0

나는 그것이 그렇게 결정 론적이라고 생각하지 않습니다. a 배열은 메모리가 연속적으로 할당되어야하기 때문에 "성장"하는 것처럼 보입니다. 그러나 q와 s는 서로 전혀 관련이 없기 때문에 컴파일러는 스택 내의 임의의 여유 메모리 위치 (정수 크기에 가장 적합한 위치)에 각각을 고정합니다.

a [2]와 q 사이에 일어난 일은 q의 위치 주변 공간이 3 개의 정수 배열을 할당하기에 충분히 크지 않다는 것입니다 (즉, 12 바이트보다 크지 않음).


그렇다면 왜 q, s, a는 우발적 인 기억이 없습니까? (예 : q 주소 : 2293612 s 주소 : 2293608 a 주소 : 2293604)

s와 a 사이에 "격차"가 있습니다

s와 a가 함께 할당되지 않았기 때문에 연속되어야하는 유일한 포인터는 배열에있는 포인터입니다. 다른 메모리는 어디에 든 할당 할 수 있습니다.
javanix 2009

0

내 스택은 더 낮은 번호의 주소로 확장되는 것 같습니다.

다른 컴퓨터에서 또는 다른 컴파일러 호출을 사용하는 경우 내 컴퓨터에서도 다를 수 있습니다. ... 또는 컴파일러 muigt는 스택을 전혀 사용하지 않기로 선택합니다 (모든 것을 인라인 (내가 주소를 사용하지 않은 경우 함수 및 변수)).

$ cat stack.c
#include <stdio.h>

int stack(int x) {
  printf("level %d: x is at %p\n", x, (void*)&x);
  if (x == 0) return 0;
  return stack(x - 1);
}

int main(void) {
  stack(4);
  return 0;
}
$ / usr / bin / gcc -Wall -Wextra -std = c89 -pedantic stack.c
$ ./a.out
레벨 4 : x는 0x7fff7781190c에 있습니다.
레벨 3 : x는 0x7fff778118ec에 있습니다.
레벨 2 : x는 0x7fff778118cc에 있습니다.
레벨 1 : x는 0x7fff778118ac에 있습니다.
레벨 0 : x는 0x7fff7781188c에 있습니다.

0

스택이 확장됩니다 (x86에서). 그러나 스택은 함수가로드 될 때 하나의 블록에 할당되며 항목이 스택에있을 순서를 보장 할 수 없습니다.

이 경우 스택에있는 2 개의 int와 3 개의 int 배열을위한 공간을 할당했습니다. 또한 배열 뒤에 추가로 12 바이트를 할당 했으므로 다음과 같습니다.

a [12 바이트]
padding (?) [12 바이트]
s [4 바이트]
q [4 바이트]

어떤 이유로 든 컴파일러는이 함수에 32 바이트를 할당해야한다고 결정했습니다. 그것은 C 프로그래머로서 당신에게 불투명합니다. 당신은 그 이유를 알지 못합니다.

이유를 알고 싶다면 코드를 어셈블리 언어로 컴파일하십시오. gcc에서는 -S이고 MS의 C 컴파일러에서는 / S라고 생각합니다. 해당 함수에 대한 시작 지침을 살펴보면 이전 스택 포인터가 저장되고 32 (또는 다른 것!)가 차감되는 것을 볼 수 있습니다. 여기에서 코드가 32 바이트 메모리 블록에 액세스하는 방법을 확인하고 컴파일러가 수행하는 작업을 파악할 수 있습니다. 함수가 끝나면 스택 포인터가 복원되는 것을 볼 수 있습니다.


0

운영 체제와 컴파일러에 따라 다릅니다.


내 대답이 왜 반대표를 받았는지 모르겠습니다. 실제로 OS와 컴파일러에 따라 다릅니다. 일부 시스템에서는 스택이 아래쪽으로 늘어나지 만 다른 시스템에서는 위쪽으로 늘어납니다. 그리고에 일부 시스템, 실제 푸시 다운 프레임 스택이 없다, 오히려 그것은 메모리의 예약 된 영역으로 시뮬레이션 또는 세트를 등록한다.
David R Tribble

3
아마도 한 문장으로 된 주장은 좋은 대답이 아니기 때문일 것입니다.
궤도에서 가벼운 경주

0

스택은 성장하지 않습니다. 따라서 f (g (h ())), h에 할당 된 스택은 더 낮은 주소에서 시작하고 g와 g는 f보다 낮습니다. 하지만 스택 내의 변수는 C 사양을 따라야합니다.

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

1206 가리키는 개체가 동일한 집계 개체의 구성원 인 경우 나중에 선언 된 구조 구성원에 대한 포인터는 구조에서 이전에 선언 된 구성원에 대한 포인터보다 큰 값을 비교하고 더 큰 첨자 값을 가진 배열 요소에 대한 포인터는 동일한 요소에 대한 포인터보다 큰 값을 비교합니다. 아래 첨자 값이 낮은 배열.

& a [0] <& a [1], 'a'할당 방법에 관계없이 항상 참이어야합니다.


대부분의 컴퓨터에서 스택은 위쪽으로 커지는 경우를 제외하고 아래쪽으로 커집니다.
Jonathan Leffler

0

이는 메모리의 데이터 세트와 관련하여 리틀 엔디안 바이트 순서 표준 때문입니다.

이를 볼 수있는 한 가지 방법은 메모리를 위쪽에서 0으로, 최대에서 아래쪽에서 보면 스택이 위로 커진다는 것입니다.

스택이 아래쪽으로 커지는 이유는 스택 또는 기본 포인터의 관점에서 역 참조 할 수 있기 때문입니다.

모든 유형의 역 참조는 가장 낮은 주소에서 가장 높은 주소로 증가합니다. 스택이 아래쪽으로 (가장 높은 주소에서 가장 낮은 주소로) 커지기 때문에 스택을 동적 메모리처럼 취급 할 수 있습니다.

이것이 많은 프로그래밍 및 스크립팅 언어가 레지스터 기반이 아닌 스택 기반 가상 머신을 사용하는 이유 중 하나입니다.


The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.아주 좋은 추론
user3405291

0

아키텍처에 따라 다릅니다. 자신의 시스템을 확인하려면 GeeksForGeeks의 다음 코드를 사용하십시오 .

// C program to check whether stack grows 
// downward or upward. 
#include<stdio.h> 

void fun(int *main_local_addr) 
{ 
    int fun_local; 
    if (main_local_addr < &fun_local) 
        printf("Stack grows upward\n"); 
    else
        printf("Stack grows downward\n"); 
} 

int main() 
{ 
    // fun's local variable 
    int main_local; 

    fun(&main_local); 
    return 0; 
} 
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.