Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

コンパイラが自身のバージョンをEXEに埋め込むようになっていた件。

きっかけ

ふと「最新コンパイラで小さいEXEってまだ作れるのかな?」と気になり、XE2の頃に試したコードを再コンパイルしたところ、当時3,584バイトだったEXEが4,608バイトに増えてしまっていました。で、原因を調べるためにとりあえずバイナリエディタで開いたところ、こんなデータが。

いつから?

少なくとも公式情報としては出ていない感じだったので、仕方なく少しずつバージョンを下げつつコンパイルを試したところ、XE7で搭載されたことが判明。バイナリ内の位置としてはPE形式でいうところの.rdataセクション内にあります。

お前を消す方法

さらに調べたところ、--no-compiler-signatureというUndocumentedなコンパイラオプションを発見し、こいつをDCC32に投げてやったところ見事にバージョン情報が消えました。これでようやく前と同じサイズのバイナリが生成される…と思いきや、出力されたEXEサイズを見てみると4,096バイト…。

.relocも増えてた

以前のバイナリと見比べたところ、バージョン情報以外にも.relocセクションが増えていました。.relocセクションというのはリロケーションテーブルとも呼ばれるもので、EXEやDLLが指定したベースアドレスにロードできなかった時にアドレス情報を再配置するための情報です。
この情報は以前はDLLだけが持てば良かったんですが*1ASLRに対応した影響かデフォルトでEXEもリロケーションテーブルを持つようになってしまったみたいです。
ASLRのオンオフにかかわらず生成されるこいつに関しては今のところ消し去る方法が見つからないので、最新バージョンでの最小EXEサイズは4,096バイトが限界になってしまったみたいです。まあこのサイズであればギリギリ4KBと言えるからいいか…。

ちなみに

コンパイラのバージョンが上がるにつれてEXEのサイズが大きくなるのは「RTTIのサイズが大きいからだ」と言われることが多いですけど、実際に調べた人って見たことないですよね。ということでついでなので調べてみました。
RTTIのサイズはTRttiType.RttiDataSizeでわりと簡単に取れます。あとはTRttiContext.GetTypesで列挙して合計してやれば取れそうですが、このRttiDataSizeが指すのはTTypeInfoのサイズ(必要最小限のTTypeDataも含む)なので、これを指すPTypeInfoのサイズ、さらにそれを指すPPTypeInfoのサイズも考慮してやる必要があります。
PTypeInfoは各TTypeInfoの直前にあります。なので(Win32の場合)4 + RttiDataSizeになります。ところがRTTIは全部詰めて配置されているわけではなく、4バイト境界に合わせて配置されているので、パディング分も考慮する必要があります。また、PPTypeInfoは一括でドンと確保されているんですが、型の数だけではなく、ユニット毎に分けるためのセパレータデータもユニット数-1配置されています。そしてユニット毎にユニット名データもRTTIとしてあります。ということでこれらをまとめたコードが以下になります。厳密にやるならSystem.Rtti分を省いたりパッケージ分のちょっとしたデータを足したりする必要がありますが、今回はとりあえずざっくり。

function GetRttiDataSize: Integer;
var
  ctx: TRttiContext;
  typ: TRttiType;
  lib: PLibModule;
  i: Integer;
  p: PByte;
begin
  Result := 0;
  for typ in ctx.GetTypes do
    Inc(Result , (SizeOf(PTypeInfo) + typ.RttiDataSize + SizeOf(Pointer) - 1) and not (SizeOf(Pointer) - 1));

  lib := LibModuleList;
  while lib <> nil do
  begin
    if lib^.TypeInfo <> nil then
    begin
      Inc(Result, SizeOf(Pointer) * lib^.TypeInfo.TypeCount);
      p := PByte(lib^.TypeInfo.UnitNames);
      for i:= 0 to lib^.TypeInfo.UnitCount-1 do
      begin
        Inc(Result, 1 + p^);
        Inc(p, 1 + p^);
      end;
    end;
    lib := lib^.Next;
  end;
end;

この関数を新規作成したVCLアプリケーションで実行してやると、10.1 Berlin上では98,376バイトになりました。約100KBなので結構でかいですが、そもそもこのEXEサイズが2,196,480バイトもあることを考えると、サイズに占める割合としては割と低くも感じます。ということでおまけでした。

さいごに

最小EXEなんて実用性ゼロなので誰も興味ないと思いますが、一応Win32/64両対応したものを置いておきます。動作確認は10.1 Berlin上で行ってます。64bit版は残念ながらちょっとでかい(4,608バイト)です。
ダウンロード

*1:EXEは一番最初にロードされるので基本的に再配置は起きない