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. |
Chris wrote,
A minor point, but the RTTI directives won’t make a difference since RTTI isn’t generated for procedural code in the first place. Replacing the WriteLn calls with MessageBox ones gets out a couple KB though.
Link | April 6th, 2011 at 7:02 pm
tom conlon wrote,
Iztok – good & interesting article.
Thanks
Tom
Link | April 6th, 2011 at 7:05 pm
Iztok Kacin wrote,
@Cris
Good point, I missed that out. I will play with different options latter. Anyway thanks for the input. I tested with removing the RTTI directives and the size was the same as you said.
Link | April 6th, 2011 at 7:16 pm
Iztok Kacin wrote,
@Tom
Thanks, I try to write about something that is not all over the internet already. I thought it would be an interesting subject.
Link | April 6th, 2011 at 7:17 pm
tondrej wrote,
Just an idea: It seems that most of the size of the executable comes from SvcMgr using Forms, Dialogs etc. which drags in all the UI-related code, due to initialization of those units which doesn’t get stripped by the linker. That was fine in the old days of “interactive services” but is useless nowadays, so perhaps a new version of SvcMgr, similar to the current one where you can use data modules and components, but without using Forms would be useful and still not producing too big executables.
Link | April 7th, 2011 at 7:38 am
Iztok Kacin wrote,
@tondrej
I made a quick test. Removing the forms and dialogs reduces the size to 438 KB. Still large but way better then 1018 KB with forms and dialogs. But the SvcMgr.pas uses Application object for some exception handling and other things, so it would not be trivial to replace the functionality. I would say that if you need it small then go the pure API way. It is not to hard to do and the gain is really big (executable really small). Otherwise 500 KB or 1000 KB doesn’t matter in today desktop or server environment. Dead code that is not executed does not slow things down. Sure it is not clean, but there is no harm, just junk bytes.
Link | April 7th, 2011 at 8:32 am
Alex wrote,
> If enough people finds it important I can finish the code
> and polish it, but otherwise it is not worth the time.
May I ask if you ever continued with this code? I find it very, very interesting and useful. However, it’s absolutely unclear to me what needs to be done until this code can be used in productive environment. I’d be ver happy to see a version that you find to be stable to use. Thank you very much for all your work!
Link | December 3rd, 2011 at 4:30 pm
Alex wrote,
No reply at all?
Link | January 14th, 2012 at 2:17 am
Iztok Kacin wrote,
Sorry for not replying earlier. As far as I see the code is pretty much complete. You can use it, I don’t see any show stoppers
Link | January 16th, 2012 at 11:31 pm