왜 여전히 스택을 거꾸로 성장 시키는가?


46

C 코드를 컴파일하고 어셈블리를 볼 때 스택은 다음과 같이 거꾸로 커집니다.

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)-이것은 기본 포인터 또는 스택 포인터가 실제로 메모리 주소보다 아래로 이동한다는 것을 의미합니까? 왜 그런 겁니까?

로 변경 $5, -4(%rbp)하고 $5, +4(%rbp)컴파일하고 코드를 실행했으며 오류가 없었습니다. 그렇다면 왜 메모리 스택에서 여전히 뒤로 가야합니까?


2
-4(%rbp)모두에서 기본 포인터를 이동하지 않고 그 +4(%rbp)가능성 일이 없습니다.
마가렛 블룸

14
" 우리는 왜 여전히 뒤로 가야 합니까?"-앞으로 나아갈 때의 이점은 무엇이라고 생각하십니까? 궁극적으로 중요하지 않습니다. 하나만 선택하면됩니다.
베르 기

31
"우리는 왜 스택을 거꾸로 확장합니까?" -왜 우리가 다른 사람에게 묻지 않으면 왜 malloc힙이 거꾸로 자라는 지 묻습니다
slebetman

2
@MargaretBloom : 분명히 OP의 플랫폼에서 CRT 시작 코드는 mainRBP를 클로버 하는지 신경 쓰지 않습니다 . 확실히 가능합니다. (예, 4(%rbp)저장된 RBP 값을 쓰는 것이 좋습니다). 실제로,이 메인은 결코하지 mov %rsp, %rbp않으므로 메모리 액세스는 호출자의 RBP 와 관련이 있습니다. 그렇다면 OP가 실제로 테스트 한 것입니다 !!! 이것이 실제로 컴파일러 출력에서 ​​복사 된 경우 일부 명령이 생략되었습니다!
Peter Cordes

1
"뒤로"또는 "앞으로"(또는 "아래로"및 "위로")는 사용자의 관점에 따라 다릅니다. 주소가 낮은 열로 메모리를 다이어그램으로 표시 한 경우 스택 포인터를 줄임으로써 스택을 늘리는 것은 실제 스택과 유사합니다.
jamesdlin

답변:


86

이것은 기본 포인터 또는 스택 포인터가 실제로 메모리 주소보다 아래로 이동한다는 것을 의미합니까? 왜 그런 겁니까?

그렇습니다. push명령어는 스택 포인터를 줄이고 스택에 쓰지만 pop반대로는 스택에서 읽고 스택 포인터를 증가시킵니다.

이는 메모리가 제한된 머신의 경우 스택이 높고 아래쪽으로 성장한 반면 힙이 낮고 위쪽으로 늘어난 점에서 다소 역사적입니다. 힙과 스택 사이에는 "사용 가능한 메모리"의 간격이 하나만 있으며이 간격은 공유되므로 개별적으로 필요에 따라 간격이 커질 수 있습니다. 따라서 스택 및 힙 충돌시 사용 가능한 메모리가없는 경우에만 프로그램의 메모리가 부족합니다. 

스택과 힙이 모두 같은 방향으로 커지면 두 개의 틈이 있고 스택은 실제로 힙의 틈으로 자랄 수 없습니다 (그 반대도 마찬가지입니다).

원래 프로세서에는 전용 스택 처리 명령이 없었습니다. 그러나 하드웨어에 스택 지원이 추가됨에 따라이 패턴은 아래로 늘어 났으며 프로세서는 오늘날에도이 패턴을 따릅니다.

64 비트 시스템에는 여러 갭을 허용하기에 충분한 주소 공간이 있다고 주장 할 수 있습니다. 증거로서 프로세스에 여러 개의 스레드가있는 경우 여러 갭이 반드시 필요한 경우입니다. 여러 갭 시스템으로 인해 성장 방향이 임의적이므로 전통 / 호환성이 규모에 영향을 미칩니다.


당신은 스택의 방향을 변경하거나 다른 전용 밀어 및 지침을 보여주고 (예를 들어, 사용을 포기하기 위해 CPU 스택 처리 지침을 변경해야 할 것 push, pop, call, ret, 등).

MIPS 명령어 세트 아키텍처에는 push& 전용이 없으므로 pop스택을 어느 방향 으로든 늘리는 것이 실용적입니다. 단일 스레드 프로세스를위한 1 개의 갭 메모리 레이아웃을 원할 수 있지만 스택을 위로 늘리고 힙을 늘릴 수 있습니다 아래쪽으로. 그러나 그렇게하면 일부 C varargs 코드에서 소스 또는 실제 매개 변수 전달을 조정해야 할 수 있습니다.

(사실 MIPS에는 전용 스택 처리가 없기 때문에 스택에서 튀어 나오는 데 정확한 역을 사용하고 스택을 튀어 나오게하는 한 스택에 푸시하기 위해 프리 또는 포스트 증가 또는 프리 또는 포스트 감소를 사용할 수 있습니다. 운영 체제는 선택된 스택 사용 모델을 준수합니다. 사실, 일부 임베디드 시스템 및 일부 교육 시스템에서는 MIPS 스택이 증가하고 있습니다.)


