두근두근이야기

cdecl 본문

IT/IT

cdecl

골든 2013. 4. 2. 13:37

우선 이 말을 베이스로 깝니다. 

--------------------------------------------------------------

즉 Borland C++ Version 3.1 에서는 

cdecl : 인수를 스택에 저장하는 순서를 오른쪽에서 왼쪽으로 한다.

pascal : 인수를 스택에 저장하는 순서를 왼쪽에서 오른쪽으로 한다.


반면 Microsoft Visual C++ 6.0 에서는

__cdecl : 인수를 스택에 저장하는 순서를 오른쪽에서 왼쪽으로 한다.

__stdcall : 인수를 스택에 저장하는 순서를 오른쪽에서 왼쪽으로 한다.

--------------------------------------------------------------


자 이제 제가 이야기 할건.. 무엇이냐면.. 보시죠.


역사성을 좀 가미해서.. 


Borland c++ Version 3.1이 나온 당시에는


cdecl과 pascal 호출 규약만이 존재했습니다. 


그 이후에 cdecl과 pascal의 장점만을 따온 _stdcall이 나오게 된거구요. 


따라서 비슷하게 보이는데 서로 틀리게 보이는 건 ^^ 바로 이런 연유에서 비롯됩니다. 


이 넘들에는 두 가지의 다른 점이 있습니다.(또 있으면 리플 달아주세요..)


(언더바는 생략할께요.. )


●인수를 스택에 집어넣는 방향에 따라서 다음과 같이 나뉘고


pascal : 인수를 스택에 저장하는 순서를 왼쪽에서 오른쪽으로 한다.

cdecl : 인수를 스택에 저장하는 순서를 오른쪽에서 왼쪽으로 한다.

stdcall : 인수를 스택에 저장하는 순서를 오른쪽에서 왼쪽으로 한다.


●스택에 인수를 pop 하는 주체에 따라서 다음과 같이 나뉩니다.


pascal : 호출을 당하는 쪽이 스택공간을 삭제합니다.

stdcall : 호출을 당하는 쪽이 스택공간을 삭제합니다.

cdecl : 호출을 하는 쪽이 스택공간을 삭제합니다. 


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


stdcall은 두 가지의 장점을 모두 따왔다고 합니다. 들리는 소리엔 말이죠.. ㅡㅡ;


여기서 중요한 점은 아직도 콜백함수에서는 pascall이나 stdcall을 사용한다는 것이죠.


전 이것이 아주 궁금했습니다. 아직도 정확히 모르구요.. 누가 좀 정확히 가르쳐주셨으면


좋겠지만.. 


아무튼 왜 두 가지의 호출 규약을 동시에 다 써야만 했는지? 그 이유가 무척이나 궁금했습니다. 


스택에 오른쪽부터 들어가던 아니면 왼쪽부터 들어가던 호출한넘이 스택공간을 삭제하던


호출 당한 넘이 스택공간을 삭제하던간에 왜 도대체 왜 pascal이라는 호출 규약을 사용하게 되었

냐!


이거지요.. 



///// 자 이제부터 제가 실제적으로 할 이야기가 나옵니다. 집중 해주시구여. ㅡㅡ; //////



많은 분들은 MSDN에 나와있는 내용을 보았으므로 대충 cdecl과 stdcall이 어떻게 다른지를 


알고 계실겁니다. 


근데 도대체 왜 저렇게 나누게 되었을까요?


떠도는 말로는 다른 언어와 dll등등을 공유하기 위해서 사용되어진다! 라고 말씀을 하시기도 하

고.. 


물론 일리는 있는 이야기입니다. 


도대체 어떤말이 옳고 그른지도 정확히 판단이 안되는 것이 이 부분입니다. 


제가 몇년간 아직도 그 이유를 찾지 못한.. 그러니까 확실한 동기도 모르고 stdcall이나 cdecl이

니 하는 


것들을 사용하고 있었다는 것이죠.. 


확실히 말씀드릴 수 있는 것들만 이야기 해보도록 하겠습니다. 


● "cdecl과 pascal은 C와 C++의 포인터 때문에 생겨났다."


이 명제가 사실일까요? 사실은 아니더라도 왜 제가 이런 이야기를 하는지 들어보시기 바랍니다. 

( 사실은 저도 잘 모릅니다. )


처음에 c와 C++의 디폴트가 cdecl방식으로 되어 있는 이유와 이것이 파스칼과 틀린 이유는 바로 


충분히 cdecl이 더 프로그래머들한테는 호감이 갔습니다. 왜냐하면 가변인자를 지원하니까요.. 


