VC++2013でxorが違う値を参照するバグ

VisualStudio2013のVC++にてReleaseビルドだとxor命令で参照する値が別の値になり、結果期待した動作をしないバグに遭遇した。
原因は不明だが、最小再現コードを作ったので公開しておく。

vs2013_xor_bug.cpp

#include  <cstdio>
#include <cstring>

#include <memory>

const unsigned int KEY_SIZE = 8;
const unsigned int HEADER_SIZE = 16;

void decrypt(const unsigned int size, const unsigned char * const key, unsigned char * const buf) {
  for (unsigned int i = 0, table_index = 0; i < size; i++) {
    buf[i] ^= key[table_index];
    table_index = (table_index + 1) % KEY_SIZE;
  }
}

bool readHeaderAndKey(const unsigned char * const in, const unsigned long long int file_size, const std::shared_ptr<unsigned char> &header, unsigned char * const key) {
  std::memcpy(header.get(), in, HEADER_SIZE);
  const unsigned int file_name_table_address = *reinterpret_cast<const unsigned int *>(&in[12]) ^ 0xF8A42792;
  if (file_name_table_address > file_size) {
    return false;
  }
  const unsigned int footer_size = static_cast<unsigned int>(file_size)-file_name_table_address;
  key[4] = header.get()[4] ^ reinterpret_cast<const unsigned char *>(&footer_size)[0];
  key[5] = header.get()[5] ^ reinterpret_cast<const unsigned char *>(&footer_size)[1];
  key[6] = header.get()[6] ^ reinterpret_cast<const unsigned char *>(&footer_size)[2];
  key[7] = header.get()[7] ^ reinterpret_cast<const unsigned char *>(&footer_size)[3];

  decrypt(HEADER_SIZE, key, header.get());
  return *reinterpret_cast<const unsigned int *>(&header.get()[12]) < file_size;
}

int main(unsigned int argc, const char * const argv[]) {
  const unsigned char data[HEADER_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x80, 0xDF, 0x90, 0x52, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x4F, 0xA4, 0xF8};
  const std::shared_ptr<unsigned char> header(new unsigned char[HEADER_SIZE]);
  unsigned char key[KEY_SIZE];
  readHeaderAndKey(data, 27420, header, key);
  for (const unsigned char c : key) {
    std::printf("%02x ", c);
  }
  readHeaderAndKey(data, sizeof(data), header, key);
  decrypt(0, NULL, NULL);
  return 0;
}

上記のコードを以下のようにコンパイルすると発生。

cl vs2013_xor_bug.cpp /O2 /Oy-

期待した動作としては
00 00 00 00 90 dc 90 52
と出力されてほしいのだが
00 00 00 00 90 cf 90 52
と出力される。
(6番目の出力がずれている)

試しにprintfしているforをstd::printf(“%02x\n”, key[5]);とでも書き換えてみると、コンパイルオプションに関わらず常にdcと表示されるようになるので、明らかにバグ。

23行目から先の逆アセンブラ画面。

選択行の部分がkey[5]へ代入する値を作っている部分、本来XOR AL,BHであるべきで実際そう書き換えると治る。

mainのprintfの後のreadHeaderAndKeyやdecryptは意味がないように見えるが、ないと発生しなくなる。
おそらく一か所でしか実行されない関数は専用の最適化が走り、それによりバグが発生する最適化の条件から外れるのだと思われる。

headerがstd::shared_ptrなのは、元はちゃんとしたクラスだったのだがshared_ptrでくるんであれば何でもいいようだったのでunsigned charにしてある。

他にもreadHeaderAndKeyの無意味そうなifや返り値も外すと発生しなくなるし、key[5]以外のxorを外したりintにまとめて一回で済ませようとしても発生しなくなる。

Visual Studio 14 CTPでも同様に発生するが、Visual Studio 2012では発生しないことを確認している。