(나는 당신이 그것을 가지고 놀고 싶다면이 답변의 모든 코드 의 요점 을 만들었 습니다)
저는 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;
stack[SP] = a * 3;
return stack[SP];
}
어떻게 호출되는지 확인하십시오 ( snippet # 4.3 ) :
someVar = triple_noLocals(11);
SP += 1;
5. 푸시 / 팝
스택 상단에 새로운 요소를 추가하는 것은 매우 빈번한 작업이므로 CPU에는 이에 대한 특별한 명령이 push
있습니다. 다음과 같이 표현합니다 ( snippet 5.1 ) :
void push(int value) {
--SP;
stack[SP] = value;
}
마찬가지로 스택의 최상위 요소 ( 스 니펫 5.2 )를 가져옵니다 .
void pop(int& result) {
result = stack[SP];
++SP;
}
푸시 / 팝의 일반적인 사용 패턴은 일시적으로 일부 값을 절약합니다. 변수에 유용한 것이 myVar
있고 어떤 이유로이를 덮어 쓰는 계산을해야 한다고 가정 해 보겠습니다 ( snippet 5.3 ) :
int myVar = ...;
push(myVar);
myVar += 10;
...
pop(myVar);
6. 기능 매개 변수
이제 스택 ( snippet # 6 )을 사용하여 매개 변수를 전달해 보겠습니다 .
int triple_noL_noParams() {
SP -= 1;
stack[SP] = stack[SP + 1] * 3;
return stack[SP];
}
int main(){
push(11);
assert(triple(11) == triple_noL_noParams());
SP += 2;
}
7. return
진술
AX 레지스터 ( snippet # 7 )에 값을 반환 해 보겠습니다 .
void triple_noL_noP_noReturn() {
SP -= 1;
stack[SP] = stack[SP + 1] * 3;
AX = stack[SP];
SP += 1;
}
void main(){
...
push(AX);
push(11);
triple_noL_noP_noReturn();
assert(triple(11) == AX);
SP += 1;
pop(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() {
SP -= 2;
stack[SP + 1] = stack[SP + 2] * 3;
stack[SP] = stack[SP + 3] * 3;
AX = stack[SP + 1] - stack[SP];
SP += 2;
}
int main(){
push(AX);
push(22);
push(11);
myAlgo_noLPR();
assert(myAlgo(11, 22) == AX);
SP += 2;
pop(AX);
}
이제 tripple
(스 니펫 # 4.1) 에서와 같이 반환하기 전에 결과를 저장할 새로운 지역 변수를 도입하기로 결정했다고 상상해보십시오 . 함수의 본문은 다음과 같습니다 ( snippet # 8.2 ).
SP -= 3;
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() {
push(BP);
BP = SP;
SP -= 2;
stack[BP - 1] = stack[BP + 1] * 3;
stack[BP - 2] = stack[BP + 2] * 3;
AX = stack[BP - 1] - stack[BP - 2];
SP = BP;
pop(BP);
}
함수에 속해 있고 완전히 제어되는 스택 슬라이스를 함수 스택 프레임 이라고 합니다 . 예를 들어 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"예, icktoofay 및 Peter 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;
}
그리고 tripple
C ++ 방식 으로 호출하는 대신 다음을 수행하십시오.
tripple
의 코드를 myAlgo
본문 시작 부분에 복사
myAlgo
항목 에서 tripple
코드를 뛰어 넘다goto
tripple
의 코드 를 실행해야 할 때 tripple
호출 직후 코드 라인의 스택 주소에 저장하면 나중에 여기로 돌아와 실행을 계속할 수 있습니다 ( PUSH_ADDRESS
아래 매크로 참조).
- 첫 번째 줄 (
tripple
함수) 의 주소로 점프 하고 끝까지 실행합니다 (3.과 4.는 함께 CALL
매크로 임).
- 마지막에
tripple
(로컬을 정리 한 후) 스택 맨 위에서 반환 주소를 가져 와서 거기로 이동합니다 ( RET
매크로).
C ++에서 특정 코드 주소로 쉽게 이동할 수있는 방법이 없기 때문에 레이블을 사용하여 점프 위치를 표시합니다. 아래의 매크로가 어떻게 작동하는지 자세히 설명하지는 않겠습니다. 내가 말한대로 작동한다고 믿으세요 ( 스 니펫 # 10.2 ).
#define PUSH_ADDRESS(labelName) { \
void* tmpPointer; \
__asm{ mov [tmpPointer], offset labelName } \
push(reinterpret_cast<int>(tmpPointer)); \
}
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)
#define CALL_IMPL(funcLabelName, callId) \
PUSH_ADDRESS(LABEL_NAME(callId)); \
goto funcLabelName; \
LABEL_NAME(callId) :
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)
#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 - 1] = stack[BP + 2] * 3;
AX = stack[BP - 1];
SP = BP;
pop(BP);
RET();
my_algo_start:
push(BP);
BP = SP;
SP -= 2;
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;
pop(BP);
}
int main() {
push(AX);
push(22);
push(11);
push(7777);
myAlgo_asm();
assert(myAlgo_withCalls(11, 22) == AX);
SP += 1;
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 대신 바이트에서 작동한다는 것입니다. 따라서 하나의 공간을 예약하기 위해 int
SP는 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
이 게시물을 읽은 후 어셈블리가 이전처럼 비밀스럽지 않습니다. :)
다음은 게시물 본문의 링크와 추가 자료입니다.
- Eli Bendersky , 스택의 상단이 x86에있는 경우 -상단 / 하단, 푸시 / 팝, SP, 스택 프레임, 호출 규칙
- Eli Bendersky , x86-64의 스택 프레임 레이아웃 -x64 , 스택 프레임, 레드 존을 전달하는 인수
- University of Mariland, Understanding the Stack- 스택 개념에 대해 잘 작성된 소개입니다. (MIPS (x86 아님) 및 GAS 구문 용이지만 주제에서는 중요하지 않습니다.) 관심이 있다면 MIPS ISA 프로그래밍 에 대한 다른 참고 사항을 참조하십시오 .
- x86 Asm 위키 북, 범용 레지스터
- x86 디스 어셈블리 위키 북, 스택
- x86 디스 어셈블리 위키 북, 함수 및 스택 프레임
- 인텔 소프트웨어 개발자 매뉴얼 -정말 하드 코어 할 것으로 예상했지만 놀랍게도 읽기 쉽습니다 (정보의 양이 압도적이지만)
- Jonathan de Boyne Pollard, The gen on function perilogues -prologue / epilogue, stack frame / activation record, red zone