메뉴 건너뛰기


Developer > Application

C,C++ calling convention (__stdcall / __cdecl)

2013.11.25 16:43

푸우 조회 수:13120

http://cafe.naver.com/set2happy.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=350 의 글을 옮겨 왔습니다.


출처 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

출처 http://cafe.naver.com/win32cpp/6

출처 http://blog.naver.com/coro101/80001644936

출처 http://www.chanywa.com/

출처 http://www.winapi.co.kr/ApiBoard/content.php?table=tblqa&pk=7530

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

 

함수호출 방식이 __cdecl, __pascal, __stdcall로 여러 가지가 있는 이유는 윈도우즈의 역사성에 있다. 우선 win16에서는 실행파일의 크기가 줄어들고 속도가 빠르다는 이유로 pascall 방식을 사용 했고 win32에서는 가변매개인자를 지원하는 함수를 제외한 모든 함수들은 __stdcall을 사용 한다. 만약 c 방식의 함수호출을 원한다면 __cdecl을 명시해 주어야 한다.(윈도우즈 프로그래밍에 있어서) 우선 c 방식의 함수 호출과 pascal 방식의 함수호출의 차이점을 알아보자. 첫 번째로 함수호출후 종료 시점에 호출한 함수의 스택을 정리하는 주체가 호출한 함수이냐 아니면 호출당한 함수이냐의 차이이다. 두 번째는 매개인자를 스택에 넣는 방향에 따라 나눈다. 즉, 다음과 같이 정리할 수가 있다.

1.인수를 스택에 집어넣는 방향에 따라서 다음과 같이 나뉘고
      pascal : 인수를 스택에 저장하는 순서를 왼쪽에서 오른쪽으로 한다.
      cdecl : 인수를 스택에 저장하는 순서를 오른쪽에서 왼쪽으로 한다.
      stdcall : 인수를 스택에 저장하는 순서를 오른쪽에서 왼쪽으로 한다.

2.스택에 인수를 pop 하는 주체에 따라서 다음과 같이 나뉘고.
      pascal : 호출을 당하는 쪽이 스택공간을 삭제한다.
      stdcall : 호출을 당하는 쪽이 스택공간을 삭제한다.
      cdecl : 호출을 하는 쪽이 스택공간을 삭제한다.

 

이렇게 stdcall은 pascal방식과 cdecl방식을 혼합한 형태를 띄운다


 

:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

그럼 __stdcall __cdecl 공통점과 차이점을 살펴보자.


공통점

함수의 인자를 스택에 쌓을  오른쪽에서부터 왼쪽으로 쌓기 때문에 가장 첫번째 인자가 스택의  위로 올라온다.

차이점

__stdcall은 호출된 함수 __stdcall 정의된 함수 내부에서 스택이 청소되고

__cdecl 호출한 함수에서 스택을 정리하여 준다.

 

__cdecl 이러한 특징 때문에 __cdecl 정의된 함수는 가변 인자를 지원할  있게 된다왜냐하면 호출한 함수에서는 가변 인자의 스택의 크기를 알지만 호출된 함수에서는 가변 인자의 스택의 크기를   없기 때문이다
 


그리고 흔히들 OS에서 사용되는 함수나 다른 프로그래밍 언어에서 사용할  있게 DLL 만들  함수를 __stdcall 선언해야 한다고 말한다.


 그럴까

그것은 __stdcall 서로 다른 프로그래밍언어 사이에서 통일된 표준이기 때문이다. OS 특정 언어만을 지원할  없고 또한 서로 다른 프로그래밍 언어들이 서로 다른 함수의 인자 처리 방식을 사용한다면  프로그래밍 언어로 쓰여진 함수가 다른 프로그래밍 언어에서 사용되는 것이 불가능하다그래서 표준이 필요하였고 그것이 __stdcall 것이다
 
또한 __stdcall __cdecl 선언되는 것보다 프로그램의 크기가 작아지고 빠르다고 한다.

 이유는 무엇일까?


 

 부분은 데브피아에 '''고임''' 님이 어셈블리 코드까지 제시하면서 자세히 말씀해 주셨다아래는 고임님의 글에 본인이 약간의 수정을 가한 것이다.

 

cdecl 방식에서는 다음과 같습니다.  
함수의 형태가 다음과 같다고 칩시다
.  
 
