스택은 어셈블리 언어에서 어떻게 작동합니까?


84

저는 현재 스택이 어떻게 작동하는지 이해하려고 노력 중이므로 어셈블리 언어를 스스로 가르치기로 결정했습니다 .이 책을 사용하고 있습니다.

http://savannah.nongnu.org/projects/pgubook/

저는 Gas를 사용 하고 있으며 Linux Mint에서 개발을하고 있습니다.

나는 무언가에 약간 혼란 스럽습니다.

내가 아는 한 스택은 단순히 데이터 구조입니다. 그래서 어셈블리로 코딩한다면 스택을 직접 구현해야한다고 생각했습니다. 그러나 이것은 다음과 같은 명령이 있기 때문에 그렇지 않은 것 같습니다.

pushl
popl

그렇다면 x86 아키텍처 용 어셈블리로 코딩 하고 Gas 구문을 사용할 때 스택은 이미 구현 된 데이터 구조일까요? 아니면 실제로 하드웨어 수준에서 구현됩니까? 아니면 다른 건가요? 또한 다른 칩셋에 대한 대부분의 어셈블리 언어에 이미 스택이 구현되어 있습니까?

나는 이것이 약간 어리석은 질문이라는 것을 알고 있지만 실제로 이것에 대해 꽤 혼란 스럽습니다.


2
이 답변의 대부분은 언어에서 사용되는 스택에 대해 이야기하며, 특히 스택에서 인수를 전달하는 것에 대해 이야기합니다. 많은 CPU에서 이것은 언어 구현을 더 쉽게하기위한 것입니다. 어셈블리를 직접 코딩하는 경우 일반적으로 레지스터의 함수에 매개 변수를 전달합니다 (적어도 언어가 너무 많이 사용했기 때문에 CPU가 스택 작업에 ​​최적화되기 전에). 스택은 대부분 호출 / 반환을 순서대로 유지하는 것입니다. 또한 인터럽트 (CPU 상태를 저장해야 함)는 기존 값을 사용할 레지스터에 푸시하고 반환하기 전에 팝합니다.
Bill K

답변:


82

나는 주로 당신이 a program's stackany old stack.

스택

Last In First Out 시스템의 정보로 구성된 추상 데이터 구조입니다. 임의의 물체를 스택에 넣은 다음 다시 꺼냅니다. 인 / 아웃 트레이처럼 맨 위 항목은 항상 벗겨지고 항상 맨 위에 놓입니다.

프로그램 스택

스택이며 실행 중에 사용되는 메모리 섹션이며 일반적으로 프로그램 당 정적 크기를 가지며 함수 매개 변수를 저장하는 데 자주 사용됩니다. 함수를 호출 할 때 매개 변수를 스택에 푸시하면 함수가 스택에 직접 주소를 지정하거나 스택에서 변수를 제거합니다.

프로그램 스택은 일반적으로 하드웨어가 아니지만 (메모리에 보관되므로 그렇게 주장 할 수 있음) 스택의 현재 영역을 가리키는 스택 포인터는 일반적으로 CPU 레지스터입니다. 이것은 스택이 주소를 지정하는 지점을 변경할 수 있으므로 LIFO 스택보다 약간 더 유연합니다.

당신은 당신이 다루는 하드웨어 스택에 대한 좋은 설명을 제공하는 위키피디아 기사를 읽고 이해해야합니다 .

이전 16 비트 레지스터의 관점에서 스택을 설명하는 이 튜토리얼 도 있지만 도움이 될 수 있고 스택에 대해 특별히 다른 하나 가 있습니다 .

Nils Pipenbrinck에서 :

일부 프로세서는 스택 (푸시, 팝, 스택 포인터 등)에 액세스하고 조작하기위한 모든 명령을 구현하지 않지만 사용 빈도 때문에 x86이 수행한다는 점에 주목할 가치가 있습니다. 이러한 상황에서 스택을 원하면 직접 구현해야합니다 (일부 MIPS 및 일부 ARM 프로세서는 스택없이 생성됨).

예를 들어, MIP에서 푸시 명령은 다음과 같이 구현됩니다.

addi $sp, $sp, -4  # Decrement stack pointer by 4  
sw   $t0, ($sp)   # Save $t0 to stack  

Pop 명령어는 다음과 같습니다.

lw   $t0, ($sp)   # Copy from stack to $t0  
addi $sp, $sp, 4   # Increment stack pointer by 4  

2
Btw-x86에는 이러한 특수 스택 명령이 있습니다. 스택에서 물건을 밀고 터뜨리는 일이 너무 자주 발생하여 짧은 opcode를 사용하는 것이 좋습니다 (더 적은 코드 공간). MIPS 및 ARM과 같은 아키텍처에는 이러한 기능이 없으므로 직접 스택을 구현해야합니다.
Nils Pipenbrinck

4
귀하의 최신 프로세서는 8086과 어느 정도 바이너리 호환이 가능하며 최초의 마이크로 프로세서 인 8008을 개발 한 8080과 소스 호환이 가능합니다. 이러한 결정 중 일부는 먼 길로 거슬러 올라갑니다.
David Thornley

4
ARM에는 스택을 조작하기위한 단일 명령어가 있지만 STMDB SP라고 불리기 때문에 명확하지 않습니다! (PUSH 용) 및 LDMIA SP! (POP 용).
Adam Goode

1
맙소사이 대답은 +500이 필요합니다 ... 나는 영원히 이것을 잘 설명하는 어떤 것도 찾지 못했습니다. 지금부터 이것을 +1하기 위해 새 계정을 만드는 것을 고려 중입니다 ...
Gabriel


36

(나는 당신이 그것을 가지고 놀고 싶다면이 답변의 모든 코드 의 요점 을 만들었 습니다)

