콜 스택은 정확히 어떻게 작동합니까?


103

프로그래밍 언어의 저수준 작업이 어떻게 작동하는지, 특히 OS / CPU와 상호 작용하는 방법에 대해 더 깊이 이해하려고합니다. 나는 아마도 스택 오버플로의 모든 스택 / 힙 관련 스레드에서 모든 답변을 읽었으며 모두 훌륭합니다. 하지만 아직 완전히 이해하지 못한 것이 하나 있습니다.

유효한 Rust 코드 인 의사 코드에서이 함수를 고려하십시오 ;-)

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(a, b);
    doAnotherThing(c, d);
}

이것이 내가 X 행에서 스택이 어떻게 보이는지 가정하는 방법입니다.

Stack

a +-------------+
  | 1           | 
b +-------------+     
  | 2           |  
c +-------------+
  | 3           | 
d +-------------+     
  | 4           | 
  +-------------+ 

이제 스택이 작동하는 방식에 대해 읽은 모든 것은 LIFO 규칙을 엄격하게 준수한다는 것입니다 (후입 선출). .NET, Java 또는 기타 프로그래밍 언어의 스택 데이터 유형과 같습니다.

하지만 그럴 경우 X 행 뒤에 무슨 일이 일어날까요? 분명하기 때문에, 다음 일이 우리의 필요와 작업하는 것입니다 ab하지만은 OS / CPU가 (?) 튀어한다는 것을 의미 d하고 c처음으로 돌아 가야 a하고 b. 이 필요하기 때문에 그러나 그것은 발에 자신을 쏠 것 cd다음 줄에.

그래서 뒤에서 정확히 무슨 일이 일어나는지 궁금 합니다.

또 다른 관련 질문입니다. 다음과 같은 다른 함수 중 하나에 대한 참조를 전달한다고 가정합니다.

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(&a, &b);
    doAnotherThing(c, d);
}

내가 일을 이해하는 방법에서, 이것은의 매개 변수는 것을 의미 doSomething본질적으로 같은 동일한 메모리 주소를 가리키는 abfoo. 하지만 다시이 방법이 더 있다는 것을 우리가 얻을 때까지 스택을 팝업하지 ab 일어날 .

이 두 가지 경우 는 스택이 정확히 작동하는 방식과 LIFO 규칙을 엄격하게 따르는 방식을 완전히 이해하지 못했다고 생각 하게합니다 .


14
LIFO는 스택의 공간을 예약하는 데만 중요합니다. 그것은 다른 많은 변수에 따라 경우에도 당신은 항상 (함수 내에서 선언) 당신의 스택 프레임에 적어도 어떤 변수에 액세스 할 수 있습니다
VoidStar

2
즉, LIFO스택의 끝에 만 요소를 추가하거나 제거 할 수 있으며 항상 모든 요소를 ​​읽고 / 변경할 수 있습니다.
HolyBlackCat 2014-06-01

12
-O0으로 컴파일 한 후 간단한 함수를 디스 어셈블하고 생성 된 명령어를 살펴 보는 것은 어떨까요? 예쁘고, 잘, 유익합니다 ;-). 코드가 RAM의 R 부분을 잘 활용한다는 것을 알 수 있습니다. 원하는대로 주소에 직접 액세스합니다. 변수 이름은 주소 레지스터 (스택 포인터)에 대한 오프셋으로 생각할 수 있습니다. 다른 사람들이 말했듯이 스택은 스택과 관련하여 LIFO입니다 (재귀 등에 좋습니다). 액세스와 관련하여 LIFO가 아닙니다. 액세스는 완전히 무작위입니다.
Peter-Monica 복원

6
배열을 사용하여 자신 만의 스택 데이터 구조를 만들고 맨 위 요소의 인덱스를 저장하고 푸시 할 때 증가하고 팝할 때 감소 할 수 있습니다. 이렇게하면 배열에서 항상 할 수있는 것처럼, 밀어 넣거나 팝하지 않고도 언제든지 배열의 개별 요소에 액세스 할 수 있습니다. 여기서 거의 같은 일이 일어나고 있습니다.
Crowman 2014-06-01