MyFunc  Proto :DWORD,:DWORD,:DWORD,:DWORD 
 
Invoke MyFunc, 1, 2, 3, 4 
 
위의 코드는 다음과 같습니다

 
  Push    4 
  Push    3 
  Push    2 
  Push    1 
  Call    MyFunc 
  Add      sp, 16  ;; -->> 
스택 정리 코드
.. 
     
 이것은 MyFunc 인자를 오른쪽에서 왼쪽으로 스택에 집어 넣습니다
.  
그리고 보다시피 스택 정리 코드가 MyFunc 리턴된 다음에 있습니다.

이는 함수를 호출한 쪽에서 호출된 함수가 리턴된 다음에 스택을 정리한다는 뜻입니다
 
stdcall 
방식은 다음과 같습니다.
 
Invoke    WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT 
 
call 
명령으로 풀어쓰면 다음과 같이 됩니다

 
    Push    SW_SHOWDEFAULT 
    Push    NULL 
    Push    NULL         
    Push    hInstance     
    Call    WinMain 
 
여기서 보면 스택을 정리하는 코드가 빠져 있습니다.

따라서 스택 정리 코드가 없어진 stdcall 방식이 실행크기가 작아지고 속도도 명령어 하나 만큼이나 빨리지게  것입니다.

하지만 한가지 의구심이 듭니다어차피 호출당한 함수에서 해제를 하나 아니면 호출한 함수에서 해제를 하나 똑같이 해제를 하는데 호출당한 함수에서 해제를 하는 것이 속도나 크기가  줄어드는 것일까요


엎어치나 되치나.. 스택을 어디선가 정리는 해야할텐데 도대체 ? 어떤 꽁수로이걸 해결했단말가

여기에는 8086 아키텍쳐에 관련된 명령어가  원인으로 등장합니다


그리고 스택을 정리한다는  자체가  함수를 호출한 뒤에  Add sp, 16으로 스택포인터를 인자의 크기만큼 변경을 시킨다는 이야기입니다근데 여기서 프로시저  함수를  수행했을때 원래 상태로 돌아가게   쓰이는명령어는 ret입니다.  (프로시저와 함수라는 용어를 병행하고 있는데.. ㅡㅡ;;  그냥 하나의 분리된 코드 덩어리다라고  이해해주시기 바랍니다정확히 보면 서로 의미가 틀리지만.. ㅡㅡ; )


함수 시작하고함수가 끝났을때 ret 명령어로 호출한 부분으로 넘어가게 됩니다.  다시 말하면  명령어는 실행되던 함수를 바로 빠져나가게 됩니다따라서..  스택을 정리할 시간이 전혀 없었습니다이에 8086설계자들은 함수에서 리턴이 될때 스택포인터(SP) 적절한 위치로 리셋을 시킬  있는 ret명령어 새로 제공을 하여  문제를 아주 손쉽게 해결해 버렸습니다.   ret, n 이라는 명령어를 제공했다는 셈이지요..  

어차피 리턴할  스택 포인터가 정리되는 부분으로 아예 리턴을 해버리란 이야기이지요.. 


이것은 가만히 앉아서 프로그램의 속도와 크기를 이점을 살리는 일이었습니다.  
  Add      sp, 16  ;; -->> 
추가된 코드
..  
호출하는 부분에서 이렇게 코딩하는 대신 호출 받는부분에서 리턴할때 ret, 16으로 해결했다는 이야기지요..  이래서 속도가 더빨라집니다크기도 줄어들구요.. 


생각을 한번 해보자구요... 이런식의 함수가 굉장히 많이 호출된다면..  크기나 실행 시간이 증가되는건 당연하겠지요.. 
 
마지막으로 정리를 하자면..


첫째, stdcall 방식은 cdecl 방식보다 빠르지만 가변 인자를 지원하지 못한다
둘째, OS 호출하는 함수나 DLL 들어가는 함수는 가능하면 stdcall 쓴다.

 

 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


자 이제는 WinMain함수를 살펴보자 일반적으로 WinMain은 다음과 같이 선언되어 있다.

int APIENTRY WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR pCmdLine, int nCmdShow )

