組み込みC/C++

C/C++リテラシー向上のためのページ

動的引数の仕組み

動的引数(可変引数)のプロトタイプ宣言のお話しがありました。printf文の裏ワザのお話しもありました。どちらもきちんとした説明は省いてしまいました。ということでここでは、Visual C++でのお話しに特化してしまいますが、動的引数の中身を理解しておこうかと思います。

 さて動的引数が現れた時にその中身にアクセスするにはstdarg.hをインクルードして以下の関数マクロと型を使いました。

 

void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
typedef (char *) va_list;

 

実際にstdarg.hを見てみると僕の処理系では下記のように定義されていました。(検索で「Visual C++ インクルードディレクトリ」を選択することで見つけられるかと思います。出てこない場合はフォルダ:C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\include を直接見てください。)

stdarg.h

#include <vadefs.h>

#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end

 

となっており、定義本体はvadefs.hにあるようでしたのでそちらを見てみると

 

vadefs.h

#elif   defined(_M_IX86)

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t)    ( *(t *)( (ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap)      ( ap = (va_list)0 )

 僕の場合x86ベースのマイクロプロセッサを使っていますので_M_IX86でdefineが切られている部分が有効になります。もちろんARM系では違う定義になるかと思いますし、RXマイコンもこれとは違う定義でした。ただ概ね似たような事をしています。何をしているか一目では判りませんが、じっくり考えて行きましょう。まず、これを解析するにはx86アーキテクチャではスタックをどのように使っているか判っておく必要があります。

 

・ある関数の呼び出しがあると実引数の右側からSTACKに積む。(STACKはアドレスが若い方へと積まれますので引数の左端が一番若いアドレスになります。)

・全ての引数を32ビット化してスタックに積む。(引数をSTACKへ積むのに全てpushlを使っています。)

 

もう少し具体的に書くと func(a, b, c, d); といった呼び出しの関数をコンパイルするとアセンブラ上では下記のように展開されます。 

 

pushl d
pushl c
pushl b
pushl a
call _func

 

次にcliant側の立場に立ってva_start()等のライブラリのinputとoutputを整理して関数の機能を確かめましょう。動的引数の例として下記のような関数がありました。

#include <stdio.h>
#include <stdarg.h>

int func(int arg_num, ... );

int main(){

    func(5, 4, 3, 2, 1, 0);
}

int func(int arg_num, ... ){

    int i = 0;
	va_list list;

	va_start(list, arg_num);

	for(i=0; i<arg_num; i++ ) printf("%d\n", va_arg(list, int));

	va_end(list);

	return(0);

}

 

まずva_listですが、これはchar *でtypedefされているだけですのでlistは1byteの実体を指すポインタです。va_startではこのlistとfuncの一番左側の引数を引数として渡し、listにfuncの左から二番目の引数のアドレスを返却してもらっています。定義をみると

#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

となっていますので、第一引数のアドレスに第一引数の大きさ分足して、第二引数のアドレスを取得している様子が見て取れます。ここで問題なのが

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

 となります。これが第一引数の大きさを表しているとはなかなか思えません。何をしているか解析してみましょう。_ADDRESSOF(v)が指す場所ですがこれはスタック上になります。スタック上の最小単位はpushlで命令されるのでint型です。という事は例えば第一引数がchar型であっても、short型であってもint型確保する事になります。またstructの場合は大きさをそのまま渡せますので5byteの型や、10byteの型というのがあり得ます。こういった場合はそれぞれintの倍数である8byte, 12byteの領域をSTACKに確保する事になります。よって_INTSIZEOF(n)をif文で表してみると

 

int SIZE;
if(sizeof(n) <= 4 ){
  SIZE = 4;
  }else if(sizeof(n) <= 8){
  SIZE = 8;
  }else if(sizeof(n) <= 12){
  SIZE = 12;
  }
return (SIZE);

 

といった事をやりたいのが判ります。しかしマクロではこんな高機能な事できません。なんとか一つの文で、かつ容量が小さく、スピードの早いプログラムにしたい。ではこのif文がどうやって_INTSIZEOF(n)のようになるか見てみましょう。まず除算を使って除余を切り捨てれば似た様な事が出来ます。

 

/*↓変形前*/
( (sizeof(n) - 1) / sizeof(int) + 1 ) * sizeof(int)
/*↓変形後*/
( (sizeof(n) + sizeof(int) - 1) / sizeof(int) ) * sizeof(int)

 

これは先ほどのif文と同じ働きをします。そしてなんだか除算の前の式が_INTSIZEOF(n)の第一項に似ていませんか?ここでもう一つの公理が出てきます。

・ int型は2の累乗のサイズである。

この公理を使うと sizeof(int) = 2 ^ k と表せます。2の累乗の除算、乗算であればシフト演算子だけで表せる事を思い出すと、

 

( ( (sizeof(n) + sizeof(int) - 1) >> k ) << k ;

 

と表す事が出来ます。ここでシフト演算子でk個行って戻ってくると何が起こっているでしょうか?下位k桁分を0にしたことと同じ結果になりますね。という事はk桁分のマスク(11110000のようなビット列)を準備すれば良い事になります。kを得るにはsizeof(int)が既知の値なので k = log2(sizeof(int)) を計算すればいいのですが、これでは本末転倒です。計算量が大きくなるだけです。そこで高校数学を思い出してみましょう。k桁分のマスク 例えば 00001111 の様なビット列の値は次の式から得られます。 

 

 S = 2^(k-1) + 2^(k-2) + ‥ + 2^1 + 2^0

 

これは2の累乗和の公式から S = 2 ^ k - 1 となり S = sizeof(int) - 1 となります。これをビット反転すれば欲しかったマスクを得る事が出来ます。~(sizeof(int) - 1) よって

 

( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

 

が導出出来ました。一度こういった導出をしっかりやっておくことで難しいプログラムを目の前にしてもなんとか自分で考える事が出来ると思います。va_argなどはこれに比べれば遙かに簡単ですので自分で考えてみてください。