3
기본적으로 스택 / 힙의 이름은 불행합니다. 데이터 구조 용어에서 스택 및 힙과 거의 유사하지 않으므로 동일하게 부르는 것은 매우 혼란 스럽습니다.
Siyuan Ren

답변:


117

호출 스택은 프레임 스택이라고도합니다. LIFO 원칙 이후
스택 되는 것은 지역 변수가 아니라 호출되는 함수의 전체 스택 프레임 ( "호출")입니다 . 지역 변수는 소위 함수 프롤로그에필로그 에서 해당 프레임과 함께 푸시 및 팝됩니다. 각각 .

프레임 내에서 변수의 순서는 완전히 지정되지 않습니다. 컴파일러 는 프레임 내에서 지역 변수의 위치를 적절하게 "재정렬"하여 정렬을 최적화하여 프로세서가 최대한 빨리 가져올 수 있도록합니다. 중요한 사실은 일부 고정 주소에 대한 변수의 오프셋이 프레임의 수명 내내 일정하다는 것입니다. 따라서 프레임 자체의 주소와 같은 앵커 주소를 가져 와서 해당 주소의 오프셋을 사용하여 작업하는 것으로 충분합니다. 변수. 이러한 앵커 주소는 실제로 소위 기본 또는 프레임 포인터에 포함되어 있습니다.EBP 레지스터에 저장됩니다. 반면 오프셋은 컴파일 타임에 명확하게 알려져 있으므로 기계 코드에 하드 코딩됩니다.

Wikipedia 의이 그래픽 은 일반적인 호출 스택의 구조를 보여줍니다 1 :

스택 그림

프레임 포인터에 포함 된 주소에 액세스하려는 변수의 오프셋을 추가하고 변수의 주소를 얻습니다. 간단히 말해, 코드는 기본 포인터에서 상수 컴파일 시간 오프셋을 통해 직접 액세스합니다. 간단한 포인터 산술입니다.

#include <iostream>

int main()
{
    char c = std::cin.get();
    std::cout << c;
}

gcc.godbolt.org 는 우리에게

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp

    movl    std::cin, %edi
    call    std::basic_istream<char, std::char_traits<char> >::get()
    movb    %al, -1(%rbp)
    movsbl  -1(%rbp), %eax
    movl    %eax, %esi
    movl    std::cout, %edi
    call    [... the insertion operator for char, long thing... ]

    movl    $0, %eax
    leave
    ret

.. for main. 코드를 세 개의 하위 섹션으로 나누었습니다. 함수 프롤로그는 처음 세 가지 작업으로 구성됩니다.

  • 기본 포인터가 스택으로 푸시됩니다.
  • 스택 포인터는 기본 포인터에 저장됩니다.
  • 스택 포인터는 지역 변수를위한 공간을 만들기 위해 뺍니다.

이어서 cin레지스터 EDI로 이동 2get호출; 반환 값은 EAX입니다.

여태까지는 그런대로 잘됐다. 이제 흥미로운 일이 발생합니다.

8 비트 레지스터 AL로 지정된 EAX의 하위 바이트를 취해 기본 포인터 바로 뒤의 바이트에 저장합니다 . 즉 , 기본 포인터-1(%rbp) 의 오프셋은입니다 -1. 이 바이트는 우리의 변수c 입니다. x86에서 스택이 아래쪽으로 커지기 때문에 오프셋은 음수입니다. 다음 작업 저장 cEAX에서는 : ESI EAX가 이동되어, cout함께 EDI로 이동하고, 삽입 조작을 호출 cout하고 c인자 인.

드디어,

  • 의 반환 값은 mainEAX : 0에 저장됩니다. 이는 암시 적 return문 때문입니다 . xorl rax rax대신을 볼 수도 있습니다 movl.
  • 나가고 콜 사이트로 돌아갑니다. leave이 에필로그를 축약하고 암시 적으로
    • 스택 포인터를 기본 포인터로 교체하고
    • 기본 포인터를 팝합니다.

