Delphi编译的,EXE最小到底能有多少个字节?
很多人会用最简单的方式做一个“Hello World!”程序来给出答案。一一在Delphi7中,会是15K字节。如果使用对话框,则马上会变成382K字节。
这里不去讨论为什么会有这种变化。因为我已经迫不及待地要向你介绍Nico Bendlin和他写的MiniDExe了。
MiniDExe的纪录是3584字节!
1.1MiniDExe如何实现内核最小化
Delphi的单元隐含地引用了Systempas单元,那么System.pas单元自身如何被编译成.DCU呢?查看$(SourceRTLImakefile 文件可以看到,System.pas是通过这样的命令行来被编译的:
$(DCC) sys\system -y -s &(RTLBDBBUG) -n$(LTB)
这里最重要的就是隐含的编译选项:“-y”。它仅用于编译System.pas等被系统保留名字的内核单,因此在DCC32的命令行帮助中是根本没有的。绝大多数资料中也没有介绍它。
MiniDExe 就是通过重写、重编译 System.pas和Sys/nit.pas两个内核单元,全面摒弃了内核中的无关代码和多余变量、常量等,从而实现了最小化的。
1.1.1MiniDExe中的System.pas单元
unit System;
interface
procedure _InitExe;
procedure _HandleFinally;
procedure _halt0;
type
TGUID = record
D1: LongWord;
D2: Word;
D3: Word;
D4: array[0..7] of Byte;
end;
const
Kernel32 = 'kernel32.dll';
User32 = 'user32.dll';
var
HKernel32: LongWord;
HUser32: LongWord;
type
TFNExitProcess = procedure(uExitCode: LongWord); stdcall;
var
ExitCode: LongWord;
ExitProcess: TFNExitProcess;
function LoadLibraryA(lpLibFileName: PAnsiChar): LongWord; stdcall;
function LoadLibraryW(lpLibFileName: PWideChar): LongWord; stdcall;
function LoadLibrary(lpLibFileName: PChar): LongWord; stdcall;
function GetProcAddress(hModule: LongWord; lpProcName: PChar): Pointer; stdcall;
implementation
function LoadLibraryA; external kernel32 name 'LoadLibraryA';
function LoadLibraryW; external kernel32 name 'LoadLibraryW';
function LoadLibrary; external kernel32 name 'LoadLibraryA';
function GetProcAddress; external kernel32 name 'GetProcAddress';
procedure _InitExe;
const
PExitProcess: PChar = 'ExitProcess';
PKernelModul: PChar = Kernel32;
PUserModul: PChar = User32;
asm
PUSH PExitProcess
PUSH PUserModul
CALL LoadLibrary
MOV HUser32, EAX
PUSH PKernelModul
CALL LoadLibrary
MOV HKernel32, EAX
PUSH EAX
CALL GetProcAddress
MOV ExitProcess, EAX
end;
procedure _HandleFinally;
asm
end;
procedure _halt0;
asm
PUSH ExitCode
CALL ExitProcess
end;
end.
其中_InitExe()是编译一个可执行文件时需要嵌入的初始化代码,_HaltO()则是可执行文件退出时的出口代码。显然,这是操作系统调入一个可执行文件必须的入口和出口代码。
_HandleFinally()则是编译Sys/nit,pas时所必需的,它用于挂接异常处理过程。
编译器还须要在System.pas中查找与COM接口相关的数据类型TCUID。这使得可以在不改写编译器的情况下与当前的COM版本保持一致。
事实上,有了上述声明的部分,System.pas已经可以用于编译.EXE可执行文件了。但是,为了让它能够做一点点工作——显示一个对话框—Nico还必须继续为System.pas添上一些东西。
接下来定义了全局变量HKerne132与HUser32,用于保存OS为当前进程映射的Kernel32.dll和User32.dl/模块的句柄。这两个变量在_InitExec()例程中填入实际的值。全局常量Kerne132和User32仅用于定义上述两个模块的名字。
最后声明的是四个外部例程:
function LoadLibraryA(lpLibFileName: PAnsiChar): LongWord; stdcall;
function LoadLibraryW(lpLibFileName: PWideChar): LongWord; stdcall;
function LoadLibrary(lpLibFileName: PChar): LongWord; stdcall;
function GetProcAddress(hModule: LongWord; lpProcName: PChar): Pointer; stdcall;
它们都是在Kerne132模块中由操作系统实现的。如果用户的执行程序根本无须调用Kerne132与User32这两个模块中的任何例程,则上面的这些声明全都是可以省略的。
1.1.2MiniDExe中的Syslnit.pas单元
unit SysInit;
interface
var
TlsIndex: LongWord;
implementation
end.
这个单元中只剩下T1sIndex这个全局变量了,它用于保存一个线程TLS时隙的索引值。如果模块不定义线程局部变量,则T1sIndex不是必需的一—尽管会导致编译器的错误警告。
1.1.3MiniDExe中的项目文件MiniDExe.dpr
{$A+,B-,C-,D-,E-,F-,G+,H+,I-,J-,K-,L-,M-,N+,O+,P+,Q-,R-,S-,T-,U-,V+,W-,X+,Y-,Z1}
{$MINSTACKSIZE $00004000}
{$MAXSTACKSIZE $00100000}
{$IMAGEBASE $00400000}
{$APPTYPE GUI}
program MiniDExe;
type
TFNMessageBox = function(hWnd: LongWord; lpText, lpCaption: PChar;
uType: LongWord): Integer; stdcall;
const
MB_ICONINFORMATION = $00000040;
begin
TFNMessageBox(GetProcAddress(HUser32, 'MessageBoxA'))
(0, 'Written in pure Delphi!', 'Hello World!', MB_ICONINFORMATION);
end.
这个项目异常简单,是一个简单的Win32API的声明和调用,这样做是为了避免在System.pas中加入Writeln()这样的输出例程代码。
1.2一些其他的内核优化?
Delphi提供了全部的内核源码和VCL源码,这为一些个人或第三方组织优化它们提供了方便,其中最有名的两个组织是KOL和“High Performance Code(optimalcode.com)”。一些常见的第三方的内核优化代码有:
- 内部例程优化:optimalcode.com的FastMath,Andrew N.Driazgov(andrewdr@newmail.ru)的QStrings,以及droopyeyes.com的FastString。
- 内存管理器优化:在QStrings库中所包含的QMem,optimalcode.com发布的HPMM,以及用于替换ShareMem的单元 FastShareMem与ShareMemRep。
- 异常管理器优化:KOL发布的异常处理单元err.pas。
- 系统单元的整体优化:KOL发布的System units replacement for D4-D7和Assarbad's system unit replace。
KOL目前还在维护着各个Delphi版本的内核单元替换,并且已经对SysUtils.pas、Classes.pas、Variants.pas和Math.pas等单元进行了优化。而optimalcode.com站点如今已经关闭了。
也有一些非常优秀的、以个人名义维护的代码优化项目。例如Assarbad就发布过一个基于Delphi4优化的内核单元(但是更确切地说,Assarbad并不注重对System单元的优化,他所关注的其实只是简化)。由Dennis Christensen负责的项目Fast Code Project也非常有名,目前的网址是http://dennishomepage.gugs-cats.dk/FastCodeProject.htm,它甚至替代了optimalcode.com的位置。
此外还有一些散见于网络的内核例程的优化代码。例如这样的一个例程:
//same of function Math. Sing()
function Sign(n: Integer): Integer;
//returns -1 for n<0
// 0 for n = 0
// 1 for n > 0
asm
or eax, eax
lahf
movzx eax, ah
shr eax,6sub
eax,1
neg eax
end;
1.3为什么要研究最小化内核
回到MiniDExe。
起来没有必要把System.pas简化到如此之小—因为类似于MiniDExe中那样简化的内核儿乎付么也做不了。但是,如果要从源代码一级分析Delphi是如何将对象、组件库、接口等框架技术包裹在应用程序之上的,从一个最小化的内核开始分析,是最方使、最清晰不过的了。
System.pas展示了Delphi如何将自己的应用挂在操作系统中。这一点也很重要。因为,以此为原点,将更易窥见Delphi是如何实现多线程、内存管理、文件系统以及其他各种数据结构的。
自下面的章节开始,我将在这个内核之上,逐层包装代码,从而将Delphi内核的每个实现细节具体展现在您的面前。