티스토리 뷰

프로젝트 개발을 다른 사람들과 협업하여 진행하게 되면 자주 듣는 용어가 있는데, 그 중 하나가 코딩 컨벤션(coding convention) 입니다. 코딩 컨벤션이란, 읽고 관리하기 쉬운 코드를 작성하기 위한 일종의 코딩 스타일 규약입니다.

 

느닷없이 갑자기 코딩 컨벤션을 왜 설명하냐고요? 컨벤션이라는 용어 자체가 협업 상황에서 자주 언급되는 단어이지만, 좀 더 저수준(low-level)에서도 쓰이기 때문입니다. 제목을 보고 이미 짐작을 하시리라 생각됩니다.

 

호출 규약(콜 컨벤션, calling convention) 이란, 호출자(caller)와 피호출자(callee) 간의 함수의 인자를 전달하는 방식에 대한 규약을 정의한 것을 의미합니다. 위키피디아에서는 호출 규약을 다음과 같이 정의하고 있습니다 :

컴퓨터 과학에서 호출 규약은 서브루틴이 어떻게 호출자로부터 파라미터를 받고 그 결과를 리턴하는지에 대한 구현-레벨(low-level) 설계(scheme)입니다.

파라미터, 반환값, 반환주소 및 스코프 링크(scope link)가 배치되는 위치(레지스터, 스택 또는 메모리 등) 및 함수 호출을 준비하고 호출 전 환경으로 복구하는 작업을 호출자와 피호출자 간에 어떻게 구분하는지에 따라 다양한 구현이 있을 수 있습니다.

 

 

 

변형(Variation)


호출 규약은 다음의 부분들이 다를 수 있습니다 :

  • 파라미터, 반환값, 반환주소가 배치되는 위치(레지스터, 콜스택, 또는 둘의 믹스 버전, 또는 다른 메모리 구조)
  • 형식 매개 변수에 대한 실제 인수가 전달되는 순서(또는 더 크고 복잡한 인자의 부분에 대한 순서)
  • (충분히 길거나 복잡한)반환값이 어떻게 피호출자에서 호출자로 전달되는지(스택, 레지스터, 또는 힙을 통해서인지)
  • 함수 호출을 위한 셋팅 및 정리와 관련된 업무를 피호출자와 호출자 사이에서 구분하는지
  • 인자를 설명하는 메타데이터의 전달 여부 및 방법
  • 루틴이 종료되었을 때 프레임 포인터를 복구하는데 사용되는 프레임 포인터의 이전값이 저장되는 위치(스택 프레임, 또는 레지스터)
  • 루틴(routine)의 비지역(non-local) 데이터 접근에 대한 정적 스코프 링크가 배치되는 위치(일반적으로 스택 프레임의 하나 이상의 위치에 있지만, 때때로 일반 레지스터(general register), 또는 일부 아키텍처에서의 특수 목적 레지스터)
  • 지역 변수가 어떻게 할당되는지 역시 호출 규약의 일부가 될 수 있습니다(호출자가 피호출자를 위해 할당할 때)

 

몇몇의 경우에서, 다음의 사항들이 차이점에 포함될 수 있습니다 :

  • 보존하지 않으면서, 피호출자가 직접 레지스터를 사용할 수 있는 규약
  • 어느 레지스터가 휘발성(volatile)로 간주되어야 하는지, 만약 휘발성이라면 피호출자에 의해 복원될 필요가 없음(ABI 디테일에서 자주 간주됩니다)

 

일반적으로 호출 규약은 아키텍처마다 다를 수 있으며(예: x86, ARM, x86-64, MIPS 등), 같은 아키텍처 내에서도 다양한 이유로 다양한 호출 규약을 가질 수 있습니다. 게다가 프로그래밍 언어에 따라, 심지어는 컴파일러의 구현마다 함수 호출 규약이 다르게 정의되거나 구현될 수 있습니다.

 

모든 아키텍처에 대한 호출 규약을 이 포스팅에서 설명하는 것은 지나치다고 생각하므로, 관심이 있으신 분들은 위키피디아Architecture variation 섹션을 보시면 되겠습니다.

 

이 포스팅에서는 가장 널리 쓰이는 아키텍처 중 하나인 x86에 대한 널리 알려진함수 호출 규약들만을 살펴보겠습니다.

 

 

 

cdecl


참고 : 본 내용은 마이크로소프트의 공식 문서를 번역하였습니다. 다른 컴파일러에서는 통용되지 않는 내용이 있을 수 있습니다. 좀 더 일반적인 범위에서의 cdecl 함수 호출 규약을 살펴보고 싶다면, 이곳cdecl 섹션을 참조하시기 바랍니다.

 