이 작업을 ret수행하고 수행 한 후에 는 프레임이 효과적으로 팝되었지만 호출자는 cdecl 호출 규칙을 사용하므로 인수를 여전히 정리해야합니다. stdcall과 같은 다른 규칙에서는 호출 수신자가 바이트 양을에 전달하여 정리해야합니다 ret.

프레임 포인터 생략

기본 / 프레임 포인터의 오프셋을 사용하지 않고 대신 스택 포인터 (ESB)의 오프셋을 사용하는 것도 가능합니다. 이로 인해 프레임 포인터 값을 포함하는 EBP 레지스터는 임의적으로 사용할 수 있지만 일부 시스템 에서는 디버깅을 불가능 하게 만들 수 있으며 일부 기능에 대해서는 암시 적으로 해제됩니다 . x86을 포함하여 레지스터가 거의없는 프로세서를 컴파일 할 때 특히 유용합니다.

이 최적화를 FPO (프레임 포인터 생략)라고하며 -fomit-frame-pointerGCC 및 -OyClang 에서 설정합니다 . 디버깅이 여전히 가능한 경우에만 모든 최적화 수준> 0에 의해 암시 적으로 트리거됩니다. 그와는 별도로 비용이 들지 않기 때문입니다. 자세한 내용은 여기여기를 참조 하십시오 .


1 주석에서 지적했듯이 프레임 포인터는 아마도 반환 주소 뒤의 주소를 가리키는 것을 의미합니다.

2 R로 시작하는 레지스터는 E로 시작하는 레지스터의 64 비트 대응 요소입니다. EAX는 RAX의 하위 4 바이트를 지정합니다. 명확성을 위해 32 비트 레지스터의 이름을 사용했습니다.


1
좋은 대답입니다. 오프셋하여 데이터를 주소가있는 것은 :) 나를 위해 누락 된 비트이었다
크리스토프

1
그림에 사소한 실수가 있다고 생각합니다. 프레임 포인터는 반환 주소의 다른쪽에 있어야합니다. 함수를 떠나는 것은 일반적으로 다음과 같이 수행됩니다. 스택 포인터를 프레임 포인터로 이동하고, 호출자 프레임 포인터를 스택에서 팝하고, 반환합니다 (즉, 호출자 프로그램 카운터 / 명령 포인터를 스택에서 팝합니다.)
kasperd

kasperd가 절대적으로 옳습니다. 프레임 포인터를 전혀 사용하지 않거나 (유효한 최적화, 특히 x86과 같이 레지스터가 부족한 아키텍처에 매우 유용함)이를 사용하고 스택에 이전 주소를 저장합니다 (보통 반환 주소 바로 뒤에). 프레임을 설정하고 제거하는 방법은 아키텍처와 ABI에 따라 다릅니다. 꽤 많은 아키텍처 모든 일 .. 더 재미있다 (안녕하세요 Itanium은)가 있습니다 (가변 크기의 인수 목록과 같은 것이있다!)
Voo

3
@Christoph 나는 당신이 개념적 관점에서 이것을 접근하고 있다고 생각합니다. 여기에이 문제를 해결할 수있는 주석이 있습니다. RTS 또는 런타임 스택은 "더티 스택"이라는 점에서 다른 스택과 약간 다릅니다. 실제로는 그렇지 않은 값을 보는 것을 방해하는 것이 없습니다. t 상단에. 다이어그램에서 녹색 방법에 대한 "Return Address"는 파란색 방법에 필요합니다. 매개 변수 뒤에 있습니다. 이전 프레임이 팝된 후 파란색 메서드는 반환 값을 어떻게 얻습니까? 글쎄, 그것은 더러운 스택이므로 손을 뻗어 잡을 수 있습니다.
Riking

1
프레임 포인터는 항상 스택 포인터의 오프셋을 대신 사용할 수 있기 때문에 실제로 필요하지 않습니다. x64 아키텍처를 대상으로하는 GCC는 기본적으로 스택 포인터를 사용하며 rbp다른 작업을 수행 할 수 있습니다.
Siyuan Ren

