HOME | プログラミングTClock |


このページの情報は、031018以降のソースコードに基づいています。

実行ファイルのサイズを小さくする

TClockでは、「デフォルトライブラリ(ランタイムライブラリ)をリンクしないことによって、実行ファイルのサイズを小さくする」というテクニックを使っています。TClock LightのMakefileやソースの#ifdefで何やってるのか詳しく知りたいときは、このページの情報を参照してください。

このテクニックは、「実行ファイルが小さいほうが気持ちいい」という作者の単なる好みによるもので、ふつうのC言語でふつうにプログラムする際には、役に立たない、覚えなくてよいものです。

TClock Lightのソースを改良したい人は、Makefileを編集して、デフォルトライブラリをリンクする設定に変えることをお勧めします。TClock LightのMakefileは、デフォルトライブラリをリンクするかどうかを、簡単に切り替えられるようにしてあります。

Visual C++では、次の行を削除します。

NODEFLIBOPT=NODEFAULTLIB=1

BCCでは、次の行を削除します。

NODEFLIBOPT=-D NODEFAULTLIB=1

デフォルトライブラリをリンクすると、tclock.exe、tcdll.tclockなどのサイズが大きくなりますが、動作に支障はありません。ちなみに、TClock2chは、tclock.exeではデフォルトライブラリをリンクせず、tcdll.tclockではデフォルトライブラリをリンクしています。

できなくなること

デフォルトライブラリをリンクしないと、次のことができなくなります。

「そこまでしてファイルサイズを小さくしたいのか」と言われたら、言い返せませんな。

マクロNODEFAULTLIB

TClock Lightのソースでは、マクロNODEFAULTLIBがあればデフォルトライブラリを使わず、なければデフォルトライブラリを使う、としています。

#ifdef NODEFAULTLIB

/* デフォルトライブラリを使わない場合のコード */

#else

/* デフォルトライブラリを使う場合のコード */

#endif

さて、物好きな人は、ここから先もお読みください。

デフォルトライブラリのリンク

objファイルをリンクして実行ファイルを作るときには、objファイルだけでなく、C言語の処理系が用意しているデフォルトライブラリがリンクされます。デフォルトライブラリには、C言語の標準関数のコードなどが含まれます。デフォルトライブラリは、Visual C++ではLIBC.LIBなど、BCCではCW32.LIBなどです。

デフォルトライブラリのリンクの図

このデフォルトライブラリをリンクしないようにするには、Visual C++ではリンカのオプションに /NODEFAULTLIB を加えます。

link /SUBSYSTEM:WINDOWS /NODEFAULTLIB foo.obj bar.obj /OUT:foobar.exe

BCCでは、リンカにCW32.LIBやC0W32.OBJを渡さないようにします。

ilink32 /c /C /Gn /x /Tpe /aa foo.obj bar.obj,foobar.exe,,kernel32.lib
 user32.lib gdi32.lib,,

スタートアップルーチン

C言語で書かれたプログラムは、関数mainやWinMainから開始するのではありません。デフォルトライブラリに含まれたスタートアップルーチンがプログラムの開始ポイントとなり、関数mainやWinMainを呼び出します。スタートアップルーチンは、標準関数が使う外部変数の準備などを行います。

スタートアップルーチンの図

Visual C++では、スタートアップルーチンはデフォルトライブラリに含まれます。BCCでは、スタートアップルーチン用のモジュールがC0W32.OBJなどに分かれています。

デフォルトライブラリを使わない場合は、スタートアップルーチンを自分で書かねばなりません。Visual C++でWindows用EXEファイルを作るときは、WinMainの代わりにWinMainCRTStartupを記述します。

次のような感じです。WinMainと違って引数は渡されないので、インスタンスハンドルはAPI GetModuleHandleで、コマンドラインパラメータはAPI GetCommandLineで取得します。プログラムを終えるときには、API ExitProcessを呼び出します。

void WINAPI WinMainCRTStartup(void)
{
    (中略)
    
    hInst = GetModuleHandle(NULL);
    pCmdLine = GetCommandLine();
    
    wndclass.style         = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
    (中略)
    RegisterClass(&wndclass);
    
    hwnd = CreateWindowEx(WS_EX_TOOLWINDOW, szClassName, szWindowText,
        0, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
        NULL, NULL, hInst, NULL);
    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);
    
    while(GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    
    ExitProcess(msg.wParam);
}

BCCでは、WinMainCRTStartupをスタートアップルーチンにしてくれません。アセンブラを使ってスタートアップルーチンを指定する必要があります。TClock Lightでは、TClock2chから引き継いだアセンブラのソースbccexe.nasに次のコードがあります。

..start: はNASMでの実行開始ポイントです。jmp WinMainCRTStartup は「WinMainCRTStartupのアドレスに飛べ」という命令です。

SEGMENT TEXT align=4 public use32 class=code

    extern WinMainCRTStartup
..start:
    jmp WinMainCRTStartup

Visual C++でDLL(tcdll.tclock)を作るときは、DllMainの代わりに_DllMainCRTStartupを書きます。