__cdecl은 C 및 C++ 프로그램에 대한 기본 호출 규약입니다. 스택이 호출자에 의해 지워지기 때문에, 가변 인자를 가지는 함수를 정의할 수 있습니다. __stdcall보다 더 큰 실행 파일을 만들게 되는데, 이는 각 함수 호출마다 스택을 정리하는 코드가 추가되어야 하기 때문입니다.

요소 구현
인수 전달 순서 오른쪽에서 왼쪽
스택 유지 관리 책임 호출하는 함수가 스택에서 인수를 꺼냅니다(정리합니다).
이름 수식 컨벤션(Name-decoration convention) C 연결을 사용하는 __cdecl 함수를 내보낼 때를 제외하고 이름 앞에 _가 붙습니다. (_함수명)
대소문자 변환 규칙 수행되지 않습니다.

 

__cdecl 지정자는 변수 또는 함수 이름의 앞에 지정해야 합니다. 기본적으로 __cdecl 함수 호출 규약을 따르기 때문에, 컴파일러가 vectorcall, fastcall 등의 다른 호출 규약을 디폴트로 지정하였을 때만 해당 지정자를 사용하면 됩니다.

 

ARM 및 x64 프로세서에서, __cdecl 지정자는 허용되지만 일반적으로 컴파일러에 의해 무시됩니다. ARM 및 x64의 컨벤션에 의해, 인자는 가능하다면 레지스터로 전달되고, 후속(subsequent) 인자들은 스택에 전달됩니다. x64 코드에서는, 기본적으로 x64 함수 호출 규약을 따르므로, __cdecl 호출 규약을 사용하기 위해서 /Gv 컴파일러 옵션을 통해 재정의하여야 합니다.

 

또한, 비정적(non-static) 클래스 함수의 경우, 함수의 정의가 클래스 블록 외부에서 정의된 경우, 선언시에만 호출 규약 지정자를 지정하고 함수를 정의하는 곳에서 별도로 함수 호출 규약을 지정할 필요가 없습니다(선언 시의 규약을 따릅니다).

 

예를 들어,

struct CMyClass
{
    void __cdecl Method();
};

void CMyClass::Method()
{
    return;
}

와 같이 사용하면 됩니다.

주의
위 코드 블럭 및 내용은 마이크로소프트의 공식 문서를 번역한 것입니다. 만약, GCC에서 __cdecl 함수 호출 규약을 지정하기 위해서는 다음과 같은 코드를 사용해야 합니다:

void function(int Arg1, int Arg2) __attribute__((cdecl));

 

 

 

stdcall


참고 : 본 내용은 마이크로소프트의 공식 문서를 번역하였습니다. 다른 컴파일러에서는 통용되지 않는 내용이 있을 수 있습니다. 좀 더 일반적인 범위에서의 stdcall 함수 호출 규약을 살펴보고 싶다면, 이곳stdcall 섹션을 참조하시기 바랍니다.

 

__stdcallWin32 API 함수를 호출하는 데에 사용되는 함수 호출 규약입니다. 피호출자가 스택을 정리합니다. 이 함수 호출 규약을 사용하는 함수들은 함수 원형(prototype)을 필요로 합니다.

요소 구현
인수 전달 순서 오른쪽에서 왼쪽
인수 전달 컨벤션(Argument-passing convention) 포인터 또는 참조자 타입이 아닌 경우, 값 전달
스택 유지 관리 책임 피호출자가 스택에서 인수들을 꺼냅니다(정리합니다).
이름 수식 컨벤션 함수명 앞에 _가 붙으며, 함수명 뒤에 인자 리스트의 바이트 크기를 @와 함께 표기합니다(_함수명@인자리스트크기)
대소문자 변환 규칙 없음.

 

ARM 및 x64 프로세서에서, __stdcall은 허용되지만 컴파일러에 의해 무시됩니다; ARM 및 x64의 컨벤션에 의해, 인자는 가능하다면 레지스터로 전달되고, 후속 인자들은 스택에 전달됩니다.

 

또한, 비정적 클래스 함수의 경우, 함수의 정의가 클래스 블록 외부에서 정의된 경우, 선언시에만 호출 규약 지정자를 지정하고 함수를 정의하는 곳에서 별도로 함수 호출 규약을 지정할 필요가 없습니다(선언 시의 규약을 따릅니다).

예를 들어,

struct CMyClass
{
    void __stdcall Method();
};

void CMyClass::Method()
{
    return;
}

주의
위 코드 블럭 및 내용은 마이크로소프트의 공식 문서를 번역한 것입니다. 만약, GCC에서 __cdecl 함수 호출 규약을 지정하기 위해서는 다음과 같은 코드를 사용해야 합니다:

void function(int Arg1, int Arg2) __attribute__((stdcall));

 

 

 

fastcall