32
그냥 아니다 pushpop대부분의 아키텍처뿐만 아니라 훨씬 더 중요한 인터럽트 처리는, call, ret, 및 어떤 다른 구운에 한 스택과의 상호 작용.
중복 제거기

3
ARM은 네 가지 스택 맛을 모두 가질 수 있습니다.
마가렛 블룸

14
가치있는 것에 대해, 나는 어느 쪽의 선택도 똑같이 좋다는 의미에서 "성장 방향은 임의적"이라고 생각하지 않습니다. 아래로 이동하면 저장된 리턴 주소를 포함하여 이전 스택 프레임의 버퍼 클로버 끝이 오버플로되는 특성이 있습니다. 자라면서 버퍼 클로버의 끝을 넘치면 동일하거나 나중에 (버퍼가 최신이 아닌 경우 나중에있을 수 있음) 호출 프레임과 (사용하지 않는 공간 만) 스택 후 페이지). 따라서 안전 측면에서 자라는 것이 매우 바람직합니다
R ..

6
@R .. : 자라더라도 취약한 함수는 일반적으로 리프 함수가 아니기 때문에 버퍼 오버런 악용을 제거하지 않습니다. 다른 함수를 호출하여 버퍼 위에 리턴 주소를 배치합니다. 발신자로부터 포인터를 가져 오는 리프 함수는 자신의 반송 주소를 덮어 쓰는 데 취약 할 수 있습니다. 예를 들어 함수가 스택에 버퍼를 할당하고이를 전달 gets()하거나 strcpy()인라인되지 않은 버퍼를 수행하는 경우 해당 라이브러리 함수 의 반환 은 덮어 쓴 반환 주소를 사용합니다. 현재 하향 성장하는 스택에서는 호출자가 돌아올 때입니다.
Peter Cordes

5
@ PeterCordes : 실제로 내 의견에 따르면 오버플로 된 버퍼와 동일한 수준 또는 최신 스택 프레임은 여전히 ​​잠재적으로 클러버 가능하지만 훨씬 적습니다. 클로버 링 함수가 버퍼가있는 함수에 의해 직접 호출되는 리프 함수 인 경우 (예 : strcpy리턴 주소가 쏟아 질 필요가없는 한 레지스터에 유지되는 아치에서) 리턴을 클로버 할 수있는 액세스 권한이 없습니다. 주소.
R ..

8

특정 시스템에서 스택은 높은 메모리 주소에서 시작하여 아래쪽으로 낮은 메모리 주소로 "증가"합니다. (낮은 것에서 높은 것까지 대칭 경우도 존재합니다)

그리고 당신이 -4와 +4에서 변경했기 때문에 그것이 그것이 정확하다는 것을 의미하지는 않습니다. 실행중인 프로그램의 메모리 레이아웃은 더 복잡하고이 간단한 프로그램에서 즉시 충돌하지 않았다는 사실에 기여할 수있는 다른 많은 요소에 따라 달라집니다.


1

스택 포인터는 할당 된 스택 메모리와 할당되지 않은 스택 메모리 사이의 경계를 가리 킵니다. 아래쪽으로 자란다는 것은 할당 된 스택 공간에서 첫 번째 구조 의 시작 을 가리키고 다른 할당 된 항목은 더 큰 주소를 따르는 것을 의미합니다. 포인터가 할당 된 구조의 시작을 가리 키도록하는 것이 다른 방법보다 훨씬 일반적입니다.

요즘 많은 시스템에는 로컬 가변 스토리지가 산재되어있는 콜 체인을 파악하기 위해 다소 안정적으로 풀 수있는 스택 프레임에 대한 별도의 레지스터 가 있습니다. 방법이 스택 프레임 레지스터가 가리키는 끝나는 몇 가지 아키텍처 수단에 설정 한 뒤에 스택 포인터와 달리 지역 변수 저장을 하기 전에 그것. 따라서이 스택 프레임 레지스터를 사용하려면 음의 인덱싱이 필요합니다.

스택 프레임과 인덱싱은 컴파일 된 컴퓨터 언어의 한 측면이므로 어셈블리 언어 프로그래머가 아닌 "부자연 스러움"을 처리해야하는 컴파일러의 코드 생성기입니다.

따라서 스택을 선택하여 아래로 자라는 좋은 역사적 이유가 있었지만 (일부 어셈블리 언어로 프로그래밍하고 적절한 스택 프레임을 설정하지 않아도 일부는 유지되지만) 가시성이 떨어졌습니다.


2
"이제 많은 시스템에서 스택 프레임에 대한 별도의 레지스터가 있습니다." 더 풍부한 디버그 정보 형식으로 인해 오늘날 프레임 포인터가 필요하지 않습니다.
피터 그린
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.