가변 인자를 지원한다는 이야기는 바로 포인터를 써야 된다는 이야기인데 pascal에는 불행히도 


존재 하지 않은 것이었지요.. 


물론 델파이에서 지원하기는 하지만 windows가 나올 당시에는 이런 포인터 개념은 


C, C++밖에 존재 하지 않았습니다. 물론 어셈블리어에는 존재합니다 아주 에전에도 ㅡㅡ;;

( 혹시 다른것도 존재하나? 불안 초조.. )


MS는 윈도우즈의 API함수들을 만들때 많은 고심을 하다가 결국 다양한 언어로 윈도우즈 프로그램

이 


가능 하도록 pascal의 호출 규약을 따르게 되었을 겁니다. 


그 이유는 비베를 확실히 밀고 있는 것으로 찾겠습니다.( 여기서부턴 경험론적 추측 ㅡㅡ;;)


그러면서도 불구하고 C와 C++의 최대의 장점인 가변인자를 버릴 수는 없어서 cdecl이라고 불리

는 


호출 규약을 만들어 놓고 C나 C++로 짠 프로그램 자체내에서 사용을 하도록 배려를 해주었다는 

것이죠..



● 이제 주의 깊게 살펴볼 점은 우리는 MFC에서 __stdcall이라는 단어를 별로 쓰질 않습니다. 


몇몇의 경우 빼고는 말이지요. 바로 콜백함수입니다. 


윈도우 프로시저라고 불리는 코드 덩어리는 항상 pascal아니면 __stdcall이었습니다. 


그리고 dll에서 사용되는 함수들이지요.. 이 함수들은 델파이에서도 사용될 수도 있고 VB에서도 

사용이


될 수도 있는 가능성이 아주 농후한 놈이란걸 명심해 두십시요. 


자 이제 살펴보기 전에..


자 그러면 몇가지를 정리 하고 넘어가도록 하지요.. 윈도우즈 OS 자체는 하나의 시스템이며, 위

의 말한 바와 같이


스택을 운용하는데 있어서 두 개의 다른 방식을 가질 수가 없습니다. 왜냐하면 시스템이 복잡해

지거덩여.. 


따라서 시스템 내부에서는 __stdcall방식으로 호출하고 있다는 것을 명심하시길 바라고 우리의 

응용 


프로그램안에서 스택을 어떻게 쓰던 전혀 상관 안하기때문에 우리는 전통적인 C, C++방식을 사용

할 수 있습니다. 


자 대충 결론을 내어보면 다음과 같습니다. 


API함수는 C, C++뿐만아니라 파스칼, VB같은 언어에서도 사용할 수 있게끔하기 위해서,


c나 C++의 방식을 포기하고 파스칼 방식을 따랐다. 그래서 __stdcall이된다. 


DLL에 들어가는 함수들도 다른 언어에서도 사용이 될 수 있으므로, __stdcall로 호출규약을 맞추

어준다. 


자 이제 콜백함수인데.. 콜백함수의 이름이 왜 콜백함수인지 부터 설명을 드리겠습니다. 


여러분은 대부분 프로그램을 작성하실때 윈도우즈의 API함수를 불러다 썼을 겁니다. 맞습니까?


즉 다음과 같다는 것이지요. 


OS(API) <---- 내 프로그램


이런 식으로 호출을 했습니다. 


즉 내 프로그램에서 API의 함수를 불러다 썼지만.. 


콜백함수들은 내 프로그램에서 실행되는 것이 아니라. 위와 반대로


OS가 필요할때마다 부르는 함수가 바로 콜백 함수가 됩니다. 


즉 OS를 기준으로 호출하는 순서가 반대로 되었다고 해서 CallBack함수가


된다는 것이지요.. 물론 WinMain도 마찬가지입니다. 


자 그러면 __stdcall을 왜 붙이냐는 것이죠.. 


afx_msg라고 있는 함수나 WinMain이나 전부 OS가 호출을 하며, 


이 OS는 사용하는 언어 자체의 기본(디폴트)이 __stdcall이기때문에


우리도 OS가 호출하는 함수들은 이 형식에 맞추어 주어야 한다는 슬픈 사실에 


도달합니다. 


한 나라에서 두 개의 언어를 쓰면 혼란이 오기때문에 표준어를 정하듯이, 


이 넘의 윈도우즈라는 나라는 표준어가 __stdcall이고 사투리가 __cdecl이라는 것입니다. 


어쨌든간에.. 저의 추리력 상상력 경험력 등등을 동원해서 대충 이야기를 끼워 맞추어보면

( 틀리면 리플 달아주십시요.. )


이렇습니다. 