27

분명히 다음으로 필요한 것은 a와 b로 작업하는 것이지만 이는 OS / CPU (?)가 a와 b로 돌아가려면 먼저 d와 c를 튀어 나와야 함을 의미합니다. 그러나 다음 줄에 c와 d가 필요하기 때문에 발에 스스로 쏠 것입니다.

요컨대 :

인수를 팝할 필요가 없습니다. foo함수 호출자 에 의해 전달 된 인수 doSomething와의 지역 변수 doSomething 는 모두 기본 포인터 의 오프셋으로 참조 될 수 있습니다 .
그래서,

  • 함수 호출이 이루어지면 함수의 인수가 스택에 PUSH됩니다. 이러한 인수는 기본 포인터에 의해 추가로 참조됩니다.
  • 함수가 호출자에게 반환되면 반환 함수의 인수가 LIFO 메서드를 사용하여 스택에서 POP됩니다.

상세히:

규칙은 각 함수 호출 결과 스택 프레임이 생성 된다는 것입니다 (최소값은 반환 할 주소 임). 그래서, 경우에 funcA호출 funcBfuncB통화 funcC, 세 스택 프레임은 또 다른 하나의 위에 설정됩니다. 함수가 반환되면 해당 프레임이 유효하지 않게 됩니다. 잘 작동하는 함수는 자체 스택 프레임에서만 작동하며 다른 프레임에서는 침입하지 않습니다. 즉, POPing은 상단의 스택 프레임에 수행됩니다 (함수에서 복귀 할 때).

여기에 이미지 설명 입력

질문의 스택은 호출자에 의해 설정됩니다 foo. doSomethingdoAnotherThing호출 되면 자체 스택을 설정합니다. 그림은이를 이해하는 데 도움이 될 수 있습니다.

여기에 이미지 설명 입력

그 주 (기능 체 기능 체는 스택을 통과 할 것이며, 리턴 어드레스가 저장되어있는 위치에서 (상위 어드레스) 아래로 통과 할 수있을 것이며, 로컬 변수를 액세스하기 위해, 하위 어드레스를 인수 액세스 할 ) 반송 주소가 저장된 위치에 상대적. 사실, 함수에 대한 일반적인 컴파일러 생성 코드는 정확히이 작업을 수행합니다. 컴파일러는이를 위해 EBP라는 레지스터를 지정합니다 (Base Pointer). 동일한 다른 이름은 프레임 포인터입니다. 컴파일러는 일반적으로 함수 본문에 대한 첫 번째 작업으로 현재 EBP 값을 스택에 푸시하고 EBP를 현재 ESP로 설정합니다. 즉,이 작업이 완료되면 함수 코드의 어느 부분에서든 인수 1은 EBP + 8 (호출자의 EBP 및 반환 주소 각각에 대해 4 바이트), 인수 2는 EBP + 12 (10 진수) 거리, 지역 변수입니다. EBP-4n이 떨어져 있습니다.

.
.
.
[ebp - 4]  (1st local variable)
[ebp]      (old ebp value)
[ebp + 4]  (return address)
[ebp + 8]  (1st argument)
[ebp + 12] (2nd argument)
[ebp + 16] (3rd function argument) 

함수의 스택 프레임 형성에 대한 다음 C 코드를 살펴보십시오.

void MyFunction(int x, int y, int z)
{
     int a, int b, int c;
     ...
}

발신자가 전화하면

MyFunction(10, 5, 2);  

다음 코드가 생성됩니다

^
| call _MyFunction  ; Equivalent to: 
|                   ; push eip + 2
|                   ; jmp _MyFunction
| push 2            ; Push first argument  
| push 5            ; Push second argument  
| push 10           ; Push third argument  

함수의 어셈블리 코드는 다음과 같습니다 (반환하기 전에 호출 수신자가 설정).