참고 : 본 내용은 마이크로소프트의 공식 문서를 번역하였습니다. fastcall 호출 규약은 표준화되지 않았으므로, 일반적인 내용만을 위해서라면 이곳fastcall 섹션을 참조하시기 바랍니다.

 

__fastcall은 가능하다면 레지스터로 전달하는 함수 인자들을 지정합니다. 이 함수 호출 규약은 오직 x86 아키텍처에서만 적용할 수 있습니다. __fastcall 함수 호출 규약의 구현은 다음과 같습니다 :

요소 구현
인수 전달 순서 왼쪽에서 오른쪽으로 진행하는 인자 리스트에서 발견된 첫 두 개의 DWORD 또는 그보다 더 작은 인수가 ECX, EDX 레지스터로 통과합니다; 다른 모든 인수는 오른쪽에서 왼쪽으로 스택에 전달됩니다.
스택 유지 관리 책임 피호출자가 스택에서 인수들을 정리합니다.
이름 수식 컨벤션 함수명 앞에 @가 붙으며, 함수명 뒤에 인자 리스트의 바이트 크기를 @와 함께 표기합니다(@함수명@인자리스트크기)
대소문자 변환 규칙 없음.

 

__fastcall 키워드는 ARM 및 x64 아키텍처를 타겟으로 하는 컴파일어에서는 허용되나 무시됩니다. x64 칩의 컨벤션에 의해, 첫 4개의 인자는 가능하다면 레지스터에 전달되고, 추가적인 인자들은 스택에 전달됩니다. 더 많은 정보는 x64 Calling Convention에서 확인하시기 바랍니다.

 

ARM 칩에서, 4개의 정수 인자와 8개의 부동소수점 인자는 레지스터에 전달될 것이며, 추가적인 인자는 스택에 전달됩니다.

 

또한, 비정적 클래스 함수의 경우, 함수의 정의가 클래스 블록 외부에서 정의된 경우, 선언시에만 호출 규약 지정자를 지정하고 함수를 정의하는 곳에서 별도로 함수 호출 규약을 지정할 필요가 없습니다(선언 시의 규약을 따릅니다).

예를 들어,

struct CMyClass
{
    void __fastcall Method();
};

void CMyClass::Method()
{
    return;
}

주의
위 코드 블럭 및 내용은 마이크로소프트의 공식 문서를 번역한 것입니다. 만약, GCC에서 __cdecl 함수 호출 규약을 지정하기 위해서는 다음과 같은 코드를 사용해야 합니다:

void function(int Arg1, int Arg2) __attribute__((fastcall));

 

 

 

thiscall


참고 : 본 내용은 마이크로소프트의 공식 문서를 번역하였습니다. 다른 컴파일러에서는 통용되지 않는 내용이 있을 수 있습니다. 좀 더 일반적인 범위에서의 stdcall 함수 호출 규약을 살펴보고 싶다면, 이곳thiscall 섹션을 참조하시기 바랍니다.

__thiscall 함수 호출 규약은 가변 인자를 사용하지 않는 C++ 멤버 함수에서 기본적으로 사용됩니다. 이 호출 규약 아래에서 피호출자는 스택을 정리하기 때문에 가변 인자 함수를 생성할 수 없습니다. 인자들은 오른쪽에서 왼쪽으로 스택에 푸쉬되며, 이 때 this 포인터가 x86 아키텍처상에서의 ECX 레지스터에 전달됩니다.

 

__thiscall을 사용하는 이유 중 하나는 멤버 함수가 기본적으로 __clrcall을 사용하는 클래스에 있습니다. 이 경우, __thiscall을 사용하여 각각의 멤버 함수들을 네이티브 코드로부터 호출할 수 있게 합니다.

 

가변 인자를 가지는 멤버 함수는 __cdecl 함수 호출 규약을 사용합니다. 모든 함수 인자들은 스택에 푸쉬되며, this 포인터는 스택의 마지막에 위치하게 됩니다.

 

이 함수 호출 규약이 오직 C++에서만 적용되기 때문에, C 네임 데코레이션 규칙을 따르지 않습니다.

 

ARM 및 x64 머신에서, __thiscall은 허용되지만 컴파일러에 의해 무시됩니다.

 

또한, 비정적 클래스 함수의 경우, 함수의 정의가 클래스 블록 외부에서 정의된 경우, 선언시에만 호출 규약 지정자를 지정하고 함수를 정의하는 곳에서 별도로 함수 호출 규약을 지정할 필요가 없습니다(선언 시의 규약을 따릅니다).

 

 

 

vectorcall


참고 : vectorcall은 Microsoft에서 지정한 독자적인 함수 호출 규약입니다.

 

__vectorcall마이크로소프트에서 게임, 그래픽, 비디오 / 오디오 및 코덱 개발자의 효율성 문제에 대한 응답으로 도입한 함수 호출 규약입니다.

 