MSDN에는 이런 이야기는 나오지 않고 그저 그냥 설명만 해놓았지요..ㅡㅡ;


저는 어쨌든 그들의 필요성을 이렇게 억지로라도 만들지 않으면 영 찜찜해서 ㅡㅡ;;


자 그럼 수칙 제 한가지.. 


첫째, OS가 호출할 함수는 절대적으로 __stdcall을 쓴다. 


왜냐하면 OS는 __stdcall의 형식으로 호출하기 때문이다. 


둘째, DLL에 들어가는 함수를 만들때 왠만하면 __stdcall을 쓴다. 


왜냐하면 다른 프로그램에서도 그 함수를 호출할 수 있기때문이다. 


마지막 셋째, WinMain은 반드시.. _stdcall이다. 


싫으면 에러만 날뿐이다. 



고임


ps. 제 이야기는 틀릴 수도 있습니다. 


확실하게 알고 계신분은 리플 달아주시길 바랍니다. 






여기서 끝일줄 알았죠? 이것에 추가한 글이 더 있습니다. 


별 쓸데 없는 이야기를 하는 바람에 모두들 머리가 아프신줄 압니다. ^^


그래도 모르고 쓰는것보다 알고 쓰는게 더 낮다고 생각하기 때문에.. ㅡㅡ;; 


열심히 공부한 것에 따르면. ㅡㅡ;; 힘들었습니다.. 헉헉.. 


경험론, 상상력, 추리력으로 쓴 제 글에 확실한 지식을 가지고.. ㅡㅡ;; 


우선 결론만 말씀드리자면


Win16과 Win32시스템에서 차이가 나게 되는데.. 


Win16에서는 실행화일의 크기가 줄어들고 속도가 빨르다는 이유로 pascal방식을 사용했습니다. 


Win32에서는 가변인자를 지원하는 함수를 제외한 나머지의 함수들은 


배타적 __stdcall을 사용하는 것을 원칙으로 합니다. 


만일 __cdecl을 사용하는 경우 반드시 명시해주어야 합니다. 


자 이제부터 예입니다. 


우선 C방식으로 함수를 호출하는 것과 pascal방식으로 함수를 호출하는 것의 차이를 


이야기 하죠.. 


크기 차이가 나는건 바로 스택을 정리하는 넘의 주체가 호출당하는 넘이냐.. 아니면


호출을 하는 넘이냐 입니다. 


여기서 부터는 밑의 API님이 쓰신 글에 중복되는 거라. 아시는 분은 훌쩍 뛰어넘으셔도 됩니

다. 


pascal방식의 경우. 가장 대표적인 것이 WinMain이겠지요.. 


어셈블러에서는 다음과 같이 호출을 하게 됩니다. 


Invoke WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT


자 여기에서 Invoke라는 어셈블러 명령어는 없고 일종의 매크로 비슷한 건데.. 


call 명령으로 풀어쓰면 다음과 같이 됩니다.


Push hInstance

Push NULL

Push NULL 

Push SW_SHOWDEFAULT

Call WinMain


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의 인자를 오른쪽에서 왼쪽으로 스택에 집어 넣습니다. 


그리고 위에선 없는 코드가 있죠.. 


바로 스택을 정리 해주는 코드입니다. Add sp, 16라는 것이 말이지요.. 


모든 함수 호출 형식이 이와 같았다면.. 실행 화일 코드에 Add sp, 16라는 명령어가 


더 들어가게 됩니다. 따라서 이 코드가 존재하지 않는 pascall 방식이 실행크기가 작아


지게 된것이구요.. 속도도 저 명령어 하나 만큼 빨라지게 되는 것입니다. 


자 그러면 왜? pascal에선 스택 정리를 호출당한 함수에서 한다고 했다고 했습니다. 


그러면 어차피 호출당한 함수에서 해제를 하나 아니면 호출한 함수에서 해제를 하나


똑같이 해제를 하는데 호출당한 함수에서 해제를 하는 것이 속도나 크기가 더 줄어드는


것일까요? 


이 부분이 MSDN을 봐도 그렇고.. 다른 책을 봐도 그렇고 다른 사람의 얘기를 들어봐도 


그렇고 제대로 설명이나 내용이 써있지 않더군요.. 정말 궁금했습니다.


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


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


그리고 스택을 정리한다는 것 자체가 그 함수를 호출한 뒤에 


Add sp, 16으로 스택포인터를 인자의 크기만큼 변경을 시킨다는 이야기입니다. 


근데 여기서 프로시저 즉 함수를 다 수행했을때 원래 상태로 돌아가게 될 때 쓰이는 명령어는 ret입니다. 


