Introduction
This is a Small Application Wizard for Visual Studio .NET. It helps you build small executables with just a few clicks. The downloadable installer was designed for Visual C++ 8, I haven't tested it on earlier versions. If you just want to start using this wizard, install it and click on Small Application in the Visual Studio's New Project window. Just like this:
If you're interested in knowing more details about this little project, just keep reading.
Let's consider this article an update to Matt Pietrek's one about his tiny libc. I never really cared about the size of executables produced by Visual Studio, until I had to build some small ones for work. So, basically, I wrote my own small libc including support for Unicode, secure functions, x64, and Itanium. When I was done with that job, I didn't want to leave the files getting old on my hard drive. So, I decided to add some functions to my small libc and create a wizard to make the whole task as easy as possible. As you can imagine, this article is not to be taken too seriously.
Small LibC
I won't post the code of the libc, since it's useless. These are the supported functions:
Header |
Functions |
stdio.h |
- ANSI:
printf puts scanf gets
- ANSI Secure:
gets_s
- Unicode:
wprintf _putws wscanf _getws
- Unicode Secure:
_getws_s |
stdlib.h |
new delete malloc free calloc realloc |
string.h |
memset memcpy memmove memcmp memchr memcpy_s
- ANSI:
strlen strcpy strncpy strcat strncat strcmp strncmp _stricmp strupr strlwr strchr strstr strtol strtoul _splitpath
- ANSI Secure:
strcpy_s strncpy_s strcat_s strncat_s _splitpath_s
- Unicode:
wcslen wcscpy wcsncpy wcscat wcsncat wcscmp wcsncmp _wcsicmp wcsupr wcslwr wcschr wcsstr wcstol wcstuol _wsplitpath
- Unicode Secure:
wcscpy_s wcsncpy_s wcscat_s wcsncat_s _wsplitpath_s |
For best optimization, it's not advisable to use functions like scanf
. I just put some stdio functions in my libc to make small console projects work.
Basically, I took half of the string functions from Microsoft's SDK. I believe I took splitpath
from wine (I don't remember for sure) and strol
/ strtoul
from somewhere on the web. The other functions I had to write by myself.
A lot of string functions are just wrappers to Windows APIs.
For example:
extern "C" size_t __cdecl strlen(const char *str)
{
#ifndef AVOID_IF_POSSIBLE_WINAPI
return (size_t) lstrlenA(str);
#else
const char *eos = str;
while (*eos++) ;
return (eos - str - 1);
#endif
}
If for whatever reason you don't want to use Windows APIs whenever possible (it can't be always avoided), just define AVOID_IF_POSSIBLE_WINAPI
, and no external function will be used. This isn't the best to reduce the executable's size, but it might be useful if you want to make the disassembled code a little bit harder to understand or to avoid simple breakpoints on APIs. If you didn't understand what I just said, forget the whole point.
Stub Code
Windows executables have always a different entry point from what you usually see. Three common entry points are:
int WINAPI _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR szCmdLine, int iCmdShow);
BOOL WINAPI DllMain(HINSTANCE hInstance,
DWORD fdwReason, LPVOID lpvReserved);
int _tmain(int argc, _TCHAR* argv[]);
But the actual ones are:
#ifdef UNICODE
extern "C" int WINAPI wWinMainCRTStartup(void)
#else
extern "C" int WINAPI WinMainCRTStartup(void)
#endif
extern "C" BOOL WINAPI _DllMainCRTStartup(HINSTANCE hInstance,
DWORD fdwReason, LPVOID lpReserved)
#ifdef UNICODE
extern "C" int WINAPI wmainCRTStartup(void)
#else
extern "C" void __cdecl mainCRTStartup(void)
#endif
In the case of the Win32 EXE and the console program, the actual entry point has to get the command line through GetCommandLine
and parse it. I used Pietrek's entries, since they were already working, no need to write new ones.
Setting Up a Visual Studio Project
C/C++ -> Optimization:
Minimize Size and Favor Small Code are quite easy to understand. What's to say is that I disabled the Whole Program Optimization because it didn't allow me to use my own libc.
C/C++ -> Code Generation:
The Struct Member Alignment is easy to understand. I had to disable the Buffer Security Check in order to use my own libc (and, of course, disabling it reduces size anyway).
Linker -> Input:
Ignore Default Libraries is to ignore the default libc. Additional Dependencies tells the linker to use my small libc. Of course, in case you'll need to compile for x64 or Itanium, you've to replace small_libc_x86.lib with small_libc_x64.lib or small_libc_Itanium.lib.
Linker -> Debugging:
I disabled the Debug Info in order to reduce size, but it shouldn't be in a release anyway. It's that information string about the debug info file which is put into your executable:
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F Ascii
00009250 00 00 00 00 00 00 00 00 00 00 00 00 58 AA 40 00 ............X�@.
00009260 F0 92 40 00 03 00 00 00 52 53 44 53 87 ED 80 26 �@.>...RSDS��&
00009270 95 9A 57 47 8D 75 B2 DA E2 1F F3 4B 02 00 00 00 ��WG�u����K<...
00009280 63 3A 5C 64 6F 63 75 6D 65 6E 74 73 20 61 6E 64 c:\documents.and
00009290 20 73 65 74 74 69 6E 67 73 5C 6E 74 6F 73 6B 72 .settings\ntoskr
000092A0 6E 6C 5C 64 6F 63 75 6D 65 6E 74 69 5C 76 69 73 nl\documenti\vis
000092B0 75 61 6C 20 73 74 75 64 69 6F 20 70 72 6F 6A 65 ual.studio.proje
000092C0 63 74 73 5C 73 6D 61 6C 6C 20 65 78 65 5C 72 65 cts\small.exe\re
000092D0 6C 65 61 73 65 5C 53 6D 61 6C 6C 20 45 78 65 2E lease\Small.Exe.
000092E0 70 64 62 00 00 00 00 00 00 00 00 00 00 00 00 00 pdb.............
It doesn't respect the programmer's privacy as well (be aware: it's put by default into all your executables, even .NET ones).
Optimization Results
Here's a small table to compare sizes between minimal EXEs produced by default linking and the ones produced with this wizard:
Project Type |
Win32 EXE |
Win32 DLL |
Console |
Default x86 |
48,00 KB |
52,00 KB |
48,00 KB |
Small x86 |
2,50 KB |
2,00 KB |
2,50 KB |
Default x64 |
44,00 KB |
45,50 KB |
44,00 KB |
Small x64 |
3,00 KB |
2,00 KB |
3,00 KB |
Default Itanium |
90,50 KB |
95,00 KB |
90,00 KB |
Small Itanium |
4,00 KB |
3,50 KB |
5,00 KB |
This seems pretty acceptable, doesn't it?
The Wizard
This is the J# code of the wizard:
function OnFinish(selProj, selObj)
{
try
{
var strProjectPath = wizard.FindSymbol('PROJECT_PATH');
var strProjectName = wizard.FindSymbol('PROJECT_NAME');
selProj = CreateCustomProject(strProjectName, strProjectPath);
AddConfig(selProj, strProjectName);
AddFilters(selProj);
var InfFile = CreateCustomInfFile();
AddFilesToCustomProj(selProj, strProjectName,
strProjectPath, InfFile);
PchSettings(selProj);
InfFile.Delete();
selProj.Object.Save();
}
catch(e)
{
if (e.description.length != 0)
SetErrorInfo(e);
return e.number
}
}
function CreateCustomProject(strProjectName, strProjectPath)
{
try
{
var strProjTemplatePath =
wizard.FindSymbol('PROJECT_TEMPLATE_PATH');
var strProjTemplate = '';
strProjTemplate = strProjTemplatePath + '\\default.vcproj';
var Solution = dte.Solution;
var strSolutionName = "";
if (wizard.FindSymbol("CLOSE_SOLUTION"))
{
Solution.Close();
strSolutionName = wizard.FindSymbol("VS_SOLUTION_NAME");
if (strSolutionName.length)
{
var strSolutionPath = strProjectPath.substr(0,
strProjectPath.length - strProjectName.length);
Solution.Create(strSolutionPath, strSolutionName);
}
}
var strProjectNameWithExt = '';
strProjectNameWithExt = strProjectName + '.vcproj';
var oTarget = wizard.FindSymbol("TARGET");
var prj;
if (wizard.FindSymbol("WIZARD_TYPE") == vsWizardAddSubProject)
{
var prjItem = oTarget.AddFromTemplate(strProjTemplate,
strProjectNameWithExt);
prj = prjItem.SubProject;
}
else
{
prj = oTarget.AddFromTemplate(strProjTemplate,
strProjectPath, strProjectNameWithExt);
}
return prj;
}
catch(e)
{
throw e;
}
}
function AddFilters(proj)
{
try
{
var strSrcFilter = wizard.FindSymbol('SOURCE_FILTER');
var group = proj.Object.AddFilter('Source Files');
group.Filter = strSrcFilter;
}
catch(e)
{
throw e;
}
}
function AddConfig(proj, strProjectName)
{
try
{
var ProjType = wizard.FindSymbol('LST_PROJECT');
var bUseUnicode = wizard.FindSymbol('RB_UNICODE');
var config = proj.Object.Configurations('Debug');
config.IntermediateDirectory = '$(ConfigurationName)';
config.OutputDirectory = '$(ConfigurationName)';
if (ProjType != 'Win32Dll')
config.ConfigurationType = 1;
else
config.ConfigurationType = 2;
if (bUseUnicode == true)
config.CharacterSet = 1;
else
config.CharacterSet = 0;
var CLTool = config.Tools('VCCLCompilerTool');
CLTool.Optimization = 0;
CLTool.MinimalRebuild = true;
CLTool.BasicRuntimeChecks = 3;
CLTool.RuntimeLibrary = 0;
CLTool.UsePrecompiledHeader = 0;
CLTool.WarningLevel = 3;
CLTool.Detect64BitPortabilityProblems = true;
CLTool.DebugInformationFormat = 4;
if (ProjType == 'Win32Exe')
CLTool.PreprocessorDefinitions = 'WIN32;_DEBUG;_WINDOWS';
else if (ProjType == 'Console')
CLTool.PreprocessorDefinitions = 'WIN32;_DEBUG;_CONSOLE';
else
CLTool.PreprocessorDefinitions = 'WIN32;_DEBUG;_WINDOWS;_USRDLL';
var LinkTool = config.Tools('VCLinkerTool');
LinkTool.LinkIncremental = 2;
LinkTool.GenerateDebugInformation = true;
if (ProjType != 'Console')
LinkTool.SubSystem = 2;
else
LinkTool.SubSystem = 1;
LinkTool.TargetMachine = 1;
config = proj.Object.Configurations('Release');
config.IntermediateDirectory = '$(ConfigurationName)';
config.OutputDirectory = '$(ConfigurationName)';
if (ProjType != 'Win32Dll')
config.ConfigurationType = 1;
else
config.ConfigurationType = 2;
if (bUseUnicode == true)
config.CharacterSet = 1;
else
config.CharacterSet = 0;
var CLTool = config.Tools('VCCLCompilerTool');
CLTool.Optimization = 1;
CLTool.FavorSizeOrSpeed = 2;
CLTool.WholeProgramOptimization = false;
CLTool.RuntimeLibrary = 0;
CLTool.StructMemberAlignment = 1;
CLTool.BufferSecurityCheck = false;
CLTool.UsePrecompiledHeader = 0;
CLTool.WarningLevel = 3;
CLTool.Detect64BitPortabilityProblems = true;
CLTool.DebugInformationFormat = 3;
if (ProjType == 'Win32Exe')
CLTool.PreprocessorDefinitions = 'WIN32;NDEBUG;_WINDOWS';
else if (ProjType == 'Console')
CLTool.PreprocessorDefinitions = 'WIN32;NDEBUG;_CONSOLE';
else
CLTool.PreprocessorDefinitions = 'WIN32;NDEBUG;_WINDOWS;_USRDLL';
var LinkTool = config.Tools('VCLinkerTool');
LinkTool.AdditionalDependencies = 'small_libc_x86.lib';
LinkTool.LinkIncremental = 1;
LinkTool.IgnoreAllDefaultLibraries = true;
LinkTool.GenerateDebugInformation = false;
if (ProjType != 'Console')
LinkTool.SubSystem = 2;
else
LinkTool.SubSystem = 1;
LinkTool.OptimizeReferences = 2;
LinkTool.EnableCOMDATFolding = 2;
LinkTool.TargetMachine = 1;
}
catch(e)
{
throw e;
}
}
function PchSettings(proj)
{
}
function DelFile(fso, strWizTempFile)
{
try
{
if (fso.FileExists(strWizTempFile))
{
var tmpFile = fso.GetFile(strWizTempFile);
tmpFile.Delete();
}
}
catch(e)
{
throw e;
}
}
function CreateCustomInfFile()
{
try
{
var fso, TemplatesFolder, TemplateFiles, strTemplate;
fso = new ActiveXObject('Scripting.FileSystemObject');
var TemporaryFolder = 2;
var tfolder = fso.GetSpecialFolder(TemporaryFolder);
var strTempFolder = tfolder.Drive + '\\' + tfolder.Name;
var strWizTempFile = strTempFolder + "\\" + fso.GetTempName();
var strTemplatePath = wizard.FindSymbol('TEMPLATES_PATH');
var strInfFile = strTemplatePath + '\\Templates.inf';
wizard.RenderTemplate(strInfFile, strWizTempFile);
var WizTempFile = fso.GetFile(strWizTempFile);
return WizTempFile;
}
catch(e)
{
throw e;
}
}
function GetTargetName(strName, strProjectName)
{
try
{
// TODO: set the name of the rendered
// file based on the template filename
var strTarget = strName;
if (strName == 'readme.txt')
strTarget = 'ReadMe.txt';
if (strName == 'main.h')
strTarget = strProjectName + '.h';
if (strName == 'main.cpp')
strTarget = strProjectName + '.cpp';
if (strName == 'crt.cpp')
strTarget = strProjectName + ' CRT.cpp';
return strTarget;
}
catch(e)
{
throw e;
}
}
function AddFilesToCustomProj(proj,
strProjectName, strProjectPath, InfFile)
{
try
{
var projItems = proj.ProjectItems
var strTemplatePath = wizard.FindSymbol('TEMPLATES_PATH');
var strTpl = '';
var strName = '';
var strTextStream = InfFile.OpenAsTextStream(1, -2);
while (!strTextStream.AtEndOfStream)
{
strTpl = strTextStream.ReadLine();
if (strTpl != '')
{
strName = strTpl;
var strTarget = GetTargetName(strName, strProjectName);
var strTemplate = strTemplatePath + '\\' + strTpl;
var strFile = strProjectPath + '\\' + strTarget;
var bCopyOnly = false;
var strExt = strName.substr(strName.lastIndexOf("."));
if(strExt==".bmp" || strExt==".ico" ||
strExt==".gif" || strExt==".rtf" ||
strExt==".css" || strExt==".lib")
bCopyOnly = true;
wizard.RenderTemplate(strTemplate, strFile, bCopyOnly);
if (strExt != ".lib")
proj.Object.AddFile(strFile);
}
}
strTextStream.Close();
}
catch(e)
{
throw e;
}
}
There are already a lot of articles on how to make a Custom Wizard for Visual Studio, so any further explanation is quite useless. I just put the code in the article to let it be ridden by someone who's not interested in downloading the attachment.
The installer just puts all the files in the right directories, there's nothing unusual in the whole process.
Final Considerations
More optimization can still be reached. I was thinking about a post-build PE optimizer...
I'd like to say a last thing about this whole subject. Reducing executable size isn't that important; most of the time, it's just playing around. The default libc makes executables very big, but the added code is not useless code. Reduce your executable size only if necessary.
Goodbye!