2011年9月24日土曜日

VisualStudio コンパイラの動作について

WindowsでVC++のプログラムの動きを見ていて気づきました。


プログラムが動作するための定数テーブルをバイナリファイルから読み込むプログラムの動きがどうも安定しないので、デバッグしてみました。
当該プログラムはバイナリで「複数」のテーブルデータが格納されており、いったんファイルをすべて読み込み、それをテーブル毎にアドレス計算してmemcopyでテーブル毎に分割して配列に格納しなしてます。
問題はバイナリファイルの定義が構造体の集まりになっており、構造体内の各部のサイズ計算が意図したものとことなり、memcopyでバイナリでコピーしてしまうとバイトがずれているようです。(メモリアクセスする場合は、当該CPU、OS、コンパイラによるアライメント、IA32だと4バイト単位、1IA64やx86_64なんかだと8バイト単位くらいになります。(x86_64は注意が必要です。Linuxだと4バイト単位のアライメントでデフォルトは行われます。OSが32bitか64bitも因子として関係してきますから)

ちょっと実験してみました。プログラムは以下の通りです。

#include <iostream>

// 基本的な構造体
struct {
double w1;
double w2;
} st1;

// 1byteを間に入れてみる
struct {
double w1;
bool dummy; // アライメントを崩す変数を定義する
double w2;
} st2;

void main()
{
std::cout << "Hello World!!" << std::endl;

std::cout << "st1 size = " << sizeof(st1) << std::endl;

std::cout << "st2 size = " << sizeof(st2) << std::endl;

}


これをVS10で設定をデフォルトにて実行した結果を以下に示します。

Hello World!!
st1 size = 16
st2 size = 24

bool変数(必要なのは1bitなんですが、通常のコンパイラは1byteにします)を間に一ついれただけですが、アライメントを調整するため8バイトかけてます。
次にコンパイルの設定で、「コード生成」→「構造体メンバーのアライメント」という項目があり、ここを「規定値」{デフォルトあ8)を「4バイト(/Zp4)」にします。

Hello World!!
st1 size = 16
st2 size = 20

見事にアライメントが4バイトにされ、bool dummyの後には3倍とパディングされました。
VC++ではオプションが「Zp]のようですが、「Zp1」なんてオプションもありました。ちょっと無茶苦茶なコードに(実行時間がかかる)展開されそうですが試してみました。

Hello World!!
st1 size = 16
st2 size = 17

本当に余分なパディングは一切しないようになっています。
やはり高級言語でmemoryのアドレスへの配置を意識しなければいけないようなコードを書くと、維持性で問題を起こします。
Linuxだとこの辺りはCPU、およびOSが32bitか64bitで固定だったと思います。(アライメントは4バイトか8バイトしかない。M$はなんで1バイトのパックまでできるような柔軟性を持たせるんだ!!組み込みのプログラムを書くときしか使わないだろうから、VS-Embededというバージョンを作るなり、そういう属性のプロジェクトをできるようにして、そこでだけ許可するようにしてくれ!)
ちなみにVCには構造体、個々にパックをいくつで(コンパイルを)行うかを指定する#pragma pack指定があり、さらにチューンすることがプログラマに許されています。
(こったプログラムを容易に作れるのと、高い維持性を持たせるのは難しいです)

2011年9月18日日曜日

protocol bufferのサイズ制限について(protobuf-2.1.0)

C++のオブジェクトをシリアライズ化するのに便利なgoogle protocol buffer(PB)を使ってます。ある時、プログラムの実行記録に使っている時、以下のWarningがでてきました。


libprotobuf WARNING google/protobuf/io/coded_stream.cc:462] Reading dangerously large protocol message.  If the message turns out to be larger than 67108864 bytes, parsing will be halted for security reasons.  To increase the limit (or to disable these warnings), see CodedInputStream::SetTotalBytesLimit() in google/protobuf/io/coded_stream.h.


よくわかりませんが、PBの扱えるサイズがセキュリティ上の問題から64MBだよと言ってるらしいです。この時のログは40MBくらいでした。様子を見ると、どうも制限がかかるのは「入力」バッファのサイズのようです。(考えてみればあたりまえですが、デコードするとき一度メモリ上に読み込むはずで、そこにMAX制限がかかってます)
ソースコードを見てみましたが、ばっちりlimitが入ってました。