APIENTRY는 WINAPI와 같은 형식을 나타낸다. 이것은 FAR __stdcall로 정의되어 있다. 또한 참고로 CALLBACK은 __stdcall로 정의되어 있다. 그럼 WINAPI와 __cdecl 함수의 호출 방식의 차이점을 예제로 알아보도록 하자. 아래에 두 방식으로 호출되는 간단한 예제가 있다.

#include "stdafx.h"

int __stdcall func(int i,int j);
int __cdecl func2(int i,int j);


int _tmain(int argc, _TCHAR* argv[])
{
        func(1,2);
        func2(2,3);

        return 0;
}

int __stdcall func(int i,int j)
{
        int r;
        r=i+j;
        return r;
}

int __cdecl func2(int i,int j)
{
        int r;
        r=i+j;
        return r;
}


이 함수들을 호출하는 부분을 디스어셈블 해 보갰다.


        func(1,2);
00411A1E  push        2    
00411A20  push        1    
00411A22  call        func (411069h) 
        func2(2,3);
00411A27  push        3    
00411A29  push        2    
00411A2B  call        func2 (4110FFh) 
00411A30  add         esp,8 <-------------- cdecl의 함수호출의 경우는 이 부분이 추가됨
        return 0;
...
...
00411AA0  ret         8    <------- func가 종료될 때 
...
00411AE0  ret              <--------func2가 종료될때
...

위에 보면은 func를 호출할 때는 없는 코드가 func2에는 있는 것을 볼 수가 있다.

 
바로 스택을 정리 해주는 코드이다.
 
add         esp,8 
 
모든 함수 호출 형식이 이와 같았다면..
실행 화일 코드에 add         esp,8라는 명령어가 더 들어가게 된다.
 
따라서 이 코드가 존재하지 않는 pascall 방식이 실행크기가 작아지게 된 것이다. 파스칼 호출 방식은 속도도 저 명령어 하나 만큼 빨라지게 되는 것이다. 여기에는 8086 아키텍쳐에 관련된 명령어가 그 원인으로 등장한다. 그리고 스택을 정리한다는 것 자체가 그 함수를 호출한 뒤에 add         esp,8으로 스택포인터를 인자의 크기만큼 변경을 시킨다는 이야긴데...근데 여기서 프로시저 즉 함수를 다 수행했을때 원래 상태로 돌아가게 될 때 쓰이는 명령어는 ret이다. 함수 시작하고, 함수가 끝났을때 ret 명령어로 호출한 부분으로 넘어가게 된다. 다시 말하면 이 명령어는 실행되던 함수를 바로 빠져나가게 된다. 따라서.. 스택을 정리할 시간이 전혀 없다. 이에 8086설계자들은 함수에서 리턴이 될 때 스택포인터(SP)를 적절한 위치로 리셋을 시킬 수 있는 ret명령어를 새로 제공을 하여 이 문제를 아주 손쉽게 해결해 버렸다. 즉 ret, n 이라는 명령어를 제공했다는 셈이다.. 

어차피 리턴할 걸 스택 포인터가 정리되는 부분으로 아예 리턴을 해버리란 이야기다.. 이것은 가만히 앉아서 프로그램의 속도와 크기를 이점을 살리는 것이다. 

add         esp,8   ; -->> 추가된 코드.. 

호출하는 부분에서 이렇게 코딩하는 대신 호출 받는부분에서 리턴할때 ret, 8으로 해결했다는 것이다. 
이래서 속도가 더빨라지는 것이다. 크기도 줄어들고..... 생각을 한번 해보자..... 이런식의 함수가 굉장히 많이 호출된다면..  크기나 실행 시간이 증가되는건 당연하지 않겠는가 ? 이 이유로 속도와 크기가 아주 중요시 되던 시절에(windows 3.0, 3.1이 널리 사용되던 시절에) OS/2와 Windows설계자들은 API함수를 설계할때 프로그램이 느려지고 크기가 커지는 C방식을 사용하지 않고 pascal이나 fortran이 사용하고 있는 방식으로 스택 프레임을 설계 하게 되었다. 바로 이런 이유가 바로 pascal방식에 비교해서 바로 cdecl의 단점이 되는 것이다.
 
자 이번에는 다시 위의 cdecl 호출의 장점을 보게 되면.. 가변매개인자를 사용할 수가 있다는 것인데... 즉, 매개인자를 오른쪽에서 왼쪽으로 집어 넣는 것이 왜 중요한가? 이다.

