How to make a very small windows service executable
I a recent stack overflow question, a user asked how to make a Delphi generated NT Service executable smaller. There was some debate, if Delphi was appropriate to do this and if the executable size it generates was to big. Let me answer those two question as I see them:
- The size is not to big. In today desktop environment a megabyte, or few of them don’t matter. The difference between 50K and 1MB executable just doesn’t matter. There is enough RAM and hard drive space, that very few situations need a smaller executable.
- And in cases when you need that, you can get to that in Delphi. You don’t need C/C++. Sure you can make the same result there, but why? Even if you only use API calls, you can still reuse some of the low level code you have written in Delphi. And that is a good enough reason to do it in your known IDE and language. You already know your tool and can save a lot of time not writing support code you already have written.
Ok, so I dusted off a very old example I made years back and recompiled it with the Delphi 2006 and Delphi XE compiler. (I have those currently at hand). The result are:
- Delphi 2006: 526 KB
- Delphi XE: 1018 KB
These are the result of opening a new service application and just clicking build. No settings were adjusted. Then I cleaned that old example a little removed the unneeded code and made sure it works (I tested the service). The results were the following:
- Delphi 2006: 22 KB
- Delphi XE: 32 KB
Here I striped XE version of RTTI as suggested on stack overflow (actually as there is only procedural code the RTTI has no effect whatsoever on the code as noticed by Cris). I left the Delphi 2006 intact. The code I used is posted bellow. It is basically a service skeleton made of two parts. One is the part that installs and controls the service. And the other is the service code itself that spawns a new “service control manager” attached process and then just waits until it is told to stop. It also reports the status to SCM.
And now the challenge. How small can you make a static linked, single c/c++ executable service. It would be fun to know. I will play more with this latter trying different compiler settings and other tricks to see it I can squeeze a byte or two out of my current size
A word of advice. Do not just copy and paste the sample service skeleton. It is just a prototype to show what can be done. It is in no way complete and does not have good error coverage. If enough people finds it important I can finish the code and polish it, but otherwise it is not worth the time.
{ NT Service model based completely on API calls. Version 0.1 Inspired by NT service skeleton from Aphex Adapted by Runner } program PureAPIService; {$APPTYPE CONSOLE} uses Windows, WinSvc; const ServiceName = 'PureAPIService'; DisplayName = 'Pure Windows API Service'; NUM_OF_SERVICES = 2; var ServiceStatus : TServiceStatus; StatusHandle : SERVICE_STATUS_HANDLE; ServiceTable : array [0..NUM_OF_SERVICES] of TServiceTableEntry; Stopped : Boolean; Paused : Boolean; var ghSvcStopEvent: Cardinal; procedure OnServiceCreate; begin // do your stuff here; end; procedure AfterUninstall; begin // do your stuff here; end; procedure ReportSvcStatus(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD); begin // fill in the SERVICE_STATUS structure. ServiceStatus.dwCurrentState := dwCurrentState; ServiceStatus.dwWin32ExitCode := dwWin32ExitCode; ServiceStatus.dwWaitHint := dwWaitHint; case dwCurrentState of SERVICE_START_PENDING: ServiceStatus.dwControlsAccepted := 0; else ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP; end; case (dwCurrentState = SERVICE_RUNNING) or (dwCurrentState = SERVICE_STOPPED) of True: ServiceStatus.dwCheckPoint := 0; False: ServiceStatus.dwCheckPoint := 1; end; // Report the status of the service to the SCM. SetServiceStatus(StatusHandle, ServiceStatus); end; procedure MainProc; begin // we have to do something or service will stop ghSvcStopEvent := CreateEvent(nil, True, False, nil); if ghSvcStopEvent = 0 then begin ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); Exit; end; // Report running status when initialization is complete. ReportSvcStatus( SERVICE_RUNNING, NO_ERROR, 0 ); // Perform work until service stops. while True do begin // Check whether to stop the service. WaitForSingleObject(ghSvcStopEvent, INFINITE); ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); Exit; end; end; procedure ServiceCtrlHandler(Control: DWORD); stdcall; begin case Control of SERVICE_CONTROL_STOP: begin Stopped := True; SetEvent(ghSvcStopEvent); ServiceStatus.dwCurrentState := SERVICE_STOP_PENDING; SetServiceStatus(StatusHandle, ServiceStatus); end; SERVICE_CONTROL_PAUSE: begin Paused := True; ServiceStatus.dwcurrentstate := SERVICE_PAUSED; SetServiceStatus(StatusHandle, ServiceStatus); end; SERVICE_CONTROL_CONTINUE: begin Paused := False; ServiceStatus.dwCurrentState := SERVICE_RUNNING; SetServiceStatus(StatusHandle, ServiceStatus); end; SERVICE_CONTROL_INTERROGATE: SetServiceStatus(StatusHandle, ServiceStatus); SERVICE_CONTROL_SHUTDOWN: Stopped := True; end; end; procedure RegisterService(dwArgc: DWORD; var lpszArgv: PChar); stdcall; begin ServiceStatus.dwServiceType := SERVICE_WIN32_OWN_PROCESS; ServiceStatus.dwCurrentState := SERVICE_START_PENDING; ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP or SERVICE_ACCEPT_PAUSE_CONTINUE; ServiceStatus.dwServiceSpecificExitCode := 0; ServiceStatus.dwWin32ExitCode := 0; ServiceStatus.dwCheckPoint := 0; ServiceStatus.dwWaitHint := 0; StatusHandle := RegisterServiceCtrlHandler(ServiceName, @ServiceCtrlHandler); if StatusHandle <> 0 then begin ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0); try Stopped := False; Paused := False; MainProc; finally ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); end; end; end; procedure UninstallService(const ServiceName: PChar; const Silent: Boolean); const cRemoveMsg = 'Your service was removed sucesfuly!'; var SCManager: SC_HANDLE; Service: SC_HANDLE; begin SCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS); if SCManager = 0 then Exit; try Service := OpenService(SCManager, ServiceName, SERVICE_ALL_ACCESS); ControlService(Service, SERVICE_CONTROL_STOP, ServiceStatus); DeleteService(Service); CloseServiceHandle(Service); if not Silent then MessageBox(0, cRemoveMsg, ServiceName, MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST); finally CloseServiceHandle(SCManager); AfterUninstall; end; end; procedure InstallService(const ServiceName, DisplayName, LoadOrder: PChar; const FileName: string; const Silent: Boolean); const cInstallMsg = 'Your service was Installed sucesfuly!'; cSCMError = 'Error trying to open SC Manager'; var SCMHandle : SC_HANDLE; SvHandle : SC_HANDLE; begin SCMHandle := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS); if SCMHandle = 0 then begin MessageBox(0, cSCMError, ServiceName, MB_ICONERROR or MB_OK or MB_TASKMODAL or MB_TOPMOST); Exit; end; try SvHandle := CreateService(SCMHandle, ServiceName, DisplayName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_IGNORE, pchar(FileName), LoadOrder, nil, nil, nil, nil); CloseServiceHandle(SvHandle); if not Silent then MessageBox(0, cInstallMsg, ServiceName, MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST); finally CloseServiceHandle(SCMHandle); end; end; procedure WriteHelpContent; begin WriteLn('To install your service please type /install'); WriteLn('To uninstall your service please type /remove'); WriteLn('For help please type /? or /h'); end; begin if (ParamStr(1) = '/h') or (ParamStr(1) = '/?') then WriteHelpContent else if ParamStr(1) = '/install' then InstallService(ServiceName, DisplayName, 'System Reserved', ParamStr(0), ParamStr(2) = '/s') else if ParamStr(1) = '/remove' then UninstallService(ServiceName, ParamStr(2) = '/s') else if ParamCount = 0 then begin OnServiceCreate; ServiceTable[0].lpServiceName := ServiceName; ServiceTable[0].lpServiceProc := @RegisterService; ServiceTable[1].lpServiceName := nil; ServiceTable[1].lpServiceProc := nil; StartServiceCtrlDispatcher(ServiceTable[0]); end else WriteLn('Wrong argument!'); end.