Mitsukiの魔法実験室

Mitsuki's Magic Laboratory
Since 2002.09.14

可変長引数で遊んでみる

Posted at 2006/08/04 23:32 in Programming

 そういえば、今日(既に昨日)は Binary 2.0カンファレンス の日でしたか。どんな内容だったか気になるなあ……。
 というわけで、せっかくなので(?)バイナリっぽいネタを少し。

 諸事情で、以下のような機能を実現する必要があるとします:

printf()と同じフォーマットが使えるデバッグログ記録関数を作る。
ただし、vsprintf()は使えない。sprintf()は使える。
独自のログ書き出し関数log_puts()は、固定引数で文字列へのポインタを1つだけ受け付ける。

 vsprintf()が使えれば一発で終了ですが、それが使えないとなるとどうしたものか。sprintf()が使えるのはいいけど、可変長で渡ってくる引数をどうやって渡せばいい?

方法1:auto変数を利用する

 Cの関数呼び出しの実装を理解してる人であれば、要はスタック操作だなーというところは見当が付くと思います。わたしが最初に思いついたのはこんなコードでした:


char workbuf[BUFSIZE];

int log_printf(char *format, ...)
{
    char argcopy[ARGSIZE];
    
    memcpy(argcopy, &format + 1, ARGSIZE);
    sprintf(workbuf, format);
    
    log_puts(workbuf);
}

 memcpy()でスタック上の引数部分をauto変数argcopy[]に複製します。auto変数はスタック上に取られますから、それがそのままsprintf()の引数として使われる……というカラクリです。sprintf()呼び出し時のスタックイメージはこんな感じ:
(ここから上は呼び出し元のスタックフレーム)
引数n
……
引数2
引数1
戻りアドレス
呼び出し元スタックフレームレジスタ待避
(現スタックフレームレジスタはここを指す)
レジスタ待避
auto変数領域:argcopy[]
(*1)
format
workbuf

 ただ、少し難点があります。

(1)処理系に依存する
レジスタ待避がauto変数領域の確保の後(上の(*1)の部分)に来る処理系が存在し、その場合はformatとargcoptyが連続しないので、sprintf()の呼び出しがうまくいかない。
(2)argcopyにコピーするサイズが固定
(2-1)main()で呼び出した場合、スタックの消費状態によってはスタックそのものの先頭を越えてコピーが発生するため、そこがアクセス保護されている場合に例外が発生する。
(2-2)引数の数が多い場合、argcopyに収まらない場合がある

 (2)はともかく、(1)は駄目な環境ではとことん駄目なので、別の方法の方がよさそうです。

方法2:構造体を利用する

 次に思いついたのがコレ:


char workbuf[BUFSIZE];

struct _argcopy_t{
    char buf[ARGSIZE];
}   argcopy;

int log_printf(char *format, ...)
{
    memcpy(argcopy.buf, &format + 1, ARGSIZE);
    sprintf(workbuf, format, argcopy);
    
    log_puts(workbuf);
}

 構造体の値渡しは、構造体のイメージそのものをスタックにコピーして渡してしまう実装が多いので、実に素直に引数のコピーが実現できます。

方法3:スタックフレームを追いかける

 次に問題点(2)ですが、Cの関数は引数の数やサイズを取得できないので、どうしたものか(printf()ではformat文字列を解析して数とサイズを決定しています。C++なら変形後の関数名をデコードする手も)。
 悩んだ結果がコレ:


char workbuf[BUFSIZE];

struct _argcopy_t{
    char buf[ARGSIZE];
}   argcopy;

typedef void (*funcptr)(void);

int getframesize(unsigned long arg)
{
    unsigned int    pre, now, len;
    
    pre = *(unsigned int*)(arg - sizeof(funcptr) - sizeof(int));
    now = (unsigned int)(arg + sizeof(char*));
    len = pre - now;
    if (len > ARGSIZE){
        len = ARGSIZE;
    }
    return len;
}

int log_printf(char *format, ...)
{
    memcpy(argcopy.buf, &format + 1,
        getframesize((unsigned long)&format));
    sprintf(workbuf, format, argcopy);
    
    log_puts(workbuf);
}

 getframesize()は、スタックフレームを追いかけることで、呼び出し元関数のスタックフレームのアドレスを割り出し、そこと第一引数のポインタとの間のサイズを算出します。引数のサイズそのものではないですが、少なくともこれで安全にアクセスできる領域は判断できますので、この範囲で引数をコピーすれば(2-1)は解決します(フレームサイズがARGSIZEよりも大きい場合は単純に切り捨てる)。

問題点

 問題は(2-2)ですが……構造体のサイズを動的に変更するわけにもいかないので、どうしたものか。これについては、ちょっといい方法が思いつきません。まあ、1引数でせいぜいsizeof(int)しか消費しないので、そこそこ確保しておけば実用上は問題ないとは思いますけど。