オルトプラスエンジニアの日常をお伝えします!

Polymorphic Allocator in C++17

こんにちは、この記事はAltplus Advent Calnendar 2017の17日目のエントリです。

仕事ではAkka Streamの綺麗さに感動しつつ、プライベートのプロジェクトではC++17でコーディング中の竹田です。

Polymorphic Allocator

先日発行されたC++17ではPolymorphic Allocatorと呼ばれるAllocatorが導入されました。この導入を受けてか、Cppcon 2017ではAllocator関連の発表が多くありました。

Allocatorの問題点

そこにあるのに誰も気付かず、ないと生きていられない、まるで空気のような存在のAllocatorですが、みんな大好きstd::vectorテンプレートの第2型パラメータの彼です。

template<
    class T,
    class Allocator = std::allocator<T> // 今回の主役。
> class vector;

Allocatorは型に対してメモリを提供することが役割で、STL(Standard Template Library)をコンピュータのメモリ・モデルから切り離すことを目的として導入されました。Allocator (C++)@Wikipedia)従来のAllocatorは幾つかの問題点を抱えており、C++11以降、少しづつ改良されてきました。

問題点の1つは実装の詳細であるはずのAllocatorが型の一部としてあることです。STLではテンプレートのパラメータとしてAllocatorが渡されています。これはAllocatorが型の一部になるということです。STLのコンテナはValue Semanticsを念頭に設計されており、数学のコンセプトを表現していますが、Allocatorという実装の詳細が型に表われると、この対応がくずれてしまいます。例えば、std::vectorは、列(sequence)を表現しており、数列{1, 2, 3}は、std::vector<int>{1, 2, 3}で表現できます。Allocatorの異なる、std::vector<int, MyAllocator>{1, 2, 3}も、std::vector<int, YourAllocator>{1, 2, 3}も同じ数列{1, 2, 3}の表現ですが、型が違うため比較することはできません。

その他にもAllocatorが型の一部であるためカスタムAllocatorを使用したいクライアントのコードまでテンプレート化する必要があるなどの問題もあります。

これらの問題点を解決するために導入されたのがstd::pmr::polymorphic_allocatorstd::pmr名前空間したにあるコンテナです。

std::pmr

まずはstd::pmr::vectorテンプレートの定義を見てみます。std::vector@cppreference単純に、std::vectorのAllocatorにstd::prm::polymporphic_alloctorを指定したalias templateです。

namespace pmr {
    template <class T>
    using vector = std::vector<T, std::pmr::polymorphic_allocator<T>>;
}

std::pmr::polymorphic_allocatorはコンストラクタでstd::pmr::memory_resourceオブジェクトへのポインタを受け取ります。std::pmr::polymorphic_allocatorの振舞いstd::pmr::memory_resourceの振舞いで定義されます。ラムダがstd::functionで型消去されるのと同様に、Allocatorはstd::pmr::polymorphic_allocatorによって型を消去されます。異なるAllocatorを型消去で単一の型として扱うことで前述のAllocatorが型に表われる問題を解決します。

カスタムAllocator

std::prm::memory_resourceから派生したクラスを実装することでカスタムAllocatorを定義することができます。Howard HinnantのStack Allocatorを、std::memory_resourceを使用して実装してみます。Stack Allocatorは、要素数の少いコンテナ用のAllocatorで、ヒープではなくスタックにメモリを確保することで動的メモリ確保のコストを避けるAllocatorです。以下のコードではSTLのC++17対応状況の関係、std::experimental名前空間を使用していますが、C++17では、experimentalは不要です。

カスタムAllocatorを実装するには、std::pmr::memory_resourceから派生し、以下の3つのメソッドをオーバーライドする必要があります。

  • do_allocate
  • do_deallocate
  • do_is_equal
#include <experimental/memory_resource>
#include <array>
#include <vector>
#include <cassert>