^
| _MyFunction:
|  sub esp, 12 ; sizeof(a) + sizeof(b) + sizeof(c)
|  ;x = [ebp + 8], y = [ebp + 12], z = [ebp + 16]
|  ;a = [ebp - 4] = [esp + 8], b = [ebp - 8] = [esp + 4], c = [ebp - 12] =   [esp]
|  mov ebp, esp
|  push ebp
 

참조 :


1
답변 주셔서 감사합니다. 또한 링크는 정말 멋진 도움 날의 끝없는 질문으로 더 많은 빛을 흘렸다하는 방법 컴퓨터 실제로 작업 :
크리스토프

무엇을 당신 평균으로는 "스택에 현재 EBP 값을 밀어"하고하는 스택 포인터 레지스터에 저장하거나 너무 스택의 위치를 차지하지 ... 난 조금 혼란 스러워요
SURAJ 자이나교

그리고 그것은 [ebp + 8]이 아니라 * [ebp + 8]이어야하지 않습니까?
Suraj Jain

@Suraj Jain; 당신은 무엇을 알고 있는가 EBPESP?
haccks

esp는 스택 포인터이고 ebp는 기본 포인터입니다. 내가 지식을 놓친 경우 친절하게 수정하십시오.
Suraj Jain

19

다른 사람들이 언급했듯이 매개 변수가 범위를 벗어날 때까지 매개 변수를 팝할 필요가 없습니다.

Nick Parlante의 "Pointers and Memory"에서 몇 가지 예를 붙여 넣겠습니다. 상황은 당신이 생각했던 것보다 조금 더 간단하다고 생각합니다.

다음은 코드입니다.

void X() 
{
  int a = 1;
  int b = 2;

  // T1
  Y(a);

  // T3
  Y(b);

  // T5
}

void Y(int p) 
{
  int q;
  q = p + 2;
  // T2 (first time through), T4 (second time through)
}

시점 T1, T2, etc. 코드에 표시되고 그 당시의 메모리 상태가 그림에 표시됩니다.

여기에 이미지 설명 입력


2
훌륭한 시각적 설명. 나는 구글을 검색하고 여기에서 논문을 찾았다 : cslibrary.stanford.edu/102/PointersAndMemory.pdf 정말 유용한 논문!
Christoph

7

다른 프로세서와 언어는 몇 가지 다른 스택 디자인을 사용합니다. 8x86 및 68000의 두 가지 전통적인 패턴을 Pascal 호출 규칙과 C 호출 규칙이라고합니다. 각 규칙은 레지스터 이름을 제외하고 두 프로세서에서 동일한 방식으로 처리됩니다. 각각은 스택 포인터 (SP 또는 A7) 및 프레임 포인터 (BP 또는 A6)라고하는 스택 및 관련 변수를 관리하기 위해 두 개의 레지스터를 사용합니다.

두 규칙 중 하나를 사용하여 서브 루틴을 호출 할 때 루틴을 호출하기 전에 모든 매개 변수가 스택에 푸시됩니다. 그런 다음 루틴의 코드는 프레임 포인터의 현재 값을 스택에 푸시하고 스택 포인터의 현재 값을 프레임 포인터에 복사 한 다음 스택 포인터에서 로컬 변수가 사용하는 바이트 수를 뺍니다. 이 작업이 완료되면 추가 데이터가 스택에 푸시 되더라도 모든 로컬 변수는 스택 포인터에서 일정한 음의 변위를 사용하여 변수에 저장되고 호출자가 스택에 푸시 한 모든 매개 변수에 액세스 할 수 있습니다. 프레임 포인터에서 일정한 양의 변위.

두 규칙의 차이점은 서브 루틴에서 종료를 처리하는 방식에 있습니다. C 규칙에서 반환 함수는 프레임 포인터를 스택 포인터에 복사하고 [이전 프레임 포인터를 누른 직후의 값으로 복원], 이전 프레임 포인터 값을 팝하고 반환을 수행합니다. 호출하기 전에 호출자가 스택에 푸시 한 모든 매개 변수는 그대로 유지됩니다. Pascal 규칙에서 이전 프레임 포인터를 팝한 후 프로세서는 함수 반환 주소를 팝하고 호출자가 푸시 한 매개 변수의 바이트 수를 스택 포인터에 추가 한 다음 팝된 반환 주소로 이동합니다. 원래 68000에서는 호출자의 매개 변수를 제거하기 위해 3 개의 명령어 시퀀스를 사용해야했습니다. 8x86 및 원본 이후의 모든 680x0 프로세서에는 "ret N"이 포함되었습니다.

