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

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

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

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

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

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

pushl
popl

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

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



답변

나는 주로 당신이 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


답변

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

저는 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

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


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


답변

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

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

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


답변

개념

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

먼저 배열을 생각하고 저수준에서 구현되는 방법을 생각하십시오 .–> 기본적으로 일련의 연속적인 메모리 위치 (서로 옆에있는 메모리 위치)입니다. 이제 머릿속에 정신적 이미지가 생겼으니, 메모리 위치에 액세스 할 수 있고 어레이에서 데이터를 제거하거나 추가 할 때 원하는대로 삭제할 수 있다는 사실을 생각해보십시오. 이제 동일한 어레이를 생각하지만 위치를 삭제할 가능성 대신 어레이에서 데이터를 제거하거나 추가 할 때 마지막 위치 만 삭제하기로 결정합니다. 이제 그런 방식으로 해당 배열의 데이터를 조작하는 새로운 아이디어를 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에 대한 어셈블리 프로그램을 작성하는 동안 다음과 같이 스택을 직접 구현해야합니다. 내가 보여 줬어.


답변

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


답변

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

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

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


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

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


답변

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

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

스택은 종유석 같아

( 여기에 svg 버전 )

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