template<std::size_t N>
class stack_memory_resource : public std::experimental::pmr::memory_resource {
    std::array<std::byte, N> buf;
    std::byte* pos;
public:
    stack_memory_resource() noexcept : pos(buf.begin()){}
    stack_memory_resource(stack_memory_resource const&) = delete;
    stack_memory_resource& operator=(stack_memory_resource const&) = delete;

protected:
     // メモリの確保。実装の簡素化の為、alignmentは無視。
    void* do_allocate(std::size_t bytes, std::size_t /*alignment*/) override {
       if(bytes > remaining()){
        throw std::bad_alloc();
       }
       auto memory = pos;
       pos += bytes;
       return memory;
    }
     // メモリの解放。実装の簡素化の為、alignmentは無視。
    void do_deallocate(void* p, std::size_t bytes, std::size_t /*alignment*/) override {
        if(pointer_in_buffer(p)){
            if(auto pb = static_cast<std::byte*>(p); (pb + bytes) == pos){
                pos = pb;
            }
        }
    }
    // オブジェクトの比較。
    bool do_is_equal(std::experimental::pmr::memory_resource const & other) const noexcept override {
        return this == &other;
    }
private:
    std::size_t remaining() const {
        return buf.end() - pos;
    }

    bool pointer_in_buffer(void* p) const {
        return buf.begin() <= p && p < buf.end();
    }
};

// 標準ライブラリが対応していないため自分でalias templateを定義。
namespace std::pmr {
    template <typename T>
    using vector = std::vector<T, std::experimental::pmr::polymorphic_allocator<T>>;
}

int main(){
    stack_memory_resource<32> resourceA;
    stack_memory_resource<64> resourceB;
    // vectorのコンストラクタはpolymorphic_allocatorを期待しているが、
    // *memory_resourceからpolymorphic_allocatorへは暗黙の変換
    std::pmr::vector<int32_t> vecA({1, 2, 3, 4}, &resourceA);
    std::pmr::vector<int32_t> vecB({1, 2, 3, 4}, &resourceB);
    assert(vecA == vecB);// 異なるリソースからメモリを確保しても比較可能。
    return 0;
}

main関数で実装したカスタムAllocatorを使用しています。std::pmr::vectorに定義したstack_memory_resourceへのポインタを渡しています。異るリソース上に構築したvector同士の比較も可能で、値のセマンティックスに従った動作をします。

ベンチマーク

実装したstack_memory_resourceが、どれくらいの効果があるかquick-bench.comで計測した結果が以下の通りです。ループ中で要素数4のvectorの生成と破棄を繰り返した性能の比較です(gcc-7.2 & c++17 & -O3)。結果を見ると、stack_memory_resourceの方が約7倍ほど速いようです。

static void VectorCreationOnHeap(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  for (auto _ : state) {
    std::vector<int32_t> v{1,2,3,4}; 
    // Make sure the variable is not optimized away by compiler
    benchmark::DoNotOptimize(v);
  }
}
// Register the function as a benchmark
BENCHMARK(VectorCreationOnHeap);

static void VectorCreationOnStack(benchmark::State& state) {
  // Code before the loop is not measured
  for (auto _ : state) {
    stack_memory_resource<64> resource;
    std::pmr::vector<int32_t> v({1, 2, 3, 4}, &resource);
    benchmark::DoNotOptimize(v);
  }
}
BENCHMARK(VectorCreationOnStack);

kLrJPtoYF3-P-cmeQmw_LDiXwug.png

quick-bench.com計測結果

STLでは、スレッド・アンセーフでグローバル・ロックをとらないリソース(unsynchronized_pool_resource)や、今回のstack_memory_resourceのようにリソースの破棄のタイミングでのみメモリを解放するmonotonic_buffer_resourceなどいくつかのmemory_resourceを提供しています。

最後に

今回はC++17で導入されたpolymporhic_allocatorの仕組みを使ってカスタムAllocatorを実装する方法を見てきました。

Allocatorは様々な用途で広く利用されています。

  • GPUなどCPU以外のデバイスのメモリを使用する。Boost.Compute pinned_allocator
  • BitCoinソフトウェアのセキュリティ向上。破棄後にメモリをゼロ・クリア。メモリのスワップによるハード・ディスク上への情報の残りを防ぐ。Bitcoin Core allocators

Allocatorを使用することで、STLのコンテナという高い抽象度のIFを使用しつつ、さまざまな機能を追加することが可能です。皆さんも目的にそったAllocatorが無いか探してみるのはいかがでしょうか。From Security to performance to GPU programmingの、スライドに多くのAllocatorが紹介されています。