Pascal 규칙은 호출자가 함수 호출 후 스택 포인터를 업데이트 할 필요가 없기 때문에 호출자 측에서 약간의 코드를 절약 할 수 있다는 장점이 있습니다. 그러나 호출 된 함수는 호출자가 스택에 넣을 매개 변수의 바이트 수를 정확히 알고 있어야합니다. Pascal 규칙을 사용하는 함수를 호출하기 전에 적절한 수의 매개 변수를 스택에 푸시하지 못하면 충돌이 발생할 가능성이 거의 보장됩니다. 그러나 이것은 호출 된 각 메서드 내의 약간의 추가 코드가 메서드가 호출되는 위치에 코드를 저장한다는 사실로 인해 상쇄됩니다. 이러한 이유로 대부분의 원래 Macintosh 도구 상자 루틴은 Pascal 호출 규칙을 사용했습니다.

C 호출 규칙은 루틴이 가변 개수의 매개 변수를 허용하고 루틴이 전달 된 모든 매개 변수를 사용하지 않더라도 견고하다는 장점이 있습니다 (호출자는 푸시 한 매개 변수의 바이트 수를 알 수 있습니다. 따라서 정리할 수 있습니다). 또한 모든 함수 호출 후 스택 정리를 수행 할 필요가 없습니다. 루틴이 4 개의 함수를 순서대로 호출하는 경우 각각 4 바이트에 해당하는 매개 변수를 사용하는 경우 ADD SP,4, 각 호출 후 를 사용하는 대신 ADD SP,16마지막 호출 이후에 하나씩 사용하여 4 개의 호출 모두에서 매개 변수를 정리할 수 있습니다.

오늘날 설명 된 호출 규칙은 다소 구식으로 간주됩니다. 컴파일러는 레지스터 사용에서 더 효율적이 되었기 때문에 모든 매개 변수를 스택에 푸시 할 것을 요구하는 것보다 메소드가 레지스터에서 몇 개의 매개 변수를 받아들이도록하는 것이 일반적입니다. 메서드가 레지스터를 사용하여 모든 매개 변수와 지역 변수를 보유 할 수 있다면 프레임 포인터를 사용할 필요가 없으므로 이전 포인터를 저장하고 복원 할 필요가 없습니다. 그럼에도 불구하고 라이브러리를 사용하기 위해 링크 된 라이브러리를 호출 할 때 이전 호출 규칙을 사용해야하는 경우가 있습니다.


1
와! 일주일 정도 당신의 뇌를 빌려도 될까요? 핵심적인 부분을 추출해야합니다! 좋은 대답입니다!
Christoph

프레임과 스택 포인터는 스택 자체 또는 다른 곳에 저장됩니까?
Suraj Jain

@SurajJain : 일반적으로 프레임 포인터의 저장된 각 사본은 새 프레임 포인터 값에 대해 고정 된 변위로 저장됩니다.
supercat

선생님, 저는이 의심을 오랫동안 가지고 있습니다. 내 기능에 나는 경우 작성하는 경우 (g==4)다음 int d = 3g내가 사용하여 입력을 scanf내가 다른 변수를 정의 그 후 int h = 5. 이제 컴파일러 d = 3는 스택에 공간을 어떻게 제공합니까 ? 어떻게 않는 경우 때문에 수행 상쇄 g하지 4, 다음 스택 D에 대한 기억이 없을 것입니다 단순히 주어진 것 오프셋 h및 경우 g == 4먼저 g 및 다음에 할 오프셋 h. 컴파일러는 어떻게 컴파일 타임에 그것을 수행합니까, 그것은 우리의 입력을 알지 못합니다g
Suraj Jain