이것은 인자의 첫번째가 어디인지 확실하다는 것이다.
 
즉 알려진 장소에서 첫번째 인자를 찾아낼 수 있다는 장점으로 가변인자를 허용할 수 있다. 호출이 되었을대 스택의 맨 상위부분이 인자의 첫번째임은 확실하니까.... 이것이 cdecl방식의 장점이 되는 것이다. 함수호출이 끝난후 스택을 정리할 때 호출한쪽에서는 정확하게 Stack을 사용한 사이즈를 알고있기 때문에 문제가되지 않지만 호출당한쪽에서는 또다른 정보를 가지고 사용한 Stack의 사이즈를 알아야 하기 때문에 심각한 문제가 발생할수 있다는 것이다. 그러기 때문에 stdcall은 함수호출 방식은 파스칼을 따르고 있지만 가변매개인자는 지원하지 못하는 것이다. 가변매개인자를 꼭 사용해야만 한다면 반드시 cdecl을 사용해야만 한다.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

많은 사람들이 모르시는 파트이신 것 같아서 올립니다.

함수를 호출하게 되면 그 파라미터(인자)들을 전달해야 됩니다.

그러기 위해서 파라미터를 스택에 저장해서 전달하게 되는데

저장하는 순서는 항상 오른쪽에서 왼쪽으로 합니다.

그러니까 첫번째 파라미터가 항상 제일 마지막에 저장됩니다.

그렇게 함으로써 SP+6이 항상 첫번째 인자를 가르키도록 하는 것입니다. (어셈블리어 관점입니다.)

아무튼 차이점은 그 함수가 종료했을 때 스택에 저장된 파라미터를 누가 지우냐하는 문제가

남습니다. 가장 좋은 것은 함수가 지우는 것입니다.

그렇게 함으로써 좋은 점은 코드의 길이가 짧아진다는 것입니다.

(아니면 함수를 호출할 때마다 그 뒷부분에 스택에서 인자를 지우는 부분이 따라야 겠죠?)

이렇게 함수가 인자를 스택에서 지우는 방식을 stdcall이라고 합니다.

하지만 가변인자의 경우(예를 들어 sprintf처럼)에는 함수를 호출한 쪽에서 지우는 것이

안전합니다. 어떤 인자들을 저장했는지 호출한 쪽은 정확히 알지만 함수쪽에서 불분명합니다.

정확히 말하자면 사용자가 형변환등을 실수로 해서 함수가 다른 데이터형으로 인식하거나해서

잘못 지우면 어떤 결과가 날지 모르니까 호출한 쪽에서 지우니 편이 안전하다는 거죠.

정리를 하면 stdcall은 함수가 인자를 스택에서 지우는 형태

cdecl은 호출한 쪽에서 인자를 스택에서 지우는 형태로

stdcall은 일반적인 Windows API, CALLBACK 함수에 사용되고

cdecl은 Windows API 중 가변인자일 경우에 사용됩니다.

그리고 참고로 보통 C++에서 위의 것을 다 생략하면 thiscall이 사용되는데

(thiscall은 키워드가 아니니 사용하지 마세요.)

방법은 cdecl과 동일하고 차이점은 추가로 this 포인터가 제일 마지막에 스택에 저장된다는

것입니다. 이 정도면 답변이 되었으면 하네요...

 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

 

함수의 호출 관례에 대해서 궁금해 하시는 분들이 많은것 같아 
부족한 실력이지만 정리해서 올립니다..많은 도음이 됐음 하네요^^ 
우선,함수의 호출관례(function calling convention)란, 
함수의 파라미터를 스택(stack)에 푸시(push)하는 순서,푸시하는쪽 및 이름 변화(name mangling)을 명시한 것입니다.. 

표로 정리 해보면.. 

변경자 푸시(push)순서 팝(pop)하는쪽 이름변화 
_cdecl Right first Caller '_'prepended 
_fastcall Left first Callee '@'prepended 
_pascal Left first Callee Uppercase 
_stdcall Right first Callee No change 

Right first는 함수의 파라미터를 오른쪽에서 왼쪽으로 평가하여 스택에 푸시하는것을 나타냅니다. 

Caller/Callee는 호출하는쪽 /호출 당하는쪽 에서 스택 동작을 하는것을 의미하구요.. 

