인프런 커뮤니티 질문&답변

CULRRY님의 프로필 이미지
CULRRY

작성한 질문수

[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part1: C++ 프로그래밍 입문

스택 프레임

스택프레임에서 저장한 매개변수에 대해서

작성

·

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

Rookiss님의 프로필 이미지
Rookiss
지식공유자

비유하자면

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

CULRRY님의 프로필 이미지
CULRRY
질문자

또 ebp의 값을 push해 주는과정이 반환주소값을 스택프레임에 넣는다고 이해한게 맞게 이해한건지 궁금합니다

1

CULRRY님의 프로필 이미지
CULRRY
질문자

그런데 스택이라는 구조가 Last-IN First-OUT의 방법을 채택하고있다고 알고있는데

그와 상관없이 ebp의 값을 더하고 빼서 원하는 값을 접근할 수 있다면  굳이 스택의 형식을 채택하는 이유는 무엇일까요? 이게 글로 쓰려니까 말을 좀 전달하기가 힘든데 스택이라면 만약에 [매개변수][리턴주소][지역변수] 이렇게 저장했다면 함수가 실행되고 리턴값을 반환하는 과정에서 LIFO방식을 사용하면 정상적으로 반환이 된다고 저로써는 생각하기가 힘든데

그냥 데이터를 저장하는 방식이고 그 데이터를 사용할때는 ebp의 값에서 더하고 빼서 쌓인 순서가 상관없이 접근할수 있다라고 이해하면되나요?

두서없이 질문해서 죄송합니다

1

Rookiss님의 프로필 이미지
Rookiss
지식공유자

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

CULRRY님의 프로필 이미지
CULRRY
질문자

아 eax, ecx는 단지 값을 push하기 위해쓰인 도구라는걸 다시 생각해보니깐 깨달았는데 왜 pop을 이용해서 저장한 변수를 쓰지 않는지는 아직 의문입니다

CULRRY님의 프로필 이미지
CULRRY

작성한 질문수

질문하기