Stackframe, Calling Convention
Stack Frame
코드
위 코드를 64bit x86-64 환경인 우분투에서 gcc -m32 -fno-stack-protector -fno-pic -g -o ex ex.cpp
로 빌드함.
그리고 gbd 를 사용해 어셈블리를 분석하면 아래와 같음
Caller()
push ebp // prolog
mov ebp, esp // prolog, update ebp
sub esp, 0x28 // make stack space
mov ecx, 0x0
mov eax, 0x20
and eax, 0xfffffffc
mov edx, eax
mov eax, 0x0
mov DWORD PTR[ebp + eax * 1 - 0x28], ecx
add eax, 0x4
cmp eax, edx
jb 0x8048461 < Caller + 26 >
mov DWORD PTR[ebp - 0x28], 0x1 // a[0] = 1
mov DWORD PTR[ebp - 0x24], 0x2 // a[1] = 2
mov DWORD PTR[ebp - 0x20], 0x3 // a[2] = 3
mov ecx, DWORD PTR[ebp - 0x20]
mov edx, DWORD PTR[ebp - 0x24]
mov eax, DWORD PTR[ebp - 0x28]
sub esp, 0x4 // not relevent with parameter
push ecx // parameter push
push edx // parameter push
push eax // parameter push
call 0x8048426 < Callee > // funcion call
add esp, 0x10 // parameter clear (0x4 + 4*3)
mov DWORD PTR[ebp - 0x1c], eax // a[4] = return
nop
leave // epilog
ret // epilog
Callee()
push ebp // prolog
mov ebp, esp // prolog, update ebp
sub esp, 0x8
push DWORD PTR[ebp + 0x10] // parameter a push
push DWORD PTR[ebp + 0xc] // parameter b push
push DWORD PTR[ebp + 0x8] // parameter c push
push 0x650 // parameter loc of "%d %d %d" push
call 0x542 < Callee + 21 > // Function Call
add esp, 0x10 // Clear Stack for Parameters
mov eax, DWORD PTR[ebp + 0x8] // return a;
leave // epilog
ret // epilog
Wiki 에서 자세한 어셈블리 분석을 볼 수 있음.
개념
함수 호출 시 스택 영역에 저장되는 함수의 호출 정보를 Stack Frame 이라고 함
- Caller / Callee
Func()
함수를Main()
에서 호출하고 있음.- 이때
Main()
을 Caller 라고 부르고Func()
을 Callee 라고 함.
- Littile Endian 주의
- 헷갈리기 쉬운데 Little Endian 참고
- 지금은 x86-64 를 쓰므로 Little Endian 임
- Stack 이 주소가 작아질수록 스택이 쌓이게 됨
- =>
sub
가 스택을 쌓는거고add
가 스택을 빼는 것임
- 기본적으로 2개의 값이 Stack 에 저장됨.
- IP(
rip
/eip
)call
명령어 시push eip
가 수행된 후 Callee 로 점프함.- Callee 가 끝날 때의
ret
명령시pop eip
과jmp eip
가 수행됨
- SFP(Stack Frame Pointer)
- Caller 의 StackFrame 시작주소 혹은 Caller 의
ebp
를 말함. - StackFrame 생성마다 스택에 저장되고 끝나면 다시 꺼냄.
- Caller 의 StackFrame 시작주소 혹은 Caller 의
- Buffer Overflow 등으로 위 값들을 변경하면 의도치 않은 작동을 가능하게 함.
- IP(
- Top of StackFrame 은 레지스터에서 계속 업데이트가 됨
esp
/rsp
가 StackFrame 의 Top 을 가르킴.- Callee 의 SFP 가 Caller 의 Top of StackFrame 이라 스택에 저장할 필요는 없음
- 함수 내부의
{ ... }
같은 __Block Scope 는 스택과 비슷한 역할을 하지만 스택과 관련이 없음. 관련질문
이하는 함수를 호출할 때 일어나는 순서대로 구분한 것임.
Call
Parameter 를 Register 또는 Stack 에 집어 넣고 call [주소]
가 수행됨.
크게 2가지 동작을 함
push eip
jmp [주소]
Prolog
함수 앞부분에 나오는 push ebp
와 move ebp, esp
구문임.
push ebp
를 하면ebp
가 스택에 저장되고esp
가 포인터 크기만큼 감소함move ebp, esp
를 하면 Caller 에서의 Top 인esp
가 Callee 의 돌아갈 주소가 될ebp
에 저장됨.
Local Variable
함수 시작부분에 sub esp, 0x28
의 구문을 통해 스택공간을 확보함
- 컴파일러 옵션에 따른 더미공간 + Local Variable Size 만큼 공간이 확보됨
- Caller 를 보면
int a[8]
덕분에4*8 = 0x20
만큼이 필요함 - 그럼 나머지
0x08
은 더미임- Local Variable 이 없는 Callee 를 보면
sub esp, 0x8
을 확인 가능
- Local Variable 이 없는 Callee 를 보면
Epilog
함수 끝부분에 나오는 leave
와 ret
를 말함
leave
는 두가지 명령어의 축약으로 Prolog 의 정확한 역임mov esp, ebp
esp
를 조작하지 않았다면 생략되기도 함.
pop ebp
ret
는 CALL 명령어에 의해 호출된 Callee 에서 Caller 로 복귀하는데 사용됨.pop eip
jmp eip
Calling Convention
코드
Caller()
~
sub esp,0x4 // not relevent with parameter
push ecx
push edx
push eax
call 0x8048426 <Callee(int, int, int)>
add esp,0x4 // not relevent with parameter
~
Callee()
~
leave
ret 0xc // Clear stack for parameters
위는 원래 코드에서 호출규약만 stdcall
로 바꾼 걸 분석한 것임.
개념
함수가 Parameter 등을 어떻게 전달하고 원상태로 돌리는지에 관한 규약
- parameters, return values, return addresses and scope links 등의 정보가
- registers, stack or memory etc. 같은 장소에 저장되어
- 함수 호출을 준비하고 호출 전 환경으로 복귀하는 방법
기본적으로 x64 에서는 적용되지 않고 x86 에서만 적용됨
- x64 기본적으로 Register 를 쓴 후 미리 확보해놓은 Stack 에 나머지를 넣는 방식을 사용함.
- 즉 이하에서 말하는 내용은 x64 에서는 적용안되며,
vectorcall
등이 적용가능함(Arm 제외)
호출규약은 엄청 많은데 스택정리가 어떻게 되는지 위주로 여기서 살펴보려함
자세한 차이는 위키 번역한 블로그 나 위키 참고.
cdecl
-
C Declaration 의 약자로 x86 환경에서 기본적으로 사용됨
- 인수는 오른쪽에서 왼쪽 순서대로 들어감.
- 레지스터에 넣는경우(fastcall 등) 를 빼면 거의 이 순서임.
- 레지스터에 넣는경우(fastcall 등) 를 빼면 거의 이 순서임.
- 스택정리를 Caller 가 함
- Caller 가
push
를 통해 변수를 Stack 에 넣고 - Caller 가
add esp, 0x10
을 통해 파라미터를 넣은 공간을 비움 - 스택을 정리하는 코드가 호출때마다 있어서 용량이 약간 큼
- Caller 가
stdcall
- ARM 이 아닌 x86 에서만 가능 하고 나머지에선 씹힘
- WinApi, DirectX Api 등 MS 제공 API 에서 주로 사용됨
- 멤버함수에
stdcall
를 쓰기도 하는데 자세한건thiscall
참고
- 멤버함수에
-
인수는 오른쪽에서 왼쪽 순서대로 들어감
- 스택정리를 Callee 가 함
- Caller 가
push
를 통해서 변수를 Stack 에 넣고 - Callee 가
ret 0xc
를 통해서 스택을 정리함- 인자가 있어서
ret
에 추가로add esp, 0xc
를 하는 것임.
- 인자가 있어서
- 그래서 가변인자를 사용할 수 없음
- Caller 가
thiscall
ARM 이 아닌 x86 의 비정적 멤버함수 에서만 기본으로 적용되어 있음.
- 즉 c++ 만 가능하며
- x64, ARM 환경에서는 무시되고
- 따로 안적으면 적용됨
어셈블리는 아래처럼 나타나며, 위는 적용안된 Ubuntu, 아래는 적용된 MSVN 환경임.
lea eax, this-pointer
push eax
call memberfunc
lea ecx, this-pointer
call memberfunc
위처럼 함수호출 전에 this
포인터를 레지스터에 넣는다는 것이 핵심임.
- 어떤 레지스터인지, 인수정리는 누가 하는지는 컴파일러마다 다름
- VS 에서 실험한 결과는 Callee 가 정리하고 으로 쓰면 Caller 가 정리함
다음과 같은 경우는 thiscall
이 적용이 안됨
- x64, ARM 환경이거나
...
를 붙이거나cdecl
,stdcall
를 명시적으로 쓰는 경우
그런경우 위의 어셈블러가 보여주듯 this
를 레지스터가 아니라 Stack 에 넣고 , 인수정리는 각 호출규약이 하듯 처리함.
- DirectX 등을 보면 이런 경우가 많음
댓글남기기