coded_stream.cc:

namespace {

static const int kDefaultTotalBytesLimit = 64 << 20;  // 64MB

static const int kDefaultTotalBytesWarningThreshold = 32 << 20;  // 32MB  ←警告を出すサイズまで定義されてます。


このサイズを変えればリミットと変えれるのかなと思って少し調べたら、入力ストリームに以下のメソッドがありましたが、コメントを見ると他のコードを混乱させるので、使うのはやめたほうがよさそうです。

void CodedInputStream::SetTotalBytesLimit(
    int total_bytes_limit, int warning_threshold){
  // Make sure the limit isn't already past, since this could confuse other
  // code.

ぐぐってたら、同じようなシリアライズ化のライブラリにMessagePackというのがありそちらのほうが高速だというのがありました。ちょっとそれとも比較してみると。

・制限のチェックをしているのはInputです。
→実装上、protbuf-2.1では入力データをmemcopyしてからデコードしているので64MBの制限がかかります。protbufと似たようなライブラリのMessagePackは入力の実装がmemcopyではなくポインタコピーで済ませているので入力(デコード)がprotbufより速い。しかし最新のprotbuf-2.4.1のInputの実装を調べてみたらmemcopyからポインタコピーになっていました。
2.1で制限として定義されていた変数名も2.4.1ではなくなっており、おそらく64MBの入力サイズの制限はないと思います。(これ間違いだった!!)

・しかし物理的な上限は発生します。
入力チェックのメソッドと思われるところで、入力ポインタ(current_point)のチェックをしているところがあり、真っ先にINT_MAXと比較しています。
当たり前ですが、ポインタはメモリアドレスを表わしていますから最大の値はINT_MAXになります。
またやっかいなことにこのINT_MAXは当該ライブラリが実行されるOSのアーキテェクチャに依存します。
つまり32bit OSの場合には物理的に4GBが最大になります。(ロギング等、実行は64bit linuxでやり、読み込みが32bit Windowsのプログラムなんかだったりするとうっかりこの制限にひっかるので注意!)

まあ、こんな大きなサイズのファイルをめったに作成しないでしょうが念のため


実験してみました。単純に同じデータをどんどん追加していく試験プログラムを作ります。(ファイル名:tttに追記していきます)


[ examples]$ ./add_person_cpp ttt
[ examples]$ ls -l ttt
-rw-r--r-- 1 hoge hoge 29561520  9月 16 15:50 ttt ←tttのサイズはまだ32MB未満です
[examples]$ ./list_people_cpp ttt|tail
  E-mail address: rinrin.hoge.com
  Mobile phone #: 2222
Person ID: 200
  Name: rinrin
  E-mail address: rinrin.hoge.com
  Mobile phone #: 2222
Person ID: 200
  Name: rinrin
  E-mail address: rinrin.hoge.com
  Mobile phone #: 2222
[examples]$ ./add_person_cpp ttt
[examples]$ ls -l ttt
-rw-r--r-- 1 hoge hoge 33661520  9月 16 15:53 ttt ←tttのサイズが32MBを超えました!
[examples]$ ./list_people_cpp ttt|tail
libprotobuf WARNING google/protobuf/io/coded_stream.cc:462] Reading dangerously large protocol message.  If the message turns out to be larger than 67108864 bytes, parsing will be halted for security reasons.  To increase the limit (or to disable these warnings), see CodedInputStream::SetTotalBytesLimit() in google/protobuf/io/coded_stream.h.
  E-mail address: rinrin.hoge.com
  Mobile phone #: 2222
Person ID: 200
  Name: rinrin
  E-mail address: rinrin.hoge.com
  Mobile phone #: 2222
Person ID: 200
  Name: rinrin
  E-mail address: rinrin.hoge.com
  Mobile phone #: 2222
[examples]$

読み込むファイルが32MBを超えると、warningがでました。(ただ、まだ正しく読めてます)
またこれ以降、追記時に一度読まないといけないのでwarningがでるようになりました。

[examples]$ ./add_person_cpp ttt
libprotobuf WARNING google/protobuf/io/coded_stream.cc:462] Reading dangerously large protocol message.  If the message turns out to be larger than 67108864 bytes, parsing will be halted for security reasons.  To increase the limit (or to disable these warnings), see CodedInputStream::SetTotalBytesLimit() in google/protobuf/io/coded_stream.h.