__vectorcall 함수 호출 규약은 기본 x64 함수 호출 규약 또는 __fastcall보다 더 많은 레지스터들을 인자를 위해 사용합니다. __vectorcall 함수 호출 규약은 Streaming SIMD Extensions 2(SSE2) 또는 그 이상을 포함하는 x86 및 x64 프로세서에서만 네이티브 코드로 지원합니다.

 

__vectorcall은 몇 개의 부동소수점 또는 SIMD 벡터 인자를 전달하는 함수의 속도를 높이고 레지스터에 로드된 인수를 활용하는 연산을 수행합니다. 다음 리스트는 __vectorcall의 x86 및 x64 구현에 대한 일반적인 특징들을 보여줍니다 :

요소 구현
C 이름 수식 컨벤션 함수명 뒤에 @@와 인자 리스트의 크기를 함께 표기합니다(함수명@@인자리스트크기)
대소문자 변환 컨벤션 없음

 

__vectorcall 함수 호출 규약을 사용하는 함수에 레지스터로 3가지 종류의 인자를 전달할 수 있습니다 : 정수 타입 값, 벡터 타입 값, 동차 벡터 집합(Homogeneous vector aggregate, HVA) 값.

 

정수 타입은 두 가지 요구사항을 만족합니다 : 타입이 프로세서의 네이티브 레지스터 크기에 딱 들어맞아야 합니다, x86에서는 4바이트 x64에서는 8바이트가 됩니다. 또한, 비트 표현을 변경하지 않고 레지스터 길이의 정수로 변환하고 되돌릴 수 있습니다.

 

예를 들어, int(x64에서는 long long)로 캐스팅될 수 있는 어떤 타입들 모두 원래 타입으로 되돌리는 데 비트 변환이 필요하지 않음을 의미합니다.정수형 타입은 4바이트 크기(x64에서는 8바이트) 또는 그 이하의 포인터, 참조자, 구조체 및 union 타입을 포함합니다. x64 플랫폼에서 큰 구조체나 union 타입은 호출자에 의해 메모리에 레퍼런스로 할당됩니다; x86 플랫폼에서는 스택으로 값 전달됩니다.

 

벡터 타입은 float, double 등의 부동소수점 타입이거나 __m128, __m256의 SIMD 벡터 타입입니다.

 

HVA 타입은 동일한 벡터 타입들을 가지는 최대 4개의 데이터 멤버의 복합 유형입니다. HVA 타입은 벡터 타입과 같은 정렬(alignment) 제약사항을 가집니다. HVA 구조체 정의의 한 예는 다음과 같습니다(32-BYTE 정렬을 가집니다) :

typedef struct
{
    __m256 x;
    __m256 y;
    __m256 z;
} hva3;    // 3 element HVA type on __m256

 

__vectorcall 지정자를 사용하여 멤버 함수를 선언할 수 있습니다. 숨겨진 this 포인터는 첫 번째 정수 타입 인자로써 레지스터에 전달됩니다.

 

ARM 머신에서, __vectorcall은 허용되지만 컴파일러에 의해 무시됩니다.

 

또한, 비정적 클래스 함수의 경우, 함수의 정의가 클래스 블록 외부에서 정의된 경우, 선언시에만 호출 규약 지정자를 지정하고 함수를 정의하는 곳에서 별도로 함수 호출 규약을 지정할 필요가 없습니다(선언 시의 규약을 따릅니다).

예를 들어,

struct CMyClass
{
    void __vectorcall Method();
};

void CMyClass::Method()
{
    return;
}

 

또한, __vectorcall 함수 호출 규약 지정자는 __vectorcall 함수를 가리키는 포인터가 생성될 때 반드시 지정되어야 합니다. 다음의 예를 통해 살펴보겠습니다 :

typedef __m256 (__vectorcall * vcfnptr)(double, double, double, double);

 

__vectorcall 함수 호출 규약의 x64에 대한 설명은 다음 링크에서 자세히 살펴보시기 바랍니다.

 

 

 

기타 호출 규약들...


이 이외에도 syscall, pascal, naked 등 다양한 함수 호출 규약들이 존재하지만, 모든 것을 다루는 것은 포스팅이 과도하게 길어진다고 판단하여 더이상 적지 않겠습니다. 궁금하신 분들은 관련 링크들을 통해 참조하시거나 검색하시는 것을 추천드리겠습니다.

 

 

 

레퍼런스


 

참고자료
Wikipedia - Calling convention
Wikipedia - x86 Calling convention
Microsoft | Docs - __cdecl
agner.org - Calling conventions for different C++ compilers and operationg systems
soen.kr - 호출 규약

'이론 > Computer' 카테고리의 다른 글

2의 보수(two's complement)  (0) 2019.11.03
메모리 얼라인먼트(Memory Alignment)  (5) 2019.06.26
댓글