CppCon 2017 アロケータ関連の発表

チャットでの「伝わる」伝え方 実践まとめ

※ この記事は「AltPlus Advent Calendar 2017」の15日目の記事です。

id:aptake です。

サーバサイドのエンジニアを担当しています。

弊社ではChatWorkを利用しており、テキストでのコミュニケーションが日常茶飯事です。
テキストを通して伝えていくわけですが、相手に伝えたからといって、意図通り正しく伝わっているかといえばそれはわかりません。伝え方次第です。
今回は、チャットでの「伝わる」伝え方について実践していることをまとめます。

使うもの

  • テキストエディタ(使い慣れたもので問題ありません)

1. 用件・主題を考える

  • 最低限必要なものにして最重要なもの
  • 一番伝えたいことは何か
  • 相手にどうしてほしいのか
  • 相手の反応についてもある程度想定する

2. 動機・背景を考える

  • 何故、伝える必要が起きたのか
  • 急を要するなら伝えなくても良いし、事後に伝えるでも良いと思うが、あればその後のコミュニケーションが円滑になる

3. 書き出す

  • 伝えたい内容をテキストエディタに箇条書きする
  • 伝える順序や体裁はいったんここでは考えない、出し切ることが大事

4. 書き出したものを整理する

整理の際には以下のことを考える。

1. 相手の持つ知識範囲

  • 相手の持つ知識で理解できる言葉を選ぶ
  • 相手の役割が何かを考えると、ある程度見えてくる

2. 言葉の使い方は正しいか

  • 不安になったら調べる(使い慣れない言葉は特に)
  • 間違った使い方が広まっている言葉もあるので注意

3. 感情の排除

  • 感情が含まれると本当に伝わってほしいことが伝わりにくくなる
  • 苦労や思い入れなどがあっても我慢してほしい

4. 不要なものは削る

  • 内容は簡潔に、読むにもコストが発生する

5. 伝わるようにソートする

  • 書き出したものをカット&ペーストして入れ替える、これを繰り返す
  • ソートのコツは、
    1. 用件・主題は最初に持ってくること
    2. 伝える内容が、それまでに伝えてきた内容を踏まえていること

何故ここまでするのか

誤解を生まないためです。
誤解が生じれば、その後に影響を及ぼします。発見が遅れればそれだけ影響も大きくなります。
それを取り戻すために余計なコストが発生します。余計なコミュニケーションも発生してしまいます。
それらを未然に防ぐためにも、伝え方は重要です。

最後に

今回、「伝わる」伝え方について書こうと思った理由を述べます。
それは、学校も含めて誰からもまともに教わる機会が無かったからです。

「伝わる」ことの重要性に気付いたのは、社会に出てから数年後のことでした。
また、その折に『理科系の作文技術』という本の存在を知りました。
本書はドキュメント作成を意識したもので、内容もちょっと古いのですが、今でも通用するものは多いと感じました。
ここまでまとめてきた点についても、本書を参考にしている部分があります。
題には「理科系」とありますが、理科系でなくても読む価値は充分あると思います。

最後の最後に、今回は「伝わる」ことを重視しているので、テキストの面白さは追求しませんでした。
それはまた別の技術になると考えています。

notepad.exeをJavaコードをかけない体に改造する

この記事は Altplus Advent Calendar 2017 の14日目のエントリです。

※Qiitaに書いた記事を転載しています。

こんにちわ。id:takuya-kikuchiです。 最近、ゲームのチートだったりハッキングだったり、怖い話が多いですね。 ああいう手口から身を守るには、まず攻撃手段について学ばなければなりません。

というわけで今回は、「notepad.exeを、2度とJavaコードが書けない体に改造する」というテーマで、悪いプログラマーになりきってみようと思います。

実演

「意味がわからない」という声が聞こえますので、まずはデモを見ていただきたいと思います。

https://camo.qiitausercontent.com/ce1df09efc9208e9879a1ff09800e60b23243726/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f38623133643233312d316365312d373433662d626633312d3561656130383461343161322e676966

おわかりいただけただろうか。