ただ、まだ正しく読めます。


新しいprotobuf-2.4.1を試そうと考えたが、そもそもprotobufの実装が違うんだからlib内の関数のエントリー名を違えば、protocが生成するヘッダーだって異なってくるのが当たり前。(libだけすげかえれば・・・・なんて甘いことはだめだった Orz)
以下のように強引にヘッダーの場所やライブラリの場所を、2.4.1に指定して(システムにはすでに2.1.0がインストールされていますが、まだこの形態をVerUpしたくない)リビルドします。

[examples]$ c++ list_people.cc addressbook.pb.cc -I/opt2/home/maeda/protobuf-2.4.1/src -lpthread -L/opt2/home/hoge/protobuf-2.4.1/src/.libs -lprotobuf -o list_people_cpp

また実行前にLD_LIBRARY_PATHを追加して、2.4.1のdllを認識させます。

[examples]$ ldd ./list_people_cpp
        libpthread.so.0 => /lib64/libpthread.so.0 (0x0000003a76000000)
        libprotobuf.so.7 => not found ←認識できていない!!
        libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000003a7b800000)
        libm.so.6 => /lib64/libm.so.6 (0x0000003a75800000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003a7b400000)
        libc.so.6 => /lib64/libc.so.6 (0x0000003a75400000)
        /lib64/ld-linux-x86-64.so.2 (0x0000003a73c00000)
[examples]$ echo $LD_LIBRARY_PATH
/opt/intel/Compiler/11.0/084/lib/intel64:/opt/intel/Compiler/11.0/084/ipp/em64t/sharedlib:/opt/inte\
l/Compiler/11.0/084/mkl/lib/em64t:/opt/intel/Compiler/11.0/084/tbb/em64t/cc4.1.0_libc2.4_kernel2.6.\
16.21/lib:/opt/intel/Compiler/11.0/084/lib/intel64:/opt/intel/Compiler/11.0/084/ipp/em64t/sharedlib\
:/opt/intel/Compiler/11.0/084/mkl/lib/em64t:/opt/intel/Compiler/11.0/084/tbb/em64t/cc4.1.0_libc2.4_\
kernel2.6.16.21/lib:/usr/local/lib:/usr/local/lib64:/opt2/qtcreator-2.0.1/lib:/usr/local/lib:/usr/l\
ocal/lib64:/opt2/qtcreator-2.0.1/lib
[examples]$ setenv LD_LIBRARY_PATH /opt2/home/hoge/protobuf-2.4.1/src/.libs:${LD_LIB\
RARY_PATH}
[examples]$ ldd ./list_people_cpp
        libpthread.so.0 => /lib64/libpthread.so.0 (0x0000003a76000000)
        libprotobuf.so.7 => /opt2/home/hoge/protobuf-2.4.1/src/.libs/libprotobuf.so.7 (0x00002ad95\
96e8000) ←dllを認識した!!
        libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000003a7b800000)
        libm.so.6 => /lib64/libm.so.6 (0x0000003a75800000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003a7b400000)
        libc.so.6 => /lib64/libc.so.6 (0x0000003a75400000)
        /lib64/ld-linux-x86-64.so.2 (0x0000003a73c00000)
        libz.so.1 => /usr/lib64/libz.so.1 (0x0000003a76400000)
[examples]$

試してみたらダメ!!同じwarningが出ます。2.4.1の実装を調べたら、2.1.0から消えた64MBの閾値を格納する変数が別の位置にしっかり定義されていました。

    google::protobuf::io::CodedInputStream decoder(&input);  // has initializer but incomplete type
    decoder.SetTotalBytesLimit(100000000, 60000000);

とやって強引にlimitを変更しようとしましたが、最初のdecoderを宣言するところでコンパイルエラーがでます。(引数に入れているインスタンスがinitializerを必要としていると、言ってきました)
どうもコメントにあるように、このメソッドは使っちゃだめなようです。(googleのところも散々調べてみましたが、特殊な入力の時だけこのSetToalBytesLimit( )は働くようですが、それでもunusualだと書いてました)

PBの最初のところに、「これはRPCのデータを送るために作られた」、「ネットワークで大きなサイズのデータを送るのは問題がおきる」と書いてありましたので、PBをログ記録に使うのはそもそも用途が間違っているようです。