본문 바로가기
IT 공부/C, C++

[C/C++] - SDL이 main 함수를 호출하는 원리 (0 A.D. 소스코드 분석)

by exdus3156 2025. 1. 2.

 

 

#1. SDL을 사용하는 게임은 왜 main으로 동작하는 것일까?

 

<0 A.D. 프로젝트의 main.cpp>

 

▷ 0 A.D. 오픈소스의 main 함수는 위와 같이 extern "C"가 붙어 있다. 링크 단계에서 C 스타일로 컴파일하라는 키워드다. 그래서 C++의 다른 함수들과 달리, 이 키워드가 붙은 함수는 네임 맹글링(name wangling)을 하지 않는다.

 

그런데 이 키워드를 main에 붙인 목적을 이해할 수 없었다. 왜냐하면 main 함수는 어차피 네임 맹글링을 하지 않기 때문이다. 따라서 굳이 extern "C" 키워드를 붙일 이유가 전혀 없다. 그런데 나중에 알고보니 0 A.D. 게임은 그래픽 라이브러리로 SDL을 사용하기 때문이라는 것을 알게 되었다.

 

<SDL_main.h>

 

실제로 <SDL_main.h> 헤더 파일에는 위와 같이 애플리케이션의 진입점은 반드시 main으로 작성하고, C++로 개발할 경우 반드시 extern "C"를 붙이라고 명시되어 있다. 0 A.D. 소스코드는 단지 이 요구사항을 따랐을 뿐이다. 

 

main에 extern "C"가 붙은 이유는 SDL의 동작 원리에 숨어 있다. 간단하게 요약하자면, SDL은 내가 작성한 main 함수의 이름을 SDL_main으로 바꿔버리고, SDL 라이브러리에 있는 main 함수가 링커에 의해 프로그램의 진입점으로 호출되는 원리다.

 

 

#. main 함수는 SDL_main으로 이름이 바뀐다.

 

SDL을 사용하는 모듈은 반드시 "SDL.h" 헤더 파일을 포함해 SDL을 사용한다. 이 헤더 파일에 #include로 연결된 SDL 관련 라이브러리들을 추적하면 여러 매크로가 조건부로 정의된 것을 확인할 수 있다. 매크로가 과정이 매우 복잡하여 여기서 포스팅을 하는 것은 의미가 없고, 단지 windows에서 빌드된다면 최종적으로 SDL 라이브러리는 아래와 같이 main이 SDL_main으로 이름이 변경된다는 것을 확인했다.

 

<SDL_main.h> 파일

 

내가 작성한 main 함수가 SDL_main으로 이름이 바뀐다면 그저 평범한 함수가 되어 버린다. 그렇다면 함수 원형에 대한 선언이 필요하다. SDL은 <SDL_main.h> 파일에 아래와 같이 SDL_main에 대한 함수의 원형을 정의했다. 그래서 빌드 시 이름이 변경되어도 문제가 없는 것이다.

 

<SDL_main.h>

 

 

#. 배포된 SDL 라이브러리에 main, wmain, WinMain이 있다!

 

그렇다면 과연 진짜 main은 어디 있는가? 바로 SDL 라이브러리의 <SDL_windows_main.c> 파일에서 아래와 같이 정의했다. 

 

<SDL_windows_main.c>

 

복잡해보이지만 빌드 조건에 따라 위 함수들은 각각 main, wmain으로 이름이 변경되어, SDL 정적 라이브러리로 빌드되면 이 라이브러리 테이블 안에 main, wmain, WinMain이 정의된다. 

 

<SDL_windows_main.c>

 

따라서 SDL을 사용하는 소스코드가 플랫폼에 상관 없이 main 함수를 진입점으로 사용할 수 있는 이유는 SDL이 main을 SDL_main으로 바꿔버리고, 링커가 SDL 자신의 main을 선택하도록 유도하기 때문이다.

 

<SDL_windows_main.c>

 

main, wmain, WinMain 내부에는 모두 main_getcmdline()이 호출된다. 

 

<SDL_windows_main.c> 파일의 main_getcmdline() 내부

 

이 함수 내부에서 SDL_main()을 실행해서 내가 쓴 main이 그대로 실행되는 원리다. 이런 것을 고려하면 왜 extern "C"가 붙어야 하는지 알 수 있다. 그것은 바로 SDL이 c언어로 작성된 모듈이면서 main_getcmdline() 함수가 SDL_main()을 호출하기 때문이다. C언어 모듈이 함수를 호출하면 이름이 있는 그대로 호출된다. SDL 모듈이 진입점을 자신이 가로채고 거기서 내 모듈의 main(SDL_main)을 호출하고 있기 때문에 나의 main은 extern "C"가 되어야 하는 것이다!! 
자세한 내용은 여기 링크

 

따라서 SDL을 사용한 소프트웨어는 /SUBSYSTEM을 굳이 CONSOLE로 설정하지 않아도 된다. WINDOWS로 하는 것이 더 편하다. WINDOWS를 서브시스템으로 설정하고 빌드하면, OS는 실행 파일을 위해 콘솔을 실행하지 않는다. 물론 서브시스템을 바꾸면 진입점이 링커에 의해 WinMainCRTStartup()으로 변경되지만 SDL 라이브러리에는 아래와 같이 WinMain()도 정의되어 있기 때문에 WinMainCRTStartup()가 이 함수를 호출해서 결과적으로 프로그램 동작에 아무런 문제가 없다.
※  자세한 내용인 여기 링크

 

<SDL_windows_main.c>

 

 

 

#2. 0 A.D. 오픈소스 프로젝트 분석

 

 

<0 A.D.> 오픈소스 게임 프로젝트의 서브시스템은 WINDOWS로 설정되어 있다. 최종 빌드 결과물인 실행 파일(PE)에 서브시스템이 WINDOWS로 정의된다. 따라서 OS는 굳이 콘솔을 세팅하지 않는다. GUI 게임이니까 당연한 조치다.

 

파란 부분!

 

/SUBSYSTEM: WINDOWS 이렇게만 정의되면 링커는 진입점을 WinMainCRTStartup()으로 자동 설정해버리지만, 이 프로젝트는 진입점이 따로 설정되어 있다. wseh_EntryPoint() 라는 함수다.

 

 

명시적으로 지정된 엔트리 포인트 함수를 쫓아가면 위와 같이 wmainCRTStartup()을 호출하고 있는 것을 볼 수 있다. 신기하게도 아예 직접 wmainCRTStartup()을 구체적으로 호출하고 있는 모습이다. wmainCRTStartup()은 wmain()을 찾아서 호출할 것이다.

 

이 wmain() 함수는 SDL 모듈에 정의되어 있고, 위에서 본 바와 같이 SDL에 정의된 wmain()은 내부적으로 SDL_main() 함수를 호출하는 것이다. 이 SDL_main() 함수가 바로 아래의 코드다. 

 

 

디버깅 포인트를 내부에 설정하고 디버깅을 해보면 아래와 같은 스택 프레임이 추적된다.

 

 

extern "C" main()이 SDL_main()으로 변경되어 실행되고 있다! 빌드 시 진입점으로 설정된 wseh_EntryPoint()부터 시작해서, CallStartupWithingTryBlock()wmainCRTStartup()wmain()main_getcmdline()SDL_main() 으로 이어지고 있다. 이제 앞뒤가 딱딱 맞다!!