(프로시저와 함수라는 용어를 병행하고 있는데.. ㅡㅡ;; 그냥 하나의 분리된 코드 덩어리다 라고 

이해해주시기 바랍니다. 정확히 보면 서로 의미가 틀리지만.. ㅡㅡ; )


함수 시작하고, 함수가 끝났을때 ret 명령어로 호출한 부분으로 넘어가게 됩니다. 


다시 말하면 이 명령어는 실행되던 함수를 바로 빠져나가게 됩니다. 따라서.. 


스택을 정리할 시간이 전혀 없었습니다. 이에 8086설계자들은 함수에서 리턴이 될때


스택포인터(SP)를 적절한 위치로 리셋을 시킬 수 있는 ret명령어를 새로 제공을 하여


이 문제를 아주 손쉽게 해결해 버렸습니다. 


즉 ret, n 이라는 명령어를 제공했다는 셈이지요.. 


멋진 속담으로는 도랑치고 가재잡고 또는 멋진 고사성어로는 일석이조 라는 말로 표현된다


하겠습니다. 


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


이것은 가만히 앉아서 프로그램의 속도와 크기를 이점을 살리는 일이었습니다. 


Add sp, 16 ;; -->> 추가된 코드.. 


호출하는 부분에서 이렇게 코딩하는 대신


호출 받는부분에서 리턴할때 ret, 16으로 해결했다는 이야기지요.. 


이래서 속도가 더빨라집니다. 크기도 줄어들구요.. 


생각을 한번 해보자구요... 이런식의 함수가 굉장히 많이 호출된다면.. 


크기나 실행 시간이 증가되는건 당연하겠지요..


이 이유로 속도와 크기가 아주 중요시 되던 옛날 옛적에 OS/2와 Windows설계자들은


API함수를 설계할때 프로그램이 느려지고 크기가 커지는 C방식을 사용하지 않고 


pascal이나 fortran이 사용하고 있는 방식으로 스택 프레임을 설계 하게 되었습니다.


바로 이런 이유가 바로 pascal방식에 비교해서 바로 cdecl의 단점이 되는 것입니다. 


자 이번에는 다시 위의 cdecl 호출의 장점을 보게 되면.. 


인자의 오른쪽에서 왼쪽으로 집어 넣는 것이 왜 중요한가? 입니다.


이것은 인자의 첫번째가 어디인지 확실하다는 것입니다. 


즉 알려진 장소에서 첫번째 인자를 찾아낼 수 있다는 장점으로 가변인자를 허용할 수 있습니다.


호출이 되었을대 스택의 맨 상위부분이 인자의 첫번째임은 확실하니까요.. 


이것이 cdecl방식의 장점이 되는 것이구요.


자 그러면 STDCALL형식은 어떨까요?


저 두개의 방식이 짬뽕이 되었다고 이야기 했습니다. 즉 장점만을 수용했다고 이야기를 했습니다.


아까 위에서 보았던 이 코드는 


Invoke WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT


call 명령으로 풀어쓰면 다음과 같이 됩니다.


Push SW_SHOWDEFAULT

Push NULL

Push NULL 

Push hInstance 

Call WinMain


와 같이 되는 것이지요.. 


스택을 정리하는 부분도 사라졌습니다. 


그리고 WinMain의 가장 첫번째 인자인 hInstance가 스택의 가장 윗부분에


들어가게 되었지요. 


이런 이유로 __stdcall 속도는 파스칼 방식과 똑같으면서 가변인자를 지원할 수 있는 방법인


cdecl과 스택프레임이 같게 됩니다. 


즉 첫번째 파라메터( 인자 == 아규멘트 )가 항상 고정된 위치로 스택에 저장이 된다는 

(제가 영어에 아주 약하기때문에 그냥 한글로 썼습니다. ㅡㅡ;)

이야기입니다. 


그렇다고 해서 __stdcall자체가 가변인자를 지원해주지는 못합니다.

겉보기에만 장점으로 추가 된것 같은. ㅡㅡ; (이건 제 의견임)

따라서 어셈블리어로 Windows프로그래밍을 할때 가변인자의 함수를 호출할때는 항상

cdecl형식으로 호출해야만 합니다. 안그러면 에러 나겠지요.. 

'IT > IT' 카테고리의 다른 글

읽어보기  (0) 2013.07.17
리눅스 부트 프로세스  (0) 2013.07.16
gets와 scanf  (0) 2013.03.28
가비지 콜렉터(Garbage Collector)  (0) 2013.03.27
달빅(Dalvik virtual machine)  (0) 2013.03.12