BOOL WINAPI _DllMainCRTStartup(HANDLE hModule, DWORD dwFunction, LPVOID lpNot)
{
    return TRUE;
}

DLLをBCCで作るときはどうすればいいのか、まだよく分かってません。とりあえず、次のようにしといたら、動いているようです。

#ifdef __BORLANDC__
#define _DllMainCRTStartup DllEntryPoint
#endif

実際のTClock Lightのソースでは、#ifdef NODEFAULTLIBで切り分けて、デフォルトライブラリを使わない場合にだけ以上の処理をするようにしています。

標準関数の置き換え

デフォルトライブラリを使わない場合は、Cの標準関数が使えないので、代わりになるものを用意します。TClock Lightでは、標準関数の代わりになる関数をcommon/nodeflib.cに記述し、宣言やマクロはcommon/common.hに書いています。

自作の関数で置き換える

文字列処理用の簡単な関数なら、自作のものをちょちょいと作ればOK。たとえば、TClock Lightではatoiを次のように書いています(common/nodeflib.c)。

int r_atoi(const char *p)
{
    int r = 0;
    while(*p)
    {
        if('0' <= *p && *p <= '9')
            r = r * 10 + *p - '0';
        p++;
    }
    return r;
}

Windows APIで置き換える

標準関数の中には、Windows APIで置き換えられるものもあります。

標準関数Windows API備考
strcatlstrcat
strcmplstrcmp
stricmplstrcmpi標準関数ではない
strcpylstrcpy
strncpylstrcpyn
strlenlstrlen
sprintfwsprintf第2引数に指定できる書式にはたぶん違いがある
mallocGlobalAllocPtrwindowsx.hで定義されているマクロ
freeGlobalFreePtrwindowsx.hで定義されているマクロ

TClockでは、ファイルの読み書きは_lopen、_lread、_lwrite、_lcloseなどのAPI、プログラムの実行はAPI ShellExecuteを使っています。

最適化で展開される関数

Visual C++では、デフォルトライブラリなしでも、次の関数はコンパイラの最適化によってインライン展開されるので、置き換えは必要ありません。/Oiオプションを参照。

memcpy、memset、strcpy、strcat、strcmp、strlen

しかし、BCCではstrcpyとstrcmpしかインライン展開されないようです。

common.hでの定義

標準関数を置き換えるための、common/common.hでの関数宣言やマクロの定義は、次のようになっています(03/10/18現在)。GlobalAllocPtrとGlobalFreePtrは、BCCでいやな警告が出るので、直接記述しています。

#ifdef NODEFAULTLIB

void r_memcpy(void *d, const void *s, size_t l);
void r_memset(void *d, int c, size_t l);
int r_strncmp(const char* d, const char* s, size_t n);
int r__strnicmp(const char* d, const char* s, size_t n);
int r_atoi(const char *p);
int r_atox(const char *p);
int r__wtoi(const WCHAR *p);
__inline int r_toupper(int c)
{
    if('a' <= c && c <= 'z') c -= 'a' - 'A';
    return c;
}
DWORDLONG r_M32x32to64(DWORD a, DWORD b);

#undef toupper
#define toupper(c) r_toupper(c)

#undef _stricmp
#define _stricmp(d,s) lstrcmpi(d,s)

#undef _strnicmp
#define _strnicmp(d,s,n) r__strnicmp(d,s,n)

#undef atoi
#define atoi(p) r_atoi(p)

#define atox(p) r_atox(p)

#undef _wtoi
#define _wtoi(p) r__wtoi(p)

#undef malloc
#define malloc(cb) (GlobalLock(GlobalAlloc((GHND), (cb))))

#undef free
#define free(p) (GlobalUnlock(GlobalHandle(p)), GlobalFree(GlobalHandle(p)))

#define M32x32to64(a,b) r_M32x32to64(a,b)

#ifdef __BORLANDC__

#undef memcpy
#define memcpy(d,s,l) r_memcpy(d,s,l)

#undef memset
#define memset(d,c,l) r_memset(d,c,l)

#undef strlen
#define strlen(s) lstrlen(s)

#undef strcat
#define strcat(d, s) lstrcat(d, s)

#undef strncmp
#define strncmp(d, s, n) r_strncmp(d, s, n)

#endif  // end of __BORLANDC__

#else   // #ifndef NODEFAULTLIB

#define atox(p) strtol((p),NULL,16)

#define M32x32to64(a,b) ((DWORDLONG)(a)*(DWORDLONG)(b))

#ifdef __BORLANDC__

#define _stricmp(d,s) stricmp(d,s)
#define _strnicmp(d,s,n) strnicmp(d,s,n)

#endif

#endif

あとがき

作者が何でこんなことにこだわってるかというと、10年以上前のMS-DOS時代に、貧弱な286マシン(拡張メモリなし)で高速軽快なプログラムを書くことに熱中してたときのクセが抜けないからです。強力なCPU、豊富なメモリやハードディスクが安価に使える現在では、このページに書かれているテクニックは無意味です。貧乏性ってやつですな。