함수의 호출
함수 호출 규약은 cdecl, syscall, optlink, stdcall 등등 많습니다.
규약에 따라 스택에 인자를 넣는 순서라던가 스택 프레임의 정리 방법등이 다릅니다.
아래는 함수가 호출되는 과정입니다. 이 과정을 통해 스택 프레임의 구성에 대해 알아볼 수 있습니다.
스택 프레임
함수가 실행되기위해 스택에 데이터를 올리게 되는데, 각 함수가 사용하는 스택의 영역을 스택 프레임이라고 합니다.
EBP로 스택 프레임을 나눠놓고,
EBP는 하나밖에 없으니 스택 프레임을 새로 만들때마다 스택에 EBP를 백업해 놓습니다.
과정
func(int a, int b);
위 함수가 호출되었을 때를 가정하여 과정을 정리해보겠습니다.
파라미터를 스택에 올린다.
호출된 함수가 매개변수를 받을 수 있도록 스택에 올려줍니다.
cdecl 규약에선 오른쪽의 인자부터 스택에 올립니다.
b가 먼저 스택에 올라간 후, a가 스택에 올라간다는 뜻입니다.함수를 호출한다.
함수가 호출되고, 함수가 끝났는데 다시 돌아와야 할 곳이 저장되어 있지 않다면 다음 실행되어야할 명령을 찾지 못할 것입니다.
그렇기 때문에 함수를 호출하게 되면 일단 스택에 돌아와야할 주소를 넣은 뒤, EIP 레지스터에 올립니다.
전 글에 설명되어있듯이 EIP 레지스터의 값은 후에 함수가 종료되고 나서 돌아와야할 주소가 됩니다.ESP, EBP를 설정한다.
함수가 호출된 시점에서 함수의 프레임 포인터를 스택에 올리고, EBP를 프레임의 최하단으로 설정하고, ESP를 프레임 포인터로 설정합니다. (프레임의 최상단)
이 시점에서 인자 a, b 각각에 (EBP+8), (EBP+12)로 접근할 수 있게되고, (EBP)는 이전 함수의 프레임 포인터, (EBP+4)는 리턴주소가 됩니다.지역변수를 저장하기 위한 공간을 할당한다.
호출된 함수의 지역변수가 저장될 공간을 준비합니다. 단순히 포인터를 움직이는 것으로 완료됩니다.
지역변수가 저장될 공간은 EBP와 ESP 사이에 할당될 것이고 EBP에서 몇바이트씩 빼는것으로 접근이 가능할 것입니다.지역변수를 저장하기 위한 공간을 할당한다.
호출된 함수에서 바로 범용 레지스터를 사용하게 되면 이전 함수에서 범용 레지스터에 저장한 값이 지워지게 될것이고, 그 상태로 원래 함수로 돌아가면 정상적인 실행이 이루어지길 바라는 것은 힘들 것입니다.
그렇기에 이전 함수의 실행내역을 보존하기 위해 범용 레지스터의 값을 스택에 올려놓습니다.함수를 실행한다.
함수의 실행상태를 복구한다.
돌아가야 하기에 과정 5에서 저장한 값을 가져옵니다. Stack은 LIFO의 자료구조입니다.
넣은 순서의 반대로 꺼내진다는 것에 주의해야합니다.스택을 정리하고 프레임 포인터를 되돌린다.
과정 4를 거꾸로 진행한다고 생각할 수 있습니다. 함수로 진입한 시점으로 되돌아갑니다.함수로부터 탈출한다.
스택에 저장된 리턴 주소를 꺼내 EIP에 올립니다. 호출한 함수에서 완전히 빠져나오게 된 것입니다.스택에 올렸던 인자 정리
호출자가 인자를 정리해야하기에 스택 포인터를 8바이트 (int형 인자 2개) 움직여 인자를 꺼내는 처리를 합니다.
이렇게 함수 호출과정이 끝났습니다.
리턴값이 있어야 할 함수였다면 EAX 레지스터에 리턴값이 저장되어있을 것입니다.
프롤로그 & 에필로그
함수 호출 규약, 예외처리에 따라 스택 프레임의 구조는 약간씩 달라진다고 합니다.
위 과정을 기준으로 프롤로그는 과정3~5, 에필로그는 과정7~9를 말합니다.