about

この記事はIPFactory Advent Calendar 2021の12/7分です.

IPFactoryというサークルについてはこちらをご覧下さい.

さて,みなさんは桂三度という落語家をご存知でしょうか.

彼は以前,世界のナベアツという名義でも活動していました.

ネタ

彼のネタとは,「3の倍数と3が付く数字のときだけ阿呆になる」というシンプルなものです.

これを聞いてエンジニア諸氏は思うことがあるでしょう.

FizzBuzzっぽいですね.

というわけで,FizzBuzzをコンパイルしようとすると,3の倍数の時に阿呆になってしまうコンパイラを作ります.

実装

こういうコンパイラに少し手を加えるくらいの事をしたい時は巨大な裁縫箱ドラゴンの肩に乗るのが良いでしょう.LLVMです.

LLVMには,Passという概念があります.

詳しくは説明しませんが,ソースコードを変換していく過程で,様々な操作(改変・解析)を行うモジュールをPassと呼んでいます.

FizzBuzzを3の倍数で阿呆にするには,3の倍数の時,つまり"Fizz"(と"FizzBuzz")が出力される時に阿呆にしてしまえば良いので,ソースコード中の"Fizz"を書き換えてしまいましょう.

bool runOnModule(Module &M) override {
  bool Changed = false;
  for (Module::global_iterator GI = M.global_begin(), GE = M.global_end(); GI != GE; GI++) {
    ConstantDataSequential *CData = dyn_cast<ConstantDataSequential>((*GI).getInitializer());
    if (CData->isString()) {
      auto orig = CData->getAsString().lower().data();
      if (strcmp(orig, "fizz") == 0 || strcmp(orig, "fizzbuzz") == 0) {
        Constant *cMod = ConstantDataArray::getString(M.getContext(), "٩( ᐛ )و", true);
        GlobalVariable *newGV = new GlobalVariable(M, cMod->getType(), true, GlobalVariable::ExternalLinkage, cMod);
        (*GI).replaceAllUsesWith(newGV);
		Changed |= true;
      }
    } else
      continue;
  }
  return true;
}

こんな感じのPassを書きます.

runOnModuleは,プログラム全体に適用されます.

この中で,ConstantDataSequentialとして定数を取得し,それが文字列で,且つ"fizz"または"fizzbuzz"に一致した場合,阿呆(٩( ᐛ )و)に置き換えてしまいます.

Passの実装や詳細についてはFPGA開発日記等の非常に参考になる記事が既にあるのでここでは避けます.

また,現在私はサイボウズ・ラボユースでLLVMを使った難読化コンパイラの研究開発を行っています(その詳細はまた別の記事で).

その過程で学んだLLVM上で難読化Passを実装する方法についても別の記事を出そうと思っているので良ければそちらもご覧ください.

コンパイルしてみる

シンプルなFizzBuzzのソースコードを,先程書いたPassを有効化したclangでコンパイルしてみます.

#include <stdio.h>

int main() {
  for (int i = 1; i <= 30; i++) {
    if (i % 15 == 0)
      puts("FizzBuzz");
    else if (i % 3 == 0)
      puts("Fizz");
    else if (i % 5 == 0)
      puts("Buzz");
    else
      printf("%d\n", i);
  }
}

ここではFizzBuzzの綺麗さは関係無いので,一般的なコードを書きます.

1から30の数字のうち,3の倍数ではFizzを,5の倍数ではBuzzを,15の倍数のときはFizzBuzzを出力し,それ以外の場合は数字をそのまま出力するプログラムです.

何もしていない普通のコンパイラでコンパイルし,実行してみると

$ gcc fizzbuzz.c -o fizzbuzz
$ ./fizzbuzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz

正しく動作しているのがわかります.

一方,先程Passを実装したclang-nabeatsuでは

$ cat fizzbuzz.c
#include <stdio.h>

int main() {
  for (int i = 1; i <= 30; i++) {
    if (i % 15 == 0)
      puts("FizzBuzz");
    else if (i % 3 == 0)
      puts("Fizz");
    else if (i % 5 == 0)
      puts("Buzz");
    else
      printf("%d\n", i);
  }
}
$ ./clang-nabeatsu fizzbuzz.c -o fizzbuzz
$ ./fizzbuzz
1
2
٩()و
4
Buzz
٩()و
7
8
٩()و
Buzz
11
٩()و
13
14
٩()و
16
17
٩()و
19
Buzz
٩()و
22
23
٩()و
Buzz
26
٩()و
28
29
٩()و

このように,ソースコードは変わっていないにも関わらず,3の倍数で阿呆になってしまいます.

面白いですね.