스텍 프레임은 함수마다 가지는 별도의 스택 공간이다.
메모리 스택은 높은 주소에서 낮은 주소로 내려가는 스택 구조를 지닌다.
아래 스택 프레임의 구조를 알아보자.
EBP (Base Pointer) : 현재 스택의 가장 바닥(처음주소)을 가리키고 있는 포인터이다.
새로운 함수가 호출될 시 EBP가 푸시되어 이전 EBP 값을 저장해놓는다.
(EBP는 호출되기 전의 값을 스택에 넣어 반환 시 호출되기 직전(caller)으로 이동한다.)
ESP (Stack Pointer) : 현재 스택의 가장 위(최신주소)에 들어있는 데이터를 가리키는 포인터다.
※ ESP는 Push, Pop을 할 때 기준이 되는 포인터다. (ESP를 기준으로 Push/pop 됨)
데이터가 추가될 때마다 ESP가 내려가며(x86) 함수의 경계를 가리키게 된다.
SFP (Stack Frame Pointer) : 함수 에필로그에서 EBP가 Push할 때 이전 EBP 값을 저장해놓는 장소다. (caller 주소가 저장된다.)
EIP (Instruction Pointer) : 다음에 실행할 명령어의 주소를 가지고 있는 PC다.
현재 실행하고 있는 명령어가 종료되면 EIP에 있는 주소를 불러오게 된다.
RET : 함수 호출 다음에 실행할 코드의 주소를 담고 있는 값이다.
RET는 popeip, jmpeip로 구성되며 에필로그에서 EIP 값으로 전달되고 다음 코드를 실행한다.
func() 함수 스택 프레임 :
에필로그 {
PUSH EIP - 다음에 실행할 코드를 스택에 미리 저장한다.
PUSH EBP - 함수를 호출하고(스택 프레임 생성), EBP를 사용하기 전에 기존 값을 스택에 저장한다.
MOV EBP, ESP - 현재 ESP 값(push 후 주소)을 EBP에 옮긴다. EBP==ESP
}
′′′
SUB ESP, 8 - 8만큼 ESP를 내린다. (2개의 지역변수만큼)
MOV DWORD [EBP-4], 1
MOV DWORD [EBP-2], 2 // 변수 2개에 각각 1과 2를 대입한다. (값 할당)
MOV EAX, [value] // 리턴 값이 있을 경우 EAX에 저장된다.
′′′
프롤로그 {
MOV ESP, EBP - 내려간 ESP를 다시 EBP가 있는 곳으로 올려줘서 변수 공간을 무효화한다.
POP EBP - pop한 값을 EBP에 전달하여 EBP가 호출 전(caller) 주소에 복원되도록 한다.
RETN - RET 명령어는 두 개의 명령어로 구성된다.
POP EIP : pop한 값을 EIP에 저장하여 다음에 실행될 코드 주소를 저장한다.
JMP EIP : 새로 저장된 주소로 EIP가 점프하여 다음 코드를 실행하게 한다.
}
′′′ main() :
ADD ESP, 8 // _cdcel 규약에 의해 스택 정리를 한다. (함수 호출에 사용한 인수를 정리함)
프롤로그 : 함수 내에서 사용할 스택 프레임을 설정하는 구간
에필로그 : 함수 처리를 완료하고 처음 호출한 지점으로 돌아가기 위해 스택을 복원하는 구간
함수 호출 규약이란 함수가 호출될 때 인자의 전달방법(스택 or 레지스터), 전달 순서(오른쪽 -> 왼쪽 or 왼쪽 -> 오른쪽), 인자 전달에 이용된 스택의 해제 위치(호출한 함수 or 호출된 함수)를 정해놓은 규약이다.
스택 정리는 함수를 실행하고 반환할 때 인자를 정리하는데 이 때 호출 규약 별로 caller/callee에서 인자를 정리한다.
visual studio는 _cdcel 규약을 사용하여 함수를 호출할 쪽(caller)에서 스택을 정리한다.
장점으론 caller가 전달되는 인자의 개수를 알고 있기 때문에 가변 인자를 사용할 수 있다.
반면 _stdcall은 callee에서 인자를 정리하는데 callee의 RET 문에서 정리한다.
_fastcall은 처음 2개의 인자는 ecx, edx에 넣되 나머지 인자부턴 스택에 넣는다. (빠르다)
담당이 callee 일 경우 스택 정리 과정 :
RETN 8 // RETN 매개로 8을 넣었는데 이는 추가로 다음과 같은 행동을 한다.
add esp, 8 // func() 인자 2개치 스택 포인터를 올려준다. (스택 정리)
main()에서 인자를 2개 받는 func(int , int) 함수를 호출한다고 가정할 때 스택 프레임에서
어떻게 callee의 매개변수에 인수를 전달하는지 그 과정을 알아본다.
main() :
push b // 인수 a,b를 push로 생성한다.
push a // <- ESP location
call func00040167 // func(int f, int g) 함수를 호출
add esp, 8 // 함수가 종료되면 생성한 인수 a,b를 정리한다.
func(a,b) :
′′′ 에필로그 생략 ′′′
SUB ESP, 8 // 매개변수 수만큼 다시 push
mov EAX, DWORD [EBP+8] // callee의 아규먼트 [ebp+8]를
mov DWORD [EBP-8], EAX // caller의 매개변수 [ebp-8]에 전달
mov ECX, DWORD [EBP+C] // [ebp+c]를
mov DWORD [EBP-4], ECX // [ebp-4]에 전달
※ 매개변수와 인수는 push를 통해 구현된다.
실제 인수의 전달은 스택 프레임을 넘나들어 caller의 스택 프레임에 접근해 [ebp+8]의 값을
callee의 매개변수인 [ebp-8]에 복사하는 방식으로 이뤄진다.
[ebp+8]은 caller 인수 a를 가리키고, [ebp-8]은 callee 매개변수 f를 뜻한다.
그리고 _cdcel은 caller에서 인자를 정리하기에 함수가 종료되면 caller에서 ESP 값을 올려준다.
add esp, 8 // 인수 스택 정리
main() :
// func(7,32); =
PUSH 32
PUSH 7 // 인수로 사용할 스택 확보 (7, 32)
call func00040167 // 함수 호출
ADD esp, 8 // caller에서 인자 정리
′′′
func(int x, int y) :
====== 에필로그 ======
PUSH EIP // EIP는 PC라서 call 명령 다음번에 실행될 ADD esp, 8의 주소를 가리키고 있다.
해당 주소를 저장한다.
PUSH EBP // ←ESP 현위치
MOVE EBP, ESP // EBP는 ESP와 같은 값이 된다.
====== 인수 전달 =====
SUB ESP, 8 // 인자용 스택 확보
mov EAX, DWORD [EBP+8] // callee의 아규먼트 a를 ([ebp+8] = "←ESP"에서 두칸위 = "push 7")
mov DWORD [EBP-8], EAX // caller의 매개변수 x에 전달
mov ECX, DWORD [EBP+C] // args b를
mov DWORD [EBP-4], ECX // y에 전달 ([ebp+c] = "push 32")
// = func(int x=7, int y=32)
====== 함수 내용 =====
′′′
MOV EAX, [value] // 리턴 값 반환
====== 프롤로그 ======
MOV ESP, EBP // 지역변수(매개변수) 정리
POP EBP // EBP는 다시 main 스택 프레임에 위치
RETN // EIP가 다음에 실행할 코드로 점프하여 ADD esp, 8 명령을 실행
pop eip // 에필로그에서 push한 기존 eip 값을 받아온다.
jmp eip // 받아온 eip 값은 함수 호출 후 실행될 main()의 다음 주소(코드)이므로
자연스레 다음 명령어(ADD esp, 8)가 실행된다.