https://camo.qiitausercontent.com/a7e33ef6cc37c7f862aae66fcb21be1b9161a1be/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f34623336626135362d363061322d613938632d393537612d3433626263373764393938302e706e67

ファイル保存時にHelloWorld.javaと入力したにも関わらず

https://camo.qiitausercontent.com/41fcb30b0663588a74d62aaa655289f174d397a5/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f34363138342f37613330326430382d363234632d353366322d326461372d3535626636373932643261612e706e67

HelloWorld.csとなっているのです。悪魔的所業。これではJavaコードがかけないじゃないか。許せない!!!

解説と実装

何をやっているのかというと、 notepad.exeが、"*.java" というファイルをオープンしようとした場合に、そのファイル拡張子を".cs"に差し替えています。 つまり、その気になれば、JavaコードだけでなくC++コードを書くことすら叶わない体に改造することも可能なのです。

さて、このような非人道的なハックを行う際に必要な手順は、以下に示すわずか3ステップです。

  1. DLLインジェクション
  2. CreateFileW APIをフック
  3. CreateFileWに渡されるファイルパスを任意に差し替える

以下、これらの処理をざっくりと解説します。

DLLインジェクション

DLLインジェクションとは、ざっくり申し上げて「他のプロセスに対して任意のDLLをロードさせる行為」です。

やりたいことは「notepad.exeのCreateFileWに渡されるすべての.javaファイルのパスを、渡される前に消し去りたい」ということですが、そんなことは外部のプロセスからは行えません。 従って、まずはnotepad.exeのプロセスに自身のコード(DLL)をロードさせる必要があります。

そんなことできるの?とお思いになるかもしれませんが、そのための手段をOSはいくつか提供しており1、その中の1つがSetWindowsHookExです。

HHOOK WINAPI SetWindowsHookEx(
  _In_ int       idHook,
  _In_ HOOKPROC  lpfn,
  _In_ HINSTANCE hMod,
  _In_ DWORD     dwThreadId
);

このAPI自体はかなり多機能で、例えばキーボード入力やマウス操作、WindowMessageをフックするような手段を提供しています。ただし、今回はその中身は対して重要ではありません。このAPIの強力なところは、起動中のほぼすべてのプロセスに対して、hModで指定したモジュール(=DLL)をロードさせることができる点です。

例えば今回作成した悪いツールでは、以下のような使い方をします。

LRESULT WINAPI HookProc(int code, WPARAM wParam, LPARAM lParam) {
    return NULL;
}

void Inject() {
    SetWindowsHookEx(WH_CALLWNDPROC, HookProc, GetModuleHandle(__TEXT("WaruiDll")), 0);
}

まず、SetWindowsHookExで指定するモジュールハンドルは、DLLのハンドルでなければなりません。(exeではダメ)。なので、HookProcはWaruiDll.dllというDLLに定義しています。なお、今回の目的はnotepad.exeにWaruiDll.dllをロードさせることなので、SetWindowsHookExに渡すHookProcの中身なんでもよいですし、何もしなくて構いません。

ちなみにこの悪いツールのエントリポイントはこちら。

int main()
{
    Inject();
    
    getchar();
    return 0;
}

WaruiApp.exeという実行ファイルとしてビルドされます。起動するや否やSetWindowsHookExを実行し、WaruiDllをばら撒く悪いやつです。

なお、親プロセスが終了するとばらまいたDLLも居なくなるようなので、getcharで待機させています。

CreateFileWをフックする

notepad.exeで書き込むファイル名を差し替えてしまいたいので、CreateFileWをフックすればよいでしょう。では、いつフック処理を行えばよいでしょうか。

Dllmain

Windows環境ではDLLロード時に実行されるDllMainというエントリポイントがありますので、そちらで処理を行うことにします。以下のようなコードになりました。

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        NotepadHook();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

DllMainは複数回呼ばれることがありますが、引数としてul_reason_for_callが渡さます。これにより、どういう状況でDllmainが呼び出されたのかがわかります。今回は1回だけフックをかけてやれば良いはずなので、DLL_PROCESS_ATTACH(=プロセスにDLLがロードされたときに呼ばれる)のときに処理を行うこととします。

では、次はいよいよフック処理です。

notepad.exeだけフック