@SurajJain : C의 초기 버전에서는 함수 내의 모든 자동 변수가 실행 가능한 문 앞에 나타나야했습니다. 복잡한 컴파일을 약간 완화하지만 한 가지 접근 방식은 SP에서 앞으로 선언 된 레이블의 값을 빼는 함수 시작 부분에 코드를 생성하는 것입니다. 함수 내에서 컴파일러는 코드의 각 지점에서 여전히 범위 내에있는 로컬 값의 바이트 수를 추적하고 범위 내에있는 로컬 값의 최대 바이트 수를 추적 할 수 있습니다. 함수의 끝에서 이전 값을 제공 할 수 있습니다 ...
supercat

5

이미 여기에 정말 좋은 답변이 있습니다. 그러나 스택의 LIFO 동작에 대해 여전히 염려한다면 변수 스택이 아닌 프레임 스택으로 생각하십시오. 제가 제안하고자하는 것은 함수가 스택의 맨 위에 있지 않은 변수에 액세스 할 수 있지만 여전히 스택 맨 위에있는 항목 ( 단일 스택 프레임) 에서만 작동한다는 것입니다 .

물론 여기에는 예외가 있습니다. 전체 콜 체인의 로컬 변수는 여전히 할당되고 사용 가능합니다. 그러나 직접 액세스 할 수는 없습니다. 대신 참조 (또는 실제로 의미 상 다른 포인터)에 의해 전달됩니다. 이 경우 훨씬 더 아래에있는 스택 프레임의 로컬 변수에 액세스 할 수 있습니다. 그러나이 경우에도 현재 실행중인 함수는 자체 로컬 데이터에서만 작동합니다. 힙, 정적 메모리 또는 스택 아래에있는 항목에 대한 참조 일 수있는 자체 스택 프레임에 저장된 참조에 액세스합니다.

이것은 함수를 임의의 순서로 호출 가능하게 만들고 재귀를 허용하는 스택 추상화의 일부입니다. 최상위 스택 프레임은 코드에서 직접 액세스하는 유일한 개체입니다. 다른 것은 간접적으로 (맨 위 스택 프레임에있는 포인터를 통해) 액세스됩니다.

특히 최적화없이 컴파일하는 경우 작은 프로그램의 어셈블리를 살펴 보는 것이 유익 할 수 있습니다. 함수의 모든 메모리 액세스는 컴파일러가 함수에 대한 코드를 작성하는 방법 인 스택 프레임 포인터의 오프셋을 통해 발생한다는 것을 알 수 있습니다. 참조에 의한 전달의 경우 스택 프레임 포인터에서 일부 오프셋에 저장된 포인터를 통해 간접 메모리 액세스 명령을 볼 수 있습니다.


4

호출 스택은 실제로 스택 데이터 구조가 아닙니다. 이면에서 우리가 사용하는 컴퓨터는 랜덤 액세스 머신 아키텍처의 구현입니다. 따라서 a와 b에 직접 액세스 할 수 있습니다.

이면에서 기계는 다음을 수행합니다.

  • get "a"는 스택 맨 아래 네 번째 요소의 값을 읽는 것과 같습니다.
  • get "b"는 스택 맨 아래 세 번째 요소의 값을 읽는 것과 같습니다.

http://en.wikipedia.org/wiki/Random-access_machine


1

다음은 C의 호출 스택을 위해 만든 다이어그램입니다. Google 이미지 버전보다 더 정확하고 현대적입니다.

여기에 이미지 설명 입력

그리고 위 다이어그램의 정확한 구조에 따라 Windows 7에서 notepad.exe x64의 디버그가 있습니다.

여기에 이미지 설명 입력