저는 2003 년 CS101 과정에서 asm에서 대부분의 기본적인 일을 해본 적이 있습니다. 그리고 asm과 스택이 C 또는 C ++로 프로그래밍하는 것과 같은 기본적이라는 사실을 깨닫기 전까지는 asm과 스택이 어떻게 작동하는지 "알아보지 못했습니다". 그러나 지역 변수, 매개 변수 및 함수는 없습니다. 아마도 아직 쉽지 않은 것 같습니다. :) 보여 드리겠습니다 ( Intel 구문 을 사용하는 x86 asm ).


1. 스택이란?

스택은 일반적으로 시작하기 전에 모든 스레드에 할당 된 연속적인 메모리 청크입니다. 원하는 것은 무엇이든 저장할 수 있습니다. C ++ 용어 ( 코드 조각 # 1 ) :

const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];

2. 스택의 상단 및 하단

원칙적으로 stack배열의 임의의 셀에 값을 저장할 수 있습니다 ( snippet # 2.1 ) :

stack[333] = 123;
stack[517] = 456;
stack[555] = stack[333] + stack[517];

그러나 어떤 세포 stack가 이미 사용 중이고 어떤 세포 가 "무료" 인지 기억하는 것이 얼마나 어려울 지 상상해보십시오 . 이것이 바로 스택에 새로운 값을 저장하는 이유입니다.

(x86) asm의 스택에 대한 한 가지 이상한 점은 마지막 인덱스부터 시작하여 더 낮은 인덱스로 이동하는 것입니다. stack [999], stack [998] 등 ( snippet # 2.2 ) :

stack[999] = 123;
stack[998] = 456;
stack[997] = stack[999] + stack[998];

그리고 아직도의 "공식"이름 (주의, 당신은 지금 혼동거야) stack[999]입니다 스택의 바닥을 .
(마지막으로 사용한 셀 stack[997]위의 예제에서)이 호출 스택의 상단 (참조 스택의 상단이 x86에서 인 경우 ).


3. 스택 포인터 (SP)

이 논의의 목적을 위해 CPU 레지스터가 전역 변수로 표시된다고 가정합니다 ( 범용 레지스터 참조 ).

int AX, BX, SP, BP, ...;
int main(){...}

스택의 맨 위를 추적하는 특수 CPU 레지스터 (SP)가 있습니다. SP는 포인터 (0xAAAABBCC와 같은 메모리 주소 보유)입니다. 그러나이 게시물의 목적을 위해 배열 인덱스 (0, 1, 2, ...)로 사용할 것입니다.

스레드가 시작되면 SP == STACK_CAPACITY프로그램과 OS가 필요에 따라 수정합니다. 규칙은 스택의 최상위를 넘어서 스택 셀에 쓸 수 없으며 SP보다 작은 인덱스는 유효하지 않고 안전하지 않으므로 ( 시스템 인터럽트로 인해 ) 먼저 SP를 감소 시킨 다음 새로 할당 된 셀에 값을 씁니다.

스택의 여러 값을 연속으로 푸시하려는 경우 모든 값에 대한 공간을 미리 예약 할 수 있습니다 ( 스 니펫 # 3 ).

SP -= 3;
stack[999] = 12;
stack[998] = 34;
stack[997] = stack[999] + stack[998];

노트. 이제 스택에서의 할당이 왜 그렇게 빠른지 알 수 있습니다. 단일 레지스터 감소 일뿐입니다.


4. 지역 변수

이 단순한 함수 ( 스 니펫 # 4.1 )를 살펴 보겠습니다 .

int triple(int a) {
    int result = a * 3;
    return result;
}

지역 변수를 사용하지 않고 다시 작성합니다 ( snippet # 4.2 ) :

int triple_noLocals(int a) {
    SP -= 1; // move pointer to unused cell, where we can store what we need
    stack[SP] = a * 3;
    return stack[SP];
}

어떻게 호출되는지 확인하십시오 ( snippet # 4.3 ) :

// SP == 1000
someVar = triple_noLocals(11);
// now SP == 999, but we don't need the value at stack[999] anymore
// and we will move the stack index back, so we can reuse this cell later
SP += 1; // SP == 1000 again

5. 푸시 / 팝

스택 상단에 새로운 요소를 추가하는 것은 매우 빈번한 작업이므로 CPU에는 이에 대한 특별한 명령이 push있습니다. 다음과 같이 표현합니다 ( snippet 5.1 ) :

void push(int value) {
    --SP;
    stack[SP] = value;
}

마찬가지로 스택의 최상위 요소 ( 스 니펫 5.2 )를 가져옵니다 .

void pop(int& result) {
    result = stack[SP];
    ++SP; // note that `pop` decreases stack's size
}

푸시 / 팝의 일반적인 사용 패턴은 일시적으로 일부 값을 절약합니다. 변수에 유용한 것이 myVar있고 어떤 이유로이를 덮어 쓰는 계산을해야 한다고 가정 해 보겠습니다 ( snippet 5.3 ) :

int myVar = ...;
push(myVar); // SP == 999
myVar += 10;
... // do something with new value in myVar
pop(myVar); // restore original value, SP == 1000

6. 기능 매개 변수

이제 스택 ( snippet # 6 )을 사용하여 매개 변수를 전달해 보겠습니다 .

int triple_noL_noParams() { // `a` is at index 999, SP == 999
    SP -= 1; // SP == 998, stack[SP + 1] == a
    stack[SP] = stack[SP + 1] * 3;
    return stack[SP];
}

int main(){
    push(11); // SP == 999
    assert(triple(11) == triple_noL_noParams());
    SP += 2; // cleanup 1 local and 1 parameter
}

7. return진술

AX 레지스터 ( snippet # 7 )에 값을 반환 해 보겠습니다 .

void triple_noL_noP_noReturn() { // `a` at 998, SP == 998
    SP -= 1; // SP == 997

    stack[SP] = stack[SP + 1] * 3;
    AX = stack[SP];

    SP += 1; // finally we can cleanup locals right in the function body, SP == 998
}

void main(){
    ... // some code
    push(AX); // save AX in case there is something useful there, SP == 999
    push(11); // SP == 998
    triple_noL_noP_noReturn();
    assert(triple(11) == AX);
    SP += 1; // cleanup param
             // locals were cleaned up in the function body, so we don't need to do it here
    pop(AX); // restore AX
    ...
}

8. 스택베이스 포인터 (BP) ( 프레임 포인터 라고도 함 ) 및 스택 프레임

더 많은 "고급"함수를 사용하여 asm과 유사한 C ++ ( snippet # 8.1 )로 다시 작성해 보겠습니다 .

int myAlgo(int a, int b) {
    int t1 = a * 3;
    int t2 = b * 3;
    return t1 - t2;
}

void myAlgo_noLPR() { // `a` at 997, `b` at 998, old AX at 999, SP == 997
    SP -= 2; // SP == 995

    stack[SP + 1] = stack[SP + 2] * 3; 
    stack[SP]     = stack[SP + 3] * 3;
    AX = stack[SP + 1] - stack[SP];

    SP += 2; // cleanup locals, SP == 997
}

int main(){
    push(AX); // SP == 999
    push(22); // SP == 998
    push(11); // SP == 997
    myAlgo_noLPR();
    assert(myAlgo(11, 22) == AX);
    SP += 2;
    pop(AX);
}

이제 tripple(스 니펫 # 4.1) 에서와 같이 반환하기 전에 결과를 저장할 새로운 지역 변수를 도입하기로 결정했다고 상상해보십시오 . 함수의 본문은 다음과 같습니다 ( snippet # 8.2 ).

SP -= 3; // SP == 994
stack[SP + 2] = stack[SP + 3] * 3; 
stack[SP + 1] = stack[SP + 4] * 3;
stack[SP]     = stack[SP + 2] - stack[SP + 1];
AX = stack[SP];
SP += 3;

우리는 함수 매개 변수와 지역 변수에 대한 모든 참조를 업데이트해야했습니다. 이를 방지하려면 스택이 커질 때 변경되지 않는 앵커 인덱스가 필요합니다.

현재 최상위 (SP 값)를 BP 레지스터에 저장하여 함수 입력시 (로컬에 공간을 할당하기 전에) 앵커를 생성합니다. 스 니펫 # 8.3 :

void myAlgo_noLPR_withAnchor() { // `a` at 997, `b` at 998, SP == 997
    push(BP);   // save old BP, SP == 996
    BP = SP;    // create anchor, stack[BP] == old value of BP, now BP == 996
    SP -= 2;    // SP == 994

    stack[BP - 1] = stack[BP + 1] * 3;
    stack[BP - 2] = stack[BP + 2] * 3;
    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP;    // cleanup locals, SP == 996
    pop(BP);    // SP == 997
}

함수에 속해 있고 완전히 제어되는 스택 슬라이스를 함수 스택 프레임 이라고 합니다 . 예를 들어 myAlgo_noLPR_withAnchor스택 프레임은 stack[996 .. 994](둘 다 idexe 포함)입니다.
프레임은 함수의 BP에서 시작하고 (함수 내에서 업데이트 한 후) 다음 스택 프레임까지 지속됩니다. 따라서 스택의 매개 변수는 호출자의 스택 프레임의 일부입니다 (참고 8a 참조).

참고 :
8a. Wikipedia는 매개 변수에 대해 달리 언급 하지만 여기서는 Intel 소프트웨어 개발자 설명서를 준수합니다 . vol. 1, 섹션 6.2.4.1 스택 프레임베이스 포인터 및 섹션 6.3.2 원거리 통화 및 RET 작동의 그림 6-2 . 함수의 매개 변수와 스택 프레임은 함수의 활성화 레코드의 일부입니다 ( 함수 주변에 대한 생성 참조 ).
8b. BP에서 함수 매개 변수를 가리키는 양의 오프셋과 지역 변수를 가리키는 음의 오프셋.
8c 디버깅에 매우 편리합니다 . stack[BP]이전 스택 프레임의 주소를 저장하고,stack[stack[BP]]이전 스택 프레임 등을 저장합니다. 이 체인을 따라 가면 아직 반환되지 않은 프로그램의 모든 함수 프레임을 찾을 수 있습니다. 이것이 디버거가 스택
8d 를 호출하는 방법 입니다. myAlgo_noLPR_withAnchor프레임을 설정하는 의 처음 3 개 명령어 (이전 BP 저장, BP 업데이트, 지역 사용자를위한 공간 예약)를 함수 프롤로그 라고 합니다.


9. 호출 규칙

스 니펫 8.1에서는에 대한 매개 변수를 myAlgo오른쪽에서 왼쪽으로 푸시 하고 결과를 AX. params를 왼쪽에서 오른쪽으로 전달하고 BX. 또는 BX 및 CX에서 매개 변수를 전달하고 AX로 반환합니다. 분명히 호출자 ( main())와 호출 된 함수는이 모든 항목이 저장되는 위치와 순서에 동의해야합니다.

호출 규칙 은 매개 변수가 전달되고 결과가 반환되는 방법에 대한 일련의 규칙입니다.

위의 코드에서 cdecl 호출 규칙을 사용했습니다 .

  • 매개 변수는 호출시 스택의 최하위 주소에있는 첫 번째 인수와 함께 스택에 전달됩니다 (마지막 <...> 푸시 됨). 호출자는 호출 후 스택에서 매개 변수를 다시 팝하는 책임이 있습니다.
  • 반환 값은 AX에 배치됩니다.
  • myAlgo_noLPR_withAnchor호출자 ( main함수)가 호출에 의해 변경되지 않은 레지스터에 의존 할 수 있도록 EBP 및 ESP는 호출 수신자 ( 우리의 경우 함수)에 의해 보존되어야합니다 .
  • 다른 모든 레지스터 (EAX, <...>)는 수신자가 자유롭게 수정할 수 있습니다. 호출자가 함수 호출 전후의 값을 유지하려면 다른 곳에 값을 저장해야합니다 (AX로이 작업을 수행합니다).

(출처 : Stack Overflow Documentation의 "32 비트 cdecl"예, icktoofayPeter Cordes의 저작권 2016 , CC BY-SA 3.0에 따라 라이센스가 부여되었습니다. 전체 Stack Overflow Documentation 콘텐츠아카이브는 archive.org에서 찾을 수 있습니다. 이 예는 주제 ID 3261 및 예 ID 11196으로 색인이 생성됩니다.)


10. 함수 호출

이제 가장 흥미로운 부분입니다. 데이터와 마찬가지로 실행 코드도 메모리에 저장되며 (스택 메모리와 완전히 관련이 없음) 모든 명령어에는 주소가 있습니다.
다른 명령이 없으면 CPU는 메모리에 저장된 순서대로 명령을 차례로 실행합니다. 그러나 우리는 CPU에 메모리의 다른 위치로 "점프"하고 거기에서 명령을 실행할 수 있습니다. asm에서는 모든 주소가 될 수 있으며 C ++와 같은 더 높은 수준의 언어에서는 레이블로 표시된 주소로만 이동할 수 있습니다 ( 해결 방법이 있지만 적어도 예쁘지는 않습니다).

이 함수를 보겠습니다 ( snippet # 10.1 ) :

int myAlgo_withCalls(int a, int b) {
    int t1 = triple(a);
    int t2 = triple(b);
    return t1 - t2;
}

그리고 trippleC ++ 방식 으로 호출하는 대신 다음을 수행하십시오.

  1. tripple의 코드를 myAlgo본문 시작 부분에 복사
  2. myAlgo항목 에서 tripple코드를 뛰어 넘다goto
  3. tripple의 코드 를 실행해야 할 때 tripple호출 직후 코드 라인의 스택 주소에 저장하면 나중에 여기로 돌아와 실행을 계속할 수 있습니다 ( PUSH_ADDRESS아래 매크로 참조).
  4. 첫 번째 줄 ( tripple함수) 의 주소로 점프 하고 끝까지 실행합니다 (3.과 4.는 함께 CALL매크로 임).
  5. 마지막에 tripple(로컬을 정리 한 후) 스택 맨 위에서 반환 주소를 가져 와서 거기로 이동합니다 ( RET매크로).

C ++에서 특정 코드 주소로 쉽게 이동할 수있는 방법이 없기 때문에 레이블을 사용하여 점프 위치를 표시합니다. 아래의 매크로가 어떻게 작동하는지 자세히 설명하지는 않겠습니다. 내가 말한대로 작동한다고 믿으세요 ( 스 니펫 # 10.2 ).

// pushes the address of the code at label's location on the stack
// NOTE1: this gonna work only with 32-bit compiler (so that pointer is 32-bit and fits in int)
// NOTE2: __asm block is specific for Visual C++. In GCC use https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
#define PUSH_ADDRESS(labelName) {               \
    void* tmpPointer;                           \
    __asm{ mov [tmpPointer], offset labelName } \
    push(reinterpret_cast<int>(tmpPointer));    \
}

// why we need indirection, read https://stackoverflow.com/a/13301627/264047
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)

// generates token (not a string) we will use as label name. 
// Example: LABEL_NAME(155) will generate token `lbl_155`
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)

#define CALL_IMPL(funcLabelName, callId)    \
    PUSH_ADDRESS(LABEL_NAME(callId));       \
    goto funcLabelName;                     \
    LABEL_NAME(callId) :

// saves return address on the stack and jumps to label `funcLabelName`
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)

// takes address at the top of stack and jump there
#define RET() {                                         \
    int tmpInt;                                         \
    pop(tmpInt);                                        \
    void* tmpPointer = reinterpret_cast<void*>(tmpInt); \
    __asm{ jmp tmpPointer }                             \
}

void myAlgo_asm() {
    goto my_algo_start;

triple_label:
    push(BP);
    BP = SP;
    SP -= 1;

    // stack[BP] == old BP, stack[BP + 1] == return address
    stack[BP - 1] = stack[BP + 2] * 3;
    AX = stack[BP - 1];

    SP = BP;     
    pop(BP);
    RET();

my_algo_start:
    push(BP);   // SP == 995
    BP = SP;    // BP == 995; stack[BP] == old BP, 
                // stack[BP + 1] == dummy return address, 
                // `a` at [BP + 2], `b` at [BP + 3]
    SP -= 2;    // SP == 993

    push(AX);
    push(stack[BP + 2]);
    CALL(triple_label);
    stack[BP - 1] = AX;
    SP -= 1;
    pop(AX);

    push(AX);
    push(stack[BP + 3]);
    CALL(triple_label);
    stack[BP - 2] = AX;
    SP -= 1;
    pop(AX);

    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP; // cleanup locals, SP == 997
    pop(BP);
}

int main() {
    push(AX);
    push(22);
    push(11);
    push(7777); // dummy value, so that offsets inside function are like we've pushed return address
    myAlgo_asm();
    assert(myAlgo_withCalls(11, 22) == AX);
    SP += 1; // pop dummy "return address"
    SP += 2;
    pop(AX);
}

참고 :
10a. 반환 주소는 스택에 저장되기 때문에 원칙적으로 변경할 수 있습니다. 이것이 스택 스매싱 공격이 작동하는 방식입니다
10b. triple_label(지역 정리, 이전 BP 복원, 반환) 의 "끝"에있는 마지막 3 개의 명령을 함수의 에필로그 라고 합니다.


11. 조립

이제 real asm for myAlgo_withCalls. Visual Studio에서이를 수행하려면 :

  • 86로 설정 빌드 플랫폼 ( 하지 x86_64의를)
  • 빌드 유형 : 디버그
  • myAlgo_withCalls 내부 어딘가에 중단 점 설정
  • 실행하고 중단 점에서 실행이 중지되면 Ctrl + Alt + D를 누릅니다.

asm과 유사한 C ++의 한 가지 차이점은 asm의 스택이 int 대신 바이트에서 작동한다는 것입니다. 따라서 하나의 공간을 예약하기 위해 intSP는 4 바이트 씩 감소합니다.
여기에 있습니다 ( snippet # 11.1 , 주석의 줄 번호는 요점 에서 가져온 것입니다 ) :

;   114: int myAlgo_withCalls(int a, int b) {
 push        ebp        ; create stack frame 
 mov         ebp,esp  
; return address at (ebp + 4), `a` at (ebp + 8), `b` at (ebp + 12)
 
 sub         esp,0D8h   ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal 
 
 push        ebx        ; cdecl requires to save all these registers
 push        esi  
 push        edi  
 
 ; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h)
 ; see https://stackoverflow.com/q/3818856/264047
 ; I guess that's for ease of debugging, so that stack is filled with recognizable values
 ; 0CCCCCCCCh in binary is 110011001100...
 lea         edi,[ebp-0D8h]     
 mov         ecx,36h    
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 
;   115:    int t1 = triple(a);
 mov         eax,dword ptr [ebp+8]   ; push parameter `a` on the stack
 push        eax  
 
 call        triple (01A13E8h)  
 add         esp,4                   ; clean up param 
 mov         dword ptr [ebp-8],eax   ; copy result from eax to `t1`
 
;   116:    int t2 = triple(b);
 mov         eax,dword ptr [ebp+0Ch] ; push `b` (0Ch == 12)
 push        eax  
 
 call        triple (01A13E8h)  
 add         esp,4  
 mov         dword ptr [ebp-14h],eax ; t2 = eax
 
 mov         eax,dword ptr [ebp-8]   ; calculate and store result in eax
 sub         eax,dword ptr [ebp-14h]  

 pop         edi  ; restore registers
 pop         esi  
 pop         ebx  
 
 add         esp,0D8h  ; check we didn't mess up esp or ebp. this is only for debug builds
 cmp         ebp,esp  
 call        __RTC_CheckEsp (01A116Dh)  
 
 mov         esp,ebp  ; destroy frame
 pop         ebp  
 ret  

asm for tripple( snippet # 11.2 ) :

 push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-0CCh]  
 mov         ecx,33h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 imul        eax,dword ptr [ebp+8],3  
 mov         dword ptr [ebp-8],eax  
 mov         eax,dword ptr [ebp-8]  
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

이 게시물을 읽은 후 어셈블리가 이전처럼 비밀스럽지 않습니다. :)


다음은 게시물 본문의 링크와 추가 자료입니다.


제가이 질문을 한 것은 오래 전이었습니다. 정말 심층적 인 답변입니다. 감사.
bplus

답변의 초기 부분에서 레지스터에 16 비트 이름을 사용하는 이유는 무엇입니까? 실제 16 비트 코드에 대해 이야기하고 있다면 [SP]유효한 주소 지정 16 비트 모드가 아닙니다. 아마도 사용하는 것이 가장 좋습니다 ESP. 당신이 선언하는 경우 또한, SPint로서 int, 당신은 모든 요소에 대해 4를 수정해야하지 1. (당신이 선언 된 경우 long *SP, 다음 SP += 2에 의해 증가 것이다 2 * sizeof(int), 따라서이 개 요소를 제거합니다. 그러나와 intSP, 즉해야 SP += 8처럼 add esp, 8. 32 -bit asm.
Peter Cordes

매혹적인! C를 사용하여 어셈블리를 설명하는 것이 흥미 롭다고 생각합니다. 전에는 본 적이 없습니다. 산뜻한. "지역 변수 없음"을 "지역 변수 작동 방식"또는 "지역 변수"로 이름을 바꾸는 것이 좋습니다.
Dave Dopson

@PeterCordes 16 비트 이름 (SP, BP)의 이유는 명확성 때문입니다. SP는 "스택 포인터"로 쉽게 변환됩니다. 적절한 32 비트 이름을 사용하는 경우 16/32/64 비트 모드의 차이점을 설명하거나 설명하지 않은 상태로 두어야합니다. 제 의도는 자바 나 파이썬 만 아는 사람이 머리를 긁지 않고 글을 따를 수 있다는 것이 었습니다. 그리고 메모리 주소 지정은 독자의주의를 분산시킬 뿐이라고 생각합니다. 또한 호기심이 많은 주제에 대한 위키 북 링크를 게시하고 게시물 끝에 ESP에 대한 몇 마디를 말했습니다.
Alexander Malakhov

1
이를 방지하려면 스택이 커질 때 변경되지 않는 앵커 인덱스가 필요합니다. 필요는 잘못된 단어입니다. -fomit-frame-pointer수년간 gcc 및 clang의 기본값이었습니다. 실제 asm을 보는 사람들은 EBP / RBP가 일반적으로 프레임 포인터로 사용되지 않는다는 것을 알아야합니다. "전통적으로 인간은 푸시 / 팝으로 변경되지 않는 앵커를 원했지만 컴파일러는 오프셋 변경을 추적 할 수 있습니다." 그런 다음 역 추적에 대한 섹션을 업데이트하여 DWARF .eh_frame메타 데이터 또는 Windows x86-64 메타 데이터를 사용할 수 있을 때 기본적으로 사용되지 않는 레거시 방법이라고 말할 수 있습니다.
Peter Cordes 2018

7

스택이 하드웨어에서 구현되는지 여부와 관련하여이 Wikipedia 기사 가 도움 이 될 수 있습니다.

x86과 같은 일부 프로세서 제품군에는 현재 실행중인 스레드의 스택을 조작하기위한 특수 지침이 있습니다. PowerPC 및 MIPS를 포함한 다른 프로세서 제품군은 명시 적 스택 지원이 없지만 대신 규칙에 의존하고 스택 관리를 운영 체제의 ABI (Application Binary Interface)에 위임합니다.

이 기사와 링크 된 다른 기사는 프로세서의 스택 사용에 대한 느낌을 얻는 데 유용 할 수 있습니다.


4

개념

먼저 모든 것을 발명 한 사람인 것처럼 생각하십시오. 이렇게 :

먼저 배열을 생각하고 저수준에서 구현되는 방법을 생각하십시오 .--> 기본적으로 일련의 연속적인 메모리 위치 (서로 옆에있는 메모리 위치)입니다. 이제 머릿속에 정신적 이미지가 생겼으니, 메모리 위치에 액세스 할 수 있고 어레이에서 데이터를 제거하거나 추가 할 때 원하는대로 삭제할 수 있다는 사실을 생각해보십시오. 이제 동일한 어레이를 생각하지만 위치를 삭제할 가능성 대신 어레이에서 데이터를 제거하거나 추가 할 때 마지막 위치 만 삭제하기로 결정합니다. 이제 그런 방식으로 해당 배열의 데이터를 조작하는 새로운 아이디어를 LIFO라고하며 이는 Last In First Out을 의미합니다. 당신의 아이디어는 배열에서 무언가를 제거 할 때마다 정렬 알고리즘을 사용하지 않고도 배열의 내용을 쉽게 추적 할 수 있기 때문에 매우 좋습니다. 또한, 배열에있는 마지막 객체의 주소가 무엇인지 항상 알기 위해 Cpu에 레지스터 하나를 할당하여 추적합니다. 이제 레지스터가이를 추적하는 방법은 배열에서 무언가를 제거하거나 추가 할 때마다 배열에서 제거하거나 추가 한 객체의 양만큼 레지스터의 주소 값을 감소 또는 증가시키는 것입니다. 그들이 차지한 주소 공간의 양). 또한 레지스터를 감소 또는 증가시키는 양이 객체 당 하나의 양 (예 : 4 개의 메모리 위치, 즉 4 바이트)으로 고정되어 있는지 확인하여 추적을보다 쉽게 ​​유지하고 가능하게합니다. 루프는 반복 당 고정 증분을 사용하기 때문에 일부 루프 구조와 함께 해당 레지스터를 사용합니다 (예 : 루프를 사용하여 배열을 반복하려면 레지스터를 반복 할 때마다 레지스터를 4 씩 증가시키는 루프를 구성합니다. 배열에 크기가 다른 객체가 있으면 불가능합니다. 마지막으로,이 새로운 데이터 구조를 "스택"이라고 부르도록 선택합니다. 레스토랑의 접시 스택이 항상 해당 스택 상단에있는 접시를 제거하거나 추가하는 것을 상기시키기 때문입니다.

구현

보시다시피 스택은 조작 방법을 결정한 연속적인 메모리 위치의 배열에 지나지 않습니다. 따라서 스택을 제어하기 위해 특수 명령어와 레지스터를 사용할 필요조차 없다는 것을 알 수 있습니다. 기본 mov, add 및 sub 명령어를 사용하고 다음과 같이 ESP 및 EBP 대신 범용 레지스터를 사용하여 직접 구현할 수 있습니다.

mov edx, 0FFFFFFFFh

; -> 이것은 코드와 데이터에서 가장 먼 스택의 시작 주소가 될 것이며, 앞서 설명한 스택의 마지막 객체를 추적하는 레지스터 역할도합니다. 이를 "스택 포인터"라고 부르므로 ESP가 일반적으로 사용되는 레지스터 EDX를 선택합니다.

서브 에디션, 4

mov [edx], dword ptr [someVar]

; -> 이 두 명령어는 스택 포인터를 4 개의 메모리 위치만큼 감소시키고 [someVar] 메모리 위치에서 시작하는 4 바이트를 현재 EDX가 가리키는 메모리 위치로 복사합니다. 마치 PUSH 명령어가 ESP를 감소시키는 것처럼 여기에서만 가능합니다. 수동으로 EDX를 사용했습니다. 따라서 PUSH 명령어는 기본적으로 ESP로 실제로이 작업을 수행하는 더 짧은 opcode입니다.

mov eax, dword ptr [edx]

edx, 4 추가

; -> 그리고 여기서는 반대로, EDX가 현재 가리키는 메모리 위치에서 시작하여 4 바이트를 레지스터 EAX로 복사합니다 (여기서 임의로 선택하면 원하는 위치에 복사 할 수 있음). 그리고 스택 포인터 EDX를 4 개의 메모리 위치만큼 증가시킵니다. 이것이 POP 명령어가하는 일입니다.

이제 명령 PUSH 및 POP 및 레지스터 ESP 및 EBP가 인텔에서 방금 추가 한 "스택"데이터 구조의 위 개념을 더 쉽게 쓰고 읽을 수 있음을 알 수 있습니다. PUSH ans POP 명령어와 스택 조작을위한 전용 레지스터가없는 RISC (Reduced Instruction Set) CPU가 여전히 있습니다. 이러한 CPU에 대한 어셈블리 프로그램을 작성하는 동안 다음과 같이 스택을 직접 구현해야합니다. 내가 보여 줬어.


3

추상 스택과 하드웨어 구현 스택을 혼동합니다. 후자는 이미 구현되어 있습니다.


3

나는 당신이 찾고있는 주요 답변이 이미 암시되었다고 생각합니다.

x86 컴퓨터가 부팅되면 스택이 설정되지 않습니다. 프로그래머는 부팅시 명시 적으로 설정해야합니다. 그러나 이미 운영 체제에있는 경우에는 처리되었습니다. 다음은 간단한 부트 스트랩 프로그램의 코드 샘플입니다.

먼저 데이터 및 스택 세그먼트 레지스터가 설정되고 스택 포인터가 0x4000 이상으로 설정됩니다.


    movw    $BOOT_SEGMENT, %ax
    movw    %ax, %ds
    movw    %ax, %ss
    movw    $0x4000, %ax
    movw    %ax, %sp

이 코드 후에 스택을 사용할 수 있습니다. 이제 여러 가지 방법으로 할 수 있다고 확신하지만 이것이 아이디어를 설명해야한다고 생각합니다.


3

스택은 프로그램과 함수가 메모리를 사용하는 방식입니다.

스택은 항상 나를 혼란스럽게했기 때문에 그림을 만들었습니다.

스택은 종유석 같아

( 여기에 svg 버전 )

누군가에게 도움이되는지 아니면 혼란 스러운지 확실하지 않습니다. 나는 그것이 옳다고 믿는다. SVG 이미지를 자유롭게 사용하십시오. 공개 도메인.


1

스택이 이미 존재하므로 코드를 작성할 때이를 가정 할 수 있습니다. 스택에는 함수의 반환 주소, 지역 변수 및 함수간에 전달되는 변수가 포함됩니다. 사용할 수있는 BP, SP (Stack Pointer) 내장과 같은 스택 레지스터도 있으므로 언급 한 내장 명령이 있습니다. 스택이 아직 구현되지 않은 경우 함수를 실행할 수없고 코드 흐름이 작동하지 않습니다.


1

스택은 스택 포인터 (여기서 x86 아키텍처 가정)를 통해 스택 세그먼트를 가리키는 "구현"됩니다 . 스택에 무언가가 푸시 될 때마다 (pushl, call 또는 유사한 스택 opcode를 통해) 스택 포인터가 가리키는 주소에 기록되고 스택 포인터가 감소합니다 (스택이 아래쪽으로 증가합니다 . 즉, 더 작은 주소). . 스택에서 무언가를 꺼내면 (popl, ret) 스택 포인터가 증가합니다. 하고 값이 스택에서 읽 힙니다.

사용자 공간 응용 프로그램에서 스택은 응용 프로그램이 시작될 때 이미 설정되어 있습니다. 커널 공간 환경에서는 먼저 스택 세그먼트와 스택 포인터를 설정해야합니다.


1

특별히 Gas 어셈블러를 보지 못했지만 일반적으로 스택은 스택의 맨 위가있는 메모리의 위치에 대한 참조를 유지하여 "구현"됩니다. 메모리 위치는 레지스터에 저장됩니다. 레지스터는 아키텍처마다 이름이 다르지만 스택 포인터 레지스터로 생각할 수 있습니다.

팝 및 푸시 명령은 마이크로 명령을 기반으로 구축하여 대부분의 아키텍처에서 구현됩니다. 그러나 일부 "교육용 아키텍처"는 직접 구현해야합니다. 기능적으로 푸시는 다음과 같이 구현됩니다.

   load the address in the stack pointer register to a gen. purpose register x
   store data y at the location x
   increment stack pointer register by size of y

또한 일부 아키텍처는 마지막으로 사용 된 메모리 주소를 스택 포인터로 저장합니다. 일부는 사용 가능한 다음 주소를 저장합니다.


1

스택이란? 스택은 컴퓨터에 정보를 저장하는 수단 인 데이터 구조의 한 유형입니다. 스택에 새 개체를 입력하면 이전에 입력 한 모든 개체 위에 배치됩니다. 즉, 스택 데이터 구조는 카드, 종이, 신용 카드 우편물 또는 생각할 수있는 기타 실제 개체의 스택과 같습니다. 스택에서 개체를 제거하면 맨 위에있는 개체가 먼저 제거됩니다. 이 방법을 LIFO (last in, first out)라고합니다.

"스택"이라는 용어는 네트워크 프로토콜 스택의 약자 일 수도 있습니다. 네트워킹에서 컴퓨터 간의 연결은 일련의 작은 연결을 통해 이루어집니다. 이러한 연결 또는 계층은 동일한 방식으로 구축 및 폐기된다는 점에서 스택 데이터 구조처럼 작동합니다.


0

스택이 데이터 구조라는 것이 맞습니다. 종종 작업하는 데이터 구조 (스택 포함)는 추상적이며 메모리의 표현으로 존재합니다.

이 경우 작업중인 스택은 더 많은 물질적 존재를 가지며 프로세서의 실제 물리적 레지스터에 직접 매핑됩니다. 데이터 구조로서 스택은 입력 된 역순으로 데이터가 제거되도록하는 FILO (선입 선출) 구조입니다. 비주얼은 StackOverflow 로고를 참조하십시오! ;)

당신은 작업하는 명령 스택 . 이것은 프로세서에 공급하는 실제 명령 스택입니다.


잘못된. 이것은 '명령 스택'이 아닙니다 (그런 것이 있습니까?) 이것은 단순히 스택 레지스터를 통해 액세스되는 메모리입니다. 임시 저장, 프로 시저 매개 변수 및 함수 호출을위한 (가장 중요한) 반환 주소에 사용
Javier

0

호출 스택은 x86 명령어 세트와 운영 체제에 의해 구현됩니다.

푸시 및 팝과 같은 명령은 스택 포인터를 조정하는 반면 운영 체제는 각 스레드에 대해 스택이 증가함에 따라 메모리 할당을 처리합니다.

x86 스택이 높은 주소에서 낮은 주소로 "성장"한다는 사실은이 아키텍처 를 버퍼 오버플로 공격에취약 하게 만듭니다 .


1
x86 스택이 커진다는 사실이 왜 버퍼 오버 플로우에 더 취약하게 만들까요? 확장 세그먼트로 동일한 오버플로를 얻을 수 없습니까?
Nathan Fellman

@nathan : 애플리케이션이 스택에 음의 메모리를 할당하도록 할 수있는 경우에만 가능합니다.
Javier

1
버퍼 오버플로 공격은 스택 기반 배열의 끝을 지나서 작성합니다. char userName [256], 이렇게하면 메모리를 낮은 값에서 높은 값으로 기록하여 반환 주소와 같은 것을 덮어 쓸 수 있습니다. 스택이 같은 방향으로 커지면 할당되지 않은 스택 만 덮어 쓸 수 있습니다.
Maurice Flanagan

0

스택이 '그냥'데이터 구조라는 것이 맞습니다. 그러나 여기서는 "더 스택"이라는 특수 목적으로 사용되는 하드웨어 구현 스택을 의미합니다.

많은 사람들이 하드웨어 구현 스택과 (소프트웨어) 스택 데이터 구조에 대해 언급했습니다. 세 가지 주요 스택 구조 유형이 있음을 추가하고 싶습니다.

  1. 호출 스택-당신이 묻는 것입니다! 그것은 기능 매개 변수와 반환 주소 등을 저장합니다. 그 책에있는 4 장 (4 페이지 즉 53 페이지에 관한 모든 것) 기능을 읽으십시오. 좋은 설명이 있습니다.
  2. 특별한 일을하기 위해 프로그램에서 사용할 수있는 일반적인 스택 ...
  3. 일반적인 하드웨어 스택
    나는 이것에 대해 확신하지 못하지만 어딘가에서 일부 아키텍처에서 사용할 수있는 범용 하드웨어 구현 스택이 있다는 것을 읽은 기억이 있습니다. 이것이 올바른지 아는 사람이 있으면 의견을 말하십시오.

가장 먼저 알아야 할 것은이 책에서 설명하는 프로그래밍중인 아키텍처입니다 (방금 검색 --link). 정말로 이해하기 위해 x86의 메모리, 주소 지정, 레지스터 및 아키텍처에 대해 배우는 것이 좋습니다 (이 책에서 배우고있는 내용이라고 가정합니다).


0

LIFO 방식으로 로컬 상태를 저장하고 복원해야하는 함수 호출 (일반화 된 코 루틴 접근 방식과 반대)은 어셈블리 언어와 CPU 아키텍처가 기본적으로이 기능을 구축하는 매우 일반적인 요구 사항입니다. 스레딩, 메모리 보호, 보안 수준 등의 개념으로 말할 수 있습니다. 이론적으로는 자신의 스택, 호출 규칙 등을 구현할 수 있지만 일부 opcode와 대부분의 기존 런타임은이 기본 개념 인 "스택"에 의존한다고 가정합니다. .


0

stack기억의 일부입니다. 그것은 사용 inputoutputfunctions. 또한 함수의 반환을 기억하는 데 사용됩니다.

esp 레지스터는 스택 주소를 기억하는 것입니다.

stackesp하드웨어에 의해 구현된다. 또한 직접 구현할 수도 있습니다. 프로그램이 매우 느려질 것입니다.

예:

esp아니요 // = 0012ffc4

푸시 0 // esp= 0012ffc0, Dword [0012ffc0] = 00000000

proc01 호출 // esp= 0012ffbc, Dword [0012ffbc] = eip, eip= adrr [proc01]

eax// eax= Dword [ esp], esp= esp+ 4


0

나는 스택이 기능 측면에서 어떻게 작동하는지에 대해 찾고 있었고, 이 블로그 가 훌륭하고 처음부터 스택에 대한 개념을 설명하고 스택에 스택 값을 저장하는 방법을 발견 했습니다 .

이제 당신의 대답에. 나는 파이썬으로 설명 할 것이지만 스택이 모든 언어에서 어떻게 작동하는지 좋은 아이디어를 얻을 것입니다.

여기에 이미지 설명 입력

그것의 프로그램 :

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(3)

여기에 이미지 설명 입력

여기에 이미지 설명 입력

출처 : Cryptroix

블로그에서 다루는 주제 중 일부 :

How Function work ?
Calling a Function
 Functions In a Stack
 What is Return Address
 Stack
Stack Frame
Call Stack
Frame Pointer (FP) or Base Pointer (BP)
Stack Pointer (SP)
Allocation stack and deallocation of stack
StackoverFlow
What is Heap?

그러나 파이썬 언어로 설명하므로 원하는 경우 살펴볼 수 있습니다.


Criptoix 사이트는 죽은 및 web.archive.org에는 복사본이 없습니다
알렉산더 Malakhov

1
@AlexanderMalakhov Cryptroix가 호스팅 문제로 인해 작동하지 않았습니다. Cryptroix는 현재 작동 중입니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.