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ではデフォルトライブラリをリンクしています。
デフォルトライブラリをリンクしないと、次のことができなくなります。
「そこまでしてファイルサイズを小さくしたいのか」と言われたら、言い返せませんな。
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 | 備考 |
|---|---|---|
| strcat | lstrcat | |
| strcmp | lstrcmp | |
| stricmp | lstrcmpi | 標準関数ではない |
| strcpy | lstrcpy | |
| strncpy | lstrcpyn | |
| strlen | lstrlen | |
| sprintf | wsprintf | 第2引数に指定できる書式にはたぶん違いがある |
| malloc | GlobalAllocPtr | windowsx.hで定義されているマクロ |
| free | GlobalFreePtr | windowsx.hで定義されているマクロ |
TClockでは、ファイルの読み書きは_lopen、_lread、_lwrite、_lcloseなどのAPI、プログラムの実行はAPI ShellExecuteを使っています。
Visual C++では、デフォルトライブラリなしでも、次の関数はコンパイラの最適化によってインライン展開されるので、置き換えは必要ありません。/Oiオプションを参照。
memcpy、memset、strcpy、strcat、strcmp、strlen
しかし、BCCではstrcpyとstrcmpしかインライン展開されないようです。
標準関数を置き換えるための、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、豊富なメモリやハードディスクが安価に使える現在では、このページに書かれているテクニックは無意味です。貧乏性ってやつですな。