#include <stdio.h> long add(long a, long b) { long x=a, y=b; return (x + y); } int main(int argc, char* argvp[]) { long a=1, b=2; printf("%d\n", add(a,b)); return 0; }
1. main() 함수 시작 – 스택 프레임 생성
코드
int main(int argc, char* argv[]) {
어셈블러
(초기 상태)
그림

설명
ebp를 저장하는 이유는 이전에 수행하던 함수의 데이터를 보존하기 위해서이다.
이것을 base pointer라고도 부르는데,
그래서 함수가 시작될 때는 이렇게 stack pointer와 base pointer를 새로 지정하며
이러한 과정을 “함수 프롤로그 과정“이라고 한다.
어셈블러
push ebp
그림

설명
‘PUSH’는 값을 스택에 집어넣는 명령이다.
따라서 “EBP 값을 스택에 집어 넣어라”라는 의미이다.
이렇게 main() 함수에서 EBP가 베이스 포인터의 역할을 할테니,
EBP가 이전에 가지고 있던 값을 스택에 백업해두기 위한 용도로 사용된다.
(나중에 main() 함수가 종료(리턴)되기 전에 이 값을 회복시켜준다.
이전 함수의 base pointer를 저장하면,
stack pointer는 4 byte 아래인 0x12ff40을 가리키게 된다.
어셈블러
mov ebp, esp
그림

설명
ESP의 값을 EBP에 복사한다.
이 명령 이후부터 EBP는 현재 ESP와 같은 값을 가지게 된다.
그리고 main() 함수가 끝날 때까지 EBP 값은 고정된다.
스택에 저장된 함수 파라미터와 로컬 변수들은 EBP를 통해 접근하기 위해 고정된 것이며,
두 명령어에 의해 main() 함수에 대한 스택 프레임이 생성되었다.
2. 로컬 변수 세팅
코드
long a=1, b=2;
어셈블러
sub esp, 8
그림

설명
ESP에서 8을 빼는 명령이다.
main() 함수의 로컬 변수 ‘a’, ‘b’는 long 타입이기에 각각 4byte 크기를 가진다.
따라서 이 두 변수들을 스택에 저장하려면 총 8byte가 필요하므로
8byte 만큼의 공간을 확장시켰다.
어셈블리
mov [ebp-4], 1 mov [ebp-8], 2
그림

설명
로컬 변수 a를 의미하는 [ebp-4]에는 1을 넣고,
로컬 변수 b를 의미하는 [ebp-8]에는 2를 넣는다.
3. add() 함수 파라미터 입력 / add 함수 호출
코드
printf("%d\n", add(a,b));
어셈블러
mov eax, [ebp-8] push eax mov ecx, [ebp-4] push ecx call 0x401000 // add()
그림

설명
변수 a, b의 값을 스택에 넣는데,
이 때 함수 파라미터의 역순 방식으로 저장시킨다.
call 명령은 함수를 호출할 때 사용되는 명령으로,
해당 함수로 들어가기 전에 해당 함수가 종료될 때 복귀할 주소(add() return address)를 스택에 저장한다.
그런 다음, EIP에 add 함수의 시작지점 주소인 0x401000를 넣는다.
4. add() 함수 시작 / 스택 프레임 생성
코드
long add (long a, long b) {
어셈블러
push ebp mov ebp, esp
그림

설명
이제 add() 함수가 시작되면, 자신만의 스택 프레임을 생성한다.
원래의 EBP 값(main() base pointer)을 스택에 저장시키고,
현재의 EBP 값(Stack Pointer)를 EBP에 복사한다.
add() 함수 내에서 EBP 값은 고정된다.
5. add() 함수의 로컬 변수 (x, y) 세팅
코드
long x=a, y=b;
어셈블러
sub esp, 8 mov eax, [ebp + 8] mov [ebp-8], eax mov ecx, [ebp + c] mov [ebp-4], ecx
그림

설명
8byte 만큼의 공간을 스택에 확장시킨다.
[ebp+8] 값인 1을 eax에, eax값인 1을 다시 [ebp-8]에 복사한다.
마찬가지로, [ebp+c]값인 2를 ecx에, ecx 값인 2를 다시 [ebp-4]에 복사한다.
6. ADD 연산
코드
...(x+y); }
어셈블러
mov eax, [ebp-8] add eax, [ebp-4]
그림

설명
[ebp-8]값인 1을 eax에 복사하고,
eax+[ebp-4](=1+2) 계산값인 3을 eax에 저장한다.
7. add() 함수의 스택 프레임 해제 / 함수 종료(리턴)
코드
return (x+y); }
어셈블러
mov esp, ebp
그림

설명
EBP 값인 0x12ff28을 ESP로 복사한다.
따라서 이전 명령어인 SUB ESP, 8 명령 효과가 사라져
스택에 쌓여있던 2, 1은 더 이상 유효하지 않게 된다.
어셈블러
pop ebp
그림

설명
스택에 백업해둔 ebp 값을 복원한다.
복원된 ebp 값은 0x12ff40이며,
이 값은 main() 함수의 ebp 값이다.
pop을 했으므로 stack pointer는 1 word 위로 올라가며,
이제 stack pointer는 return address를 가리킨다.
이로써, add() 함수의 스택프레임은 해제된다.
어셈블러
retn
그림

설명
스택에 저장된 복귀주소로 리턴한다.
return address는 POP 되어 EIP에 저장되고,
stack pointer는 한번더 1 word 위로 올라간다.
8. add() 함수 파라미터 제거 (스택 정리)
어셈블러
add esp, 8
그림

설명
‘ADD’ 명령으로 ESP에 8을 더하여, 스택을 8byte 줄인다.
add() 함수가 완전히 종료되었기 때문에
파라미터로 쓰이는 2, 1은 더이상 필요없어 스택을 정리한다.
9. printf() 함수 호출
코드
printf("%d\n", add(a, b));
어셈블러
push eax push 0x40b384 //"%d\n" push 0x401067 //printf() add esp, 8
그림

설명
printf() 함수에 사용된 파리머터의 개수가 2개이며, 8바이트가 된다.
(32비트 레지스터+32비트 상수=64비트=8바이트)
호출 후, 마찬가지로 ‘ADD’ 명령으로 파라미터를 정리한다.
10. 리턴 값 세팅 / 스택 프레임 해제 / main() 종료
코드
return 0; }
어셈블러
xor eax, eax mov esp, ebp pop ebp retn
그림

설명
main() 함수의 리턴 값인 0을 세팅하기 위해 ‘XOR’ 명령어를 사용한다.
같은 값끼리 XOR하면 0이 되는 특징이 있는데, “MOV EAX, 0″보다 실행 속도가 빨라
위와 같이 레지스터를 초기화할 때 많이 사용된다.
main() 함수의 스택 프레임을 해제시키는데
stack pointer는 mov 명령에 의해 2word 위로,
pop 명령에 의해 1word 위로, 즉 3word위로 올라간다.
마지막으로, 스택에 저장된 복귀주소로 리턴되어
ret address는 POP되어 EIP에 저장되고,
stack pointer는 한번 더 1word 위로 올라간다.