void NotepadHook() {
    TCHAR fileNameBuf[MAX_PATH];

    // only for notepad.exe
    if (GetModuleFileName(GetModuleHandle(NULL), fileNameBuf, MAX_PATH) > 0) {
        if (_tcsstr(fileNameBuf, TEXT("notepad.exe")) != NULL) {
            createFileWPtr = (fpCreateFileW)RewriteFunction("kernel32.dll", "CreateFileW", HookedCreateFileW);
        }
    }
}

今回はnotepad.exeのみを対象にしたかったので、プロセス自身のモジュール名を取得し、notepad.exe以外はスルーすることにしました。2 GetModuleHandle(NULL)でプロセスのモジュールを取得し、GetModuleFileName()でファイルパスを取得し、notepad.exeであるか判定を行ないます。

実際にAPIフックを行う処理はRewriteFunctionですが、 RewriteFunctionの実装は、APIフック (Cライブラリ関数やWindowsAPIの書き換え) と、その応用例を参考にさせていただき、掲載されているコードをほぼそのまま使わせていただきました3ので、そちらをご参照いただければと思います。

HookedCreateFileW

CreateFileWの差し替え先あるHookedCreateFileWの実装はこちら。

HANDLE
WINAPI
HookedCreateFileW(
    _In_ LPCWSTR lpFileName,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwShareMode,
    _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD dwCreationDisposition,
    _In_ DWORD dwFlagsAndAttributes,
    _In_opt_ HANDLE hTemplateFile
)
{
    if (lpFileName != NULL) {
        if (auto fileNameLength = lstrlen(lpFileName) > 5) {
            auto extPtr = lpFileName + lstrlen(lpFileName) - 5;
            if (lstrcmpW(extPtr, TEXT(".java")) == 0) {
                // ファイルパスを書き換える。よい子はこういうキャストはしないこと
                lstrcpy((LPWSTR)extPtr, TEXT(".cs"));
            }
        }
    }
    // オリジナルのAPIを呼び出す
    return createFileWPtr(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}

渡されたlpFileNameの末尾が.javaであった場合に.csに書き換える悪魔的所業はこちらで行われております。lpFileNameに小細工を行うだけで、最終的にはオリジナルのAPI(CreateFileW)を呼び出しています。

以上で実装は完了です。WaruiApp.exeをひとたび起動すれば、あなたのWindows上で動いているnotepad.exeはjavaファイルを作成できなくなっていることでしょう。

立つ鳥跡を濁さず

DLLがデタッチされるときにはちゃんとフックを解除してあげないと、フックされたnotepad.exeは2度とCreateFileWを呼べない体になってしまうので気をつけましょう。

手口まとめ

  1. WaruiApp.exeがnotepad.exeにWaruiDll.dllをインジェクションする(SetWindowsHookExを使う)
  2. WaruiDll.dllが、notepad.exeのCreateFileWをフックし、悪意ある処理に差し替える
  3. Javaコードが保存できない

おわりに

今回のコードはこちらにアップしてあります。くれぐれもご利用は慎重に、自己責任でよろしくお願いします。 特にSetWindowsHookによるグローバルフックやAPIフックは下手をするとOSを巻き込んで問題を起こしてしまう可能性が多分にあるので、ご注意ください。ただし、そのぶんいじって楽しい分野ですね。個人の趣味の範囲であればね

余談ですが、DLLインジェクションやAPIフックは、マルウェア・セキュリティソフトウェア双方が活用している技術です。使い方次第で毒にも薬にもなりますが、個人の感想としては、毒の成分のほうが多めであるのでは感は否めません。なので近頃OSのアップデートによってどんどん用途や自由度が狭められている印象があります。

あと、notepadでコーディングをするのはおすすめしません。

脚注


  1. 昔はもっと手軽にインジェクションする手段(AppInit_Dlls)も提供されていましたが、そりゃまぁ危険な技術なので、最近はどんどん肩身が狭くなってきました

  2. ここで絞るのを忘れてexplorerとかに変なコードを仕込むと、最終的にはOSを巻き込んで死ぬこと請け合いなので、気をつけよう!

  3. 64bitOSで動かしたかったので、コメント欄で指摘されているようにDWORDキャストの問題だけ修正させていただきました。