많은 프로그래밍 강사들과 서적이 void main()으로 가르치고 있는 실정이다.
하지만 표준 C에서 main()의 원형은 int main(int argc, char **argv) 이다.
혹은 int main()으로 써도 무방한데, 호출 컨벤션이 cdecl이므로 호출자 측에서 스택을 클리어하기 때문이다.
하지만 void main()은 이야기가 다르다. 다음의 코드를 보자.
#include <windows.h>값을 리턴하지 않는 voidTest 함수를 선언하였다. 이 함수는 단지 내부적으로 GetModuleHandle Win32 API를 호출하고 끝나버린다. 그 밑에는 unsigned int를 리턴하는 함수 포인터 타입 UIntFunc를 선언하였다.
#include <cstdio>
#include <cstdlib>
void voidTest()
{
GetModuleHandle(NULL);
}typedef unsigned int (*UIntFunc)();
int main()
{
UIntFunc func = (UIntFunc)&voidTest;
printf("voidTest returned: %d\n", func());
printf("GetModuleHandle(NULL) returned: %d\n", GetModuleHandle(NULL));
return EXIT_SUCCESS;
}
다음은 메인을 보자. UIntFunc 타입의 함수 포인터 변수를 선언하고, voidTest를 강제 캐스팅하여 집어넣었다. 그리고는 함수 포인터를 통해 voidTest를 호출한다. 밑에는 값의 비교를 위해 역시 GetModuleHandle을 호출하였다.
두 콜의 리턴값은 모두 printf로 출력되게 되어 있다. 그럼 출력 결과가 무엇이라고 생각하는가?
일단 밑의 GetModuleHandle(NULL)은 실행되고 있는 프로세스 모듈의 핸들을 리턴하므로 프로세스의 핸들이 출력될 것이다.
그럼 위의 voidTest는? voidTest는 값을 리턴하지 않는다. 단순히 void형 함수를 unsigned int를 리턴하듯이 캐스트했을 뿐이다. return 하는 값이 없으니 아마도 쓰레기값이 출력되지 않을까.
직접 실행해 본 혹자는 저 두 값이 동일하다는 데 경악을 금치 못했을 것이다. 어째서 두 값이 동일한가? return도 하지 않았는데 함수 내부에서 호출한 GetModuleHandle 값이 나올리는 없지 않은가?
상식적으로만 생각하면 당연하다. 하지만 한가지 우리가 여기서 간과하고 있는 사실이 있다. 컴퓨터가 읽어들이는 것은 기계어라는 사실. 함수 리턴부 근처의 기계어를 어셈블리로 변환하여 구조를 대충 보겠다.
push ebp대충 이런 구조로 되어있을 것이다.
mov ebp, esp
; 레지스터 저장
push 0
call dword ptr GetModuleHandleA@4
; 레지스터 복원
pop ebp
ret
voidTest 함수의 구조이다.
이것만 봐서는 잘 모르니 한가지 더 보도록 하자.
DWORD dw = GetModuleHandle(NULL);로 변경했을 때의 코드이다.
push ebp중간을 잘보면 mov [ebp-4], eax라는 구문이 보인다. 이 부분이 바로 GetModuleHandle(NULL)의 리턴값을 변수 공간에 저장하는 부분이다. [ebp-4]는 첫 번째 지역변수 주소이다. 참고로 mov 인스트럭션은 mov A, B 와 같은 형식으로 사용하며, 사용 효과는 B의 값을 A에 쓴다. 즉 eax 레지스터의 값을 첫 번째 지역변수에 복사한 것이다.
mov ebp, esp
; 레지스터 저장
sub esp, 4 ; 변수 공간 확보
push 0
call dword ptr GetModuleHandleA@4
mov [ebp-4], eax
; 레지스터 복원
mov esp, ebp ; 변수 공간 제거
pop ebp
ret
이것이 왜 중요하냐 하면, C/C++의 4대 호출 컨벤션인 cdecl, stdcall, fastcall, thiscall은 모두 리턴값을 저장하는 데 eax 레지스터를 사용하기 때문이다. 즉, 함수는 eax 레지스터에 리턴값을 기록할 의무가 있고 호출자는 eax 레지스터에 리턴값을 기대할 수 있다. 헌데, 위와 같은 상황이 되면 이야기가 조금 다르다. 실제로 언어 로직상 voidTest가 리턴한 값은 없지만, 마지막으로 기록된 eax 레지스터의 값이 GetModuleHandle(NULL)이 리턴한 값이기 때문에, voidTest()의 리턴값을 받아오게 되면 자연히 GetModuleHandle(NULL)의 리턴값이 출력되는 것이다.
main의 원형은 int main(int argc, char **argv) 이고 이 함수의 호출자는 어디까지나 이 원형 그대로를 기대한다. cdecl이므로 인자값은 받든 안받든 상관없지만, void main()으로 하는 경우 호출자가 int 리턴값을 사용하려고 하는 경우에 예상치 못한 값이 들어갈 수 있다. Windows에서는 대표적으로 배치 프로그램으로 실행하려고 할 때 문제가 되며, 리턴값이 256보다 크면 시스템에서 오류를 띄우는 운영체제도 있다. 따라서 항상 int main()으로 쓰는 것이 좋은 버릇일 것이다.
참고: 32비트 환경에서 32비트 레지스터의 허용치보다 큰 값을 리턴시에는, 예외적으로 파라미터에 주소값이 넘어가게 된다.

댓글을 달아 주세요