이름 변화란.. 함수 이름 오버로딩..(이름이 같은 함수를 구분하기 위해서 컴파일러는 이름 장식(name mangling)작업을 하죠..) 에서 쓰이는데(어셈브러 소스를 보시면 알수 있습니다..) 
c방식의 경우 명칭 앞에 언더스코어_가 붙는다는 애기죠. 

Windows API(Application programning Interface)함수는 모두 WIN32방식(_stdcall)을 사용합니다.. 
이것은 파라미터를 팝하는 쪽이 Callee이기 때문이죠..(아마도 윈도우즈 운영체제에 이미 존재하는 함수들에 대해서 일관된 파라미터 팝 방식을 적용함으로써 얻어지는 최적화의 이점 때문인듯..) 
만약 호출하는 쪽에서 파라미터를 팝해야 한다면 ,실행 코드를 생성할때 컴파일러는 이부분의 코드를 생성해야 되겠죠.. 컴파일러의 부담이 커지는것은 둘째치고(실행파일의 크기증가),운영체제의 관할 영역을 건드리는 보호차원에서 문제가 될수 있습니다.. 

모든 C++컴파일러에서 기본값은 cdecl 입니다.. 
콜백함수에 명시적으로 _stdcall 즉,WIN32혹은 WINAPI를 지정해야하는 이유죠... 
(*참고)WIN32는 windows.h에 선언된 _stdcall의 매크로입니다. 
WIN32에서 _pascal은 _stdcall로 대치되었습니다. 

Windows응용프로그램의 시작함수인 WinMain()은 _stdcall이지만 
WIN32콘솔 응용프로그램에서 시작함수인 main()은 여전히 _cdecl입니다. 

호출관례에 대한 예를 들어보면.. 

#include <iostream.h> 

void _pascal f (int i,int j) { 
cout<< i << " " << j <<endl; 

void _cdecl g(int i ,int j) { 
cout<< i << " " << j <<endl; 


void main() { 
int i,j; 

i=1; j=2; 
f(i==j,i=j) ;//왼쪽에서 오른쪽으로 평가(left to right evaluation) 
g(i==j,i=j);//오른쪽에서 왼쪽으로 평가(right to left evaluation) 


실행 결과는 0,2 
1,2 

pascal방식은 파라미터 푸시를 왼쪽에서 오른쪽으로 한다는 사실을 주의해야겠죠.. 
f(i==j,i=j);에서 i==j가 먼저 평가되어 거짓이 되어서 "0"이 출력 되는것입니다.. 

windows에서 더이상 pascal방식은 존재 하지 않습니다._stdcall로 대치되었습니다

 

 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

 

호출관례에 대한 예를 들어보면.. 

#include <iostream.h> 

void _pascal f (int i,int j) { 
cout<< i << \" \" << j <<endl; 

void _cdecl g(int i ,int j) { 
cout<< i << \" \" << j <<endl; 


void main() { 
int i,j; 

i=1; j=2; 
f(i==j,i=j) ;//왼쪽에서 오른쪽으로 평가(left to right evaluation) 
g(i==j,i=j);//오른쪽에서 왼쪽으로 평가(right to left evaluation) 


실행 결과는 0,2 
1,2 

pascal방식은 파라미터 푸시를 왼쪽에서 오른쪽으로 한다는 사실을 주의해야겠죠.. 
f(i==j,i=j);에서 i==j가 먼저 평가되어 거짓이 되어서 \"0\"이 출력 되는것입니다.. 

windows에서 더이상 pascal방식은 존재 하지 않습니다._stdcall로 대치되었습니다.

_____________________________________________________________________________

먼저 좋은 가르침을 주셔서 너무 감사드립니다.

그런데 한가지 문제점이 있는데요...그건 뭐냐면 님의 소스중에 __pascal이란 것은
지금은 인식이 않 됩니다. 그래서 _stdcall로 대체해서 call해줬는데...

그런데 결과가 님이 생각하는 0,2 1,2 가 아닌 1, 2 1, 2입니다.

그리고 MSDN에서도 Parameter push 방법이 둘다 차이가 없다고 나옵니다.

아마도 이부분은 님께서 약간 착각하시지 않은가 생각되네요 ^^;

님이 확인하신후 이 부분을 알려주셨음 합니다.