작성
·
632
1
제가 디스어셈블리로 함수를 뜯어보는 과정에서 맨처음에 함수로 진입하기전에 스택영역에 각 매개변수를 eax, ecx에 push해서 저장하는것 까지는 확인을 했는데 함수로 들어갔을때 왜 pop을해서 사용하지 않은채로 다른값으로 바꾸는지 이해할수가 없습니다.
int add(int a, int b)
{
001A1EA0 push ebp
001A1EA1 mov ebp,esp
001A1EA3 sub esp,0CCh
001A1EA9 push ebx
001A1EAA push esi
001A1EAB push edi
001A1EAC lea edi,[ebp-0CCh]
001A1EB2 mov ecx,33h
001A1EB7 mov eax,0CCCCCCCCh
001A1EBC rep stos dword ptr es:[edi]
001A1EBE mov ecx,1AF029h
001A1EC3 call @__CheckForDebuggerJustMyCode@4 (01A1384h)
int result = a + b;
001A1EC8 mov eax,dword ptr [a]
001A1ECB add eax,dword ptr [b]
001A1ECE mov dword ptr [result],eax
return result;
001A1ED1 mov eax,dword ptr [result]
}
답변 5
2
비유하자면
struct StackFrame
{
int a;
int b;
...
};
stack<StackFrame> st;
를 사용한다고 했을 때,
StackFrame은 무조건 마지막에 들어간 애를 꺼내서 사용하고는 있지만,
그 마지막 StackFrame의 a, b는 마음대로 사용하는 것과 유사합니다.
스택 프레임 전체를 하나의 요소로 보시면,
LIFO 구조가 지켜지는 것을 알 수 있습니다.
결국 핵심은 함수에 중첩되서 호출될 때,
A 내부에서 B를 호출하고, B 내부에서 C를 호출하면,
A 스택프레임 -> B 스택프레임 -> C 스택프레임 순서로 만들어지고
반드시 C 함수가 return되어 C 스택프레임이 정리 되어야
비로소 B 스택프레임도 정리될 수 있습니다.
그리고 push ebp는 반환주소값(정확히는 다음 실행되어야 할 프로그램 실행 주소 : eip에 들어감)과는 다른 개념입니다.
돌아갈 코드 주소는 함수 호출 (call)을 하는 순간에 스택에 들어가는데,
사실상 call XX이 push eip; jump XX 두개를 동시에 호출하는 셈입니다.
[매개변수][리턴주소][지역변수]에서 [리턴주소]는 엄밀히 말해
[RET][EBP] 두개로 이루어져 있는데
그냥 이전 스택 프레임과 관련된 애들을 한 번에 묶어서 표현한겁니다.
어떤 함수의 호출이 완료되면,
그 함수를 호출하기 바로 직전 위치의 instruction 주소로 돌아가서
이어서 코드가 실행해야 하는데,
그 명령어 주소가 [RET] 위치에 들어가는 값이고,
함수가 끝나서 ret하는 순간 eip 레지스터에 복원됩니다.
반면 push ebp는 말 그대로 기존 스택 프레임의 ebp를 [EBP] 위치에 저장한 것이고
함수 호출이 완료되면 기존 스택 프레임의 내용물을 pop ebp를 통해 복원하게 됩니다.
요약하면
- return address은 이전 함수가 이어서 실행할 코드 주소
- ebp는 이전 함수의 스택 프레임 base pointer
입니다.
1
1
그런데 스택이라는 구조가 Last-IN First-OUT의 방법을 채택하고있다고 알고있는데
그와 상관없이 ebp의 값을 더하고 빼서 원하는 값을 접근할 수 있다면 굳이 스택의 형식을 채택하는 이유는 무엇일까요? 이게 글로 쓰려니까 말을 좀 전달하기가 힘든데 스택이라면 만약에 [매개변수][리턴주소][지역변수] 이렇게 저장했다면 함수가 실행되고 리턴값을 반환하는 과정에서 LIFO방식을 사용하면 정상적으로 반환이 된다고 저로써는 생각하기가 힘든데
그냥 데이터를 저장하는 방식이고 그 데이터를 사용할때는 ebp의 값에서 더하고 빼서 쌓인 순서가 상관없이 접근할수 있다라고 이해하면되나요?
두서없이 질문해서 죄송합니다
1
push를 해서 넣어놨다고, 사용할 때도 꼭 pop을 해서 사용할 필요는 없기 때문입니다.
사실 push도 꼭 사용해야 하는 것은 아니고
스택 주소 계산을 통해 알맞는 위치에 mov를 통해 값을 넣어주고,
esp를 조작해도 똑같은 결과를 얻을 수 있습니다.
위 그림을 보면 push push~를 통해 b, a를 넣어놓은 상태입니다.
그리고 함수 호출 후 스택 프레임이 만들어지면서
현재 함수의 ebp는 0x100를 가리키는 상태라고 가정해봅시다.
넘겨준 b와 a의 값을 사용하고 싶으면,
각각 ebp+0xc, ebp+0x8을 통해 접근해서 알 수 있습니다.
반면 pop이라는 명령어는 현재 esp가 가리키는 위치를 기반으로 내용물을 꺼내기 때문에,
b, a를 pop으로 꺼내오려면 esp를 딱 맞게 조정해야 하는데
번거롭게 굳이 그럴 필요가 없기 때문에 pop을 사용하지 않습니다.
사실 위 어셈블리 코드에서 [a] [b]와 같은 코드는
우리가 보기 편하라고 그렇게 되어 있는 것이지
실제로 바이너리가 만들어진 상태에서 정확한 표현은
[ebp + 0xc]와 같이 ebp에다가 뭔가를 더해서 접근하는 형태로 만들어져 있습니다.
1