하위 주소와 상위 주소가 스왑되어 스택이이 다이어그램에서 위로 올라갑니다. 빨간색은 첫 번째 다이어그램과 똑같은 프레임을 나타냅니다 (빨간색과 검정색을 사용했지만 이제 검정색이 용도가 변경되었습니다). 검은 색은 가정 공간입니다. 파란색은 반환 주소로, 호출 후 명령에 대한 호출자 함수의 오프셋입니다. 주황색은 정렬이고 분홍색은 명령 포인터가 호출 직후와 첫 번째 명령 이전을 가리키는 위치입니다. homespace + return 값은 창에서 허용되는 가장 작은 프레임이며 호출 된 함수의 시작 부분에서 16 바이트 rsp 정렬이 유지되어야하므로 항상 8 바이트 정렬도 포함됩니다.BaseThreadInitThunk 등등.

빨간색 함수 프레임은 피 호출자 함수가 논리적으로 '소유'하고 읽고 / 수정하는 내용을 설명합니다 (-Ofast의 레지스터에 전달하기에는 너무 큰 스택에 전달 된 매개 변수를 수정할 수 있음). 녹색 선은 함수가 함수의 시작부터 끝까지 할당하는 공간을 구분합니다.


RDI 및 기타 레지스터 인수는 디버그 모드에서 컴파일하는 경우에만 스택에 유출되며 컴파일이 해당 순서를 선택한다는 보장은 없습니다. 또한 가장 오래된 함수 호출에 대해 다이어그램 상단에 스택 인수가 표시되지 않는 이유는 무엇입니까? 어떤 프레임이 어떤 데이터를 "소유"하는지 다이어그램에 명확한 경계가 없습니다. (피 호출자는 스택 인수를 소유합니다.) 다이어그램 상단에서 스택 인수를 생략하면 "레지스터로 전달할 수없는 매개 변수"가 항상 모든 함수의 반환 주소 바로 위에 있다는 것을 알기가 훨씬 더 어려워집니다.
Peter Cordes

@PeterCordes goldbolt asm 출력은 clang 및 gcc 피 호출자가 레지스터에 전달 된 매개 변수를 기본 동작으로 스택에 푸시하므로 주소가 있습니다. gcc register에서 매개 변수 뒤에 사용 하면이를 최적화하지만 주소가 함수 내에서 절대로 사용되지 않으므로 어쨌든 최적화 될 것이라고 생각할 것입니다. 상단 프레임을 고정하겠습니다. 나는 줄임표를 별도의 빈 프레임에 넣어야했습니다. '호출자가 스택 인수를 소유합니다', 레지스터에 전달할 수없는 경우 호출자가 푸시하는 인수를 포함하여 무엇입니까?
Lewis Kelsey

예, 최적화가 비활성화 된 상태에서 컴파일하면 피 호출자가 어딘가에이를 유출합니다. 그러나 스택 인수 (그리고 틀림없이 저장된 RBP)의 위치와는 달리 어디에 대해 표준화 된 것은 없습니다. Re : 피 호출자가 스택 인수를 소유합니다. 예, 함수는 들어오는 인수를 수정할 수 있습니다. 자체적으로 유출되는 정규 인수는 스택 인수가 아닙니다. 컴파일러는 때때로 이것을 수행하지만 IIRC는 인수를 다시 읽지 않더라도 반환 주소 아래의 공간을 사용하여 스택 공간을 낭비하는 경우가 많습니다. 발신자가 동일한 인수로 다른 전화를 걸려면 안전을 위해 다른 복사본을 저장해야합니다call
Peter Cordes

@PeterCordes 글쎄, rbp가 가리키는 위치를 기준으로 스택 프레임을 구분했기 때문에 호출자 스택의 인수 부분을 만들었습니다. 일부 다이어그램은 이것을 호출 수신자 스택의 일부로 표시하고 (이 질문의 첫 번째 다이어그램처럼) 일부는 호출자 스택의 일부로 표시하지만 매개 변수 범위로 보는 호출 수신자 스택의 일부로 만드는 것이 합리적 일 수 있습니다. 더 높은 수준의 코드에서 발신자가 액세스 할 수 없습니다. 예, 보이는 register것과 const최적화는 -O0에서만 차이를 만듭니다.
Lewis Kelsey

@PeterCordes 내가 그것을 변경했습니다. 그래도 다시 변경할 수 있습니다
Lewis Kelsey
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.