TL;DR: the gist is roughly the same as commented before on the question. WinMainCRTStartup
is the function which performs some boilerplate initialization for the C/C++ runtime ("CRT") and eventually calls your WinMain
. So the former isn't your own code, but code supplied by the compiler toolchain and the latter gets eventually called by the former. The leading underscore(s) originate from name mangling.
Frankly you are giving too little context, but based on experience I will fill the gaps with assumptions 😉
You may want to have a look at this answer first and this answer second, I'll wait.
Okay, so not assuming too much prior knowledge (i.e. in case you skipped the reading part), you should know that we have several levels here.
- The PE optional header has a field
IMAGE_OPTIONAL_HEADER::AddressOfEntryPoint
which contains the RVA of the entry point to the software
- in your C/C++ program -- and further assuming MSVC based on the names from your question -- the
main
or wmain
(but I'll subsume them under "main
") function for console programs or WinMain
/wWinMain
for GUI programs, respectively, is what you perceive as the entry point to your program, but it's not prior to your the C/C++ runtime (msvcrt.dll or the newer msvcr1xx.dll) takes control (in case of statically linked C/C++ runtime, this boilerplate code gets embedded into your binary).
We can prove this easily. Suppose we take a trivial C++ program like this (go ahead, follow along):
// File is named test.cpp
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow)
{
return MessageBox(NULL, "Hello, world", "Hellow Windows world", 0);
}
And compile it from a Visual C++/Studio "Developer Command Prompt" (32-bit/x86 default) with:
cl /nologo /permissive- /Zi /Fe:test.exe test.cpp /link /opt:ref /opt:icf user32.lib
/Zi
removes optimizations as it implies /debug
, so we counter with /opt:ref
and /opt:icf
passed directly to the linker.
Looking at the resulting test.exe
first with dumpbin /nologo /headers test.exe
from the same prompt you should be seeing something along the lines of (excerpt):
OPTIONAL HEADER VALUES
10B magic # (PE32)
14.34 linker version
C600 size of code
8400 size of initialized data
0 size of uninitialized data
125C entry point (0040125C) _WinMainCRTStartup
1000 base of code
The penultimate line contains the reference to _WinMainCRTStartup
which is set as entry point for the PE file. Exactly what you are seeing, right?
Well, turns out if you have the Professional edition or higher of Visual Studio, you have the source code to this stuff probably installed already (it's still proprietary code!). It's inside $(VCToolsInstallDir)\crt
($(VCToolsInstallDir)
can be retrieved by invoking "%Program Files(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -nologo -property installationPath
and drilling down into VC\Tools\MSVC\*
).
Now you can find the files for the entry point(s):
- Console subsystem,
_UNICODE
not defined:$(VCToolsInstallDir)\crt\src\vcruntime\exe_main.cpp
- Console subsystem,
_UNICODE
defined: $(VCToolsInstallDir)\crt\src\vcruntime\exe_wmain.cpp
- GUI ("windows") subsystem,
_UNICODE
not defined: $(VCToolsInstallDir)\crt\src\vcruntime\exe_winmain.cpp
- GUI ("windows") subsystem,
_UNICODE
defined: $(VCToolsInstallDir)\crt\src\vcruntime\exe_wwinmain.cpp
So in your case we're looking at a GUI program without _UNICODE
defined ("ANSI entry point").
Loading this file into IDA we will land right at _WinMain@16
:

But pay attention to the Exports view which I added here as well. We can see the _WinMainCRTStartup
. That function is defined by Visual Studio and the linker is hardcoded to "know" about a bunch of names for entry points, this one included, by default.
The code (also seen in exe_winmain.cpp
) amounts to:
extern "C" DWORD WinMainCRTStartup(LPVOID)
{
return __scrt_common_main();
}
And the extern "C"
is what causes the name to be mangled and prepended an underscore.
It's worthwhile peeking at the code for the C/C++ runtime startup code yourself and getting to know it. Potentially the thread local storage (TLS) callbacks also get to run prior to your entry point.
Bonus exercise
NB: A prior look at The Old New Thing can't hurt 😉
Alright, let's swap the contents of the test.cpp
for this one:
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
DWORD CALLBACK TrueEntryPoint(void)
{
(void)::MessageBeep(0xFFFFFFFFU);
return 42;
}
(Note: the choice of MessageBeep
was deliberate so we could rebuild still with only importing from user32.dll
.)
... and rebuild, this time telling the linker to use TrueEntryPoint
as the PE entry point:
cl /nologo /permissive- /Zi /Fe:test.exe test.cpp /link /opt:ref /opt:icf user32.lib /entry:TrueEntryPoint
Executing the code you should now hear a beep (speaker) or some short sound which you also get otherwise in the GUI on occasion.
But wait, the binary is now 2.5 KiB in size, as compared to a staggering 81.0 KiB for the original binary.
That size difference is owed to all the C/C++ runtime functionality such as printf
, but also some default exception handling, buffer overrun checks etc. Some may consider it bloat, but if you don't have it you aren't allowed to call any of the C/C++ runtime functionality without first performing the initialization steps yourself.
Note that if we hadn't statically linked the C/C++ runtime but instead used it dynamically, things would be slightly different, as all of that "bloat" would now be in the C/C++ runtime DLLs which hopefully only exist once (per version) on a given system. But it requires you to install the "Runtime Redistriutables" in order to run your application.
WinMainCRTStartup
Is provided by the CRT and gives the CRT a chance to do it's initialization; the CRT will then call yourWinMain
when it is ready.main
function written by the application developer.