함수 호출 규약

J·2025년 12월 19일

테스트

목록 보기
11/12

함수 호출 규약

함수가 호출되고 반환되는 일련의 과정에서 정형화된 부분을 의미

  • 함수 호출 시 매개변수의 전달을 어떻게 할 것인지
  • 함수의 반환값은 어디에 저장해서 전달할 것인지 (eax)
  • 함수 반환 이유 남은 스택의 정리를 어떻게 할 것인지? (전달된 매개변수의 정리를 호출자? 피호출자? 누가 할 것인가?

x86

thiscall

  • 클래스 멤버 함수 호출 시 객체의 포인터는 ecx 레지스터를 통해 전달
  • 매개 변수 전달 순서 (오른쪽 -> 왼쪽 push)
  • 스택 정리 주체
    • 가변 인자 존재 o -> 호출자(caller)가 스택 정리
    • 가변 인자 존재 x -> 피호출자(callee)가 스택 정리
class MyClass
{
public:
//	void Go(int a) {m_iV = a;}

	void Go(int a, ...) { m_iV = a; }
	
	int m_iV;
};
int main()
{
	
	MyClass myClass;
	myClass.m_iV = 3;
	
	myClass.Go(5);

}
  • 가변 인자 존재 x

  • 가변 인자 존재 o

cdecl

  • 기본 호출 규약
  • 매개변수의 전달 순서 (오른쪽 -> 왼쪽 push)
  • 스택 정리 주체
    • 호출자 (Caller)
  • cdecl 호출 규약은 가변 인자 함수를 가능하게 할 수 있음 ( 호출자가 호출된 함수를 정리하기 때문에 )
int Sum(int a, int b,...)
{
	return a + b;
}
int main()
{
	int c = Sum(3, 4);

}

함수 종료 후 main에서 add esp 8 을 통해 stack pointer를 내려주는 것을 확인.

stdcall

  • 매개 변수 전달 순서 (오른쪽 -> 왼쪽 push)
  • 스택 정리 주체
    • 피호출자 (Callee)
  • 반복문이 아닌 코드에서 함수 호출이 많이 발생했을 때 cdecl같은 경우는 함수 호출 후 스택을 정리하는 코드가 중복됨, stdcall은 이러한 코드의 중복을 줄일 수 있어서 과거 (바이너리 크기에 민감하던 시절)에는 나름 장점이었음.
int __stdcall Sum(int a, int b)
{
	return a + b;
}
int main()
{
	int c = Sum(3, 4);

}

함수 종료 시 Sum 함수 내부에서 ret 8 을 통해 stack pointer를 내려주는 것을 확인.

fastcall

  • 매개변수의 전달 순서 (오른쪽 -> 왼쪽 push)

  • 정수의 경우, 기본적으로 2개의 매개변수는 레지스터를 통해 전달.

    • ecx, edx 레지스터를활용
  • 부동 소수점의 경우 무조건 push

  • 스택 정리 주체

    • 피호출자 (Callee)
int __fastcall Sum(int a, int b)
{
	return a + b;
}

int __fastcall Sum(int a, int b, int c)
{
	return a + b + c;
}

LONGLONG __fastcall Sum64(LONGLONG a)
{
	return a+3;
}

LONGLONG __fastcall Sum64(LONGLONG a, LONGLONG b)
{
	return a + b;
}
double __fastcall Sum(double a, double b)
{
	return a + b;
}

double __fastcall Sum(double a, double b, double c)
{
	return a + b + c;
}
int main()
{
	int fastcall1 = Sum(1, 2);
	int fastcall2 = Sum(1, 2, 3);

	double fastcall3 = Sum(1.0, 2.0);
	double fastcall4 = Sum(1.0, 2.0, 3.0);

	LONGLONG fastcall5 = Sum64(0x1111111122222222);
	LONGLONG fastcall6 = Sum64(0x1111111122222222,0x2222222233333333);
}
  • 정수 (4바이트)

    Sum(a,b)의 경우는 ecx, edx 레지스터에 담겨서 전달되고, push하지 않기때문에 바로 ret

    Sum(a, b, c)의 경우 인자가 3개이기 때문에 2개는 레지스터에, 한 개는 push한 후 호출된 함수에서 스택을 정리하는 것을 확인.


  • 부동소수점 (float - 4 / double - 8)

매개변수의 개수와 상관없이 바로 스택에 푸쉬한 후, 호출된 함수에서 스택을 정리하는 것을 확인. 한 가지 특이한 것은 정수와는 다르게 값을 바로 push하지 않고 레지스터에 담은 후 그 값을 push하는 것을 볼 수 있다. 내 생각엔 아마 부동 소수점 데이터를 바로 못 담아서 레지스터를 활용하는 것 같은데.. 이것도 나중에 봤을 때 기억나도록 적어놓도록 한다..

  • 8바이트

    다음과 같은 형태로 push된다.

x64

  • 호출 규약이 통합됨 ( 호출 규약 명시해도 무시됨 )

  • 매개변수의 전달 순서 (오른쪽 -> 왼쪽 push)

  • 스택 정리의 주체 (호출자, Caller)

  • 정수의 경우 rcx / rdx / r8 / r9 레지스터를 통해 전달

  • 부동소수점의 경우 xmm0 / xmm1 / xmm2 / xmm3 레지스터를 통해 전달

  • 클래스의 멤버 함수 호출 시 rcx를 통해 this 포인터 전달

  • 값의 리턴은 rax (정수) / xmm0 (부동소수점) 레지스터를 활용

  • x86 호출 규약과 비교했을 때

    • x64에선 base pointer가 없이 rsp를 통해 데이터를 접근한다.
      - VC++ 의 경우에는 함수 호출 시 레지스터를 이용하여 전달되는 4개의 인자가 스택에 저장될 수 있도록 무조건 4개 인자를 위한 공간을 확보한다. 복사되는 이유 : 함수 내에서 다른 함수가 호출된다면 기존의 인자들은 사라질 것이기 때문에 따로 스택 내부에 보관을 해둔다.
    • 함수 호출을 하는 경우 무조건 호출자가 rsp를 통해 4개 인자를 위한 공간을 확보한 후 사용

예시) 기본적인 함수 호출

매개변수가 레지스터만으로 부족한 경우는 매개변수를 저장할 공간을 늘리고, 기본적인 동작은 위와 비슷하다.

int Plus(int a, int b)
{
	return a + b;
}

int main()
{
	int a = Plus(1, 2);
}

기본적인 형태는 다음과 같다.

각 단계 직후의 상황을 a,b,c,d,e,f 로 표현했으며 그 순간의 스레드 스택은 이와 같다.

초기

  • 아직 stack pointer를 늘리기 전이다.


a

  • stack pointer의 크기만 변화하였다.


b

  • 늘린 stack pointer + call로 최상단엔 call이 리턴하면 실행될 명령어의 주소가 담겨있다.


c

  • ecx, edx 레지스터를 통해 전달된 1,2 값이 스택 내부에 복사되었다.

d

  • Plus(int, int) 함수가 종료되며 ret을 통해 최상단의 ret주소가 pop된 상태이다.

e

  • Plus(int, int)가 종료되기 전 계산된 리턴값을 eax 레지스터에 저장하였고, 그 값을 해당 메모리 지점에 복사한다.

f

  • main 함수가 종료하기 전, 시작할때 늘려놨던 길이만큼 축소시킨다. 시작될 때와 동일하다.
profile
낙서장

0개의 댓글