winapi-rsでCONTEXTを扱うと結構な確率で998(3E6)で落ちる

この記事ではCONTEXTとは (winapi::um::winnt::CONTEXT)https://docs.rs/winapi/0.3.9/winapi/um/winnt/struct.CONTEXT.html のことを指す

tl;dr

alignを気にする必要があった

#[derive(Default)]
#[repr(align(16))]
struct Context(CONTEXT);

または winapi の v0.4 がリリースされるのを待つ必要がある

いきさつ

最近ぷよぷよテトリスというゲームに悪さをしていて*1、特定のbreakpointに対して 0xCC を貼ったりしてプログラムの動きをこちらの都合の良いものに変える等のことをしている

今回の要件としてはbreakpointを貼った地点でのレジスタの値を取る必要があった

ぷよぷよテトリスのSteam版はWindowsで動くのだが、Windows上でこれを満たすためには通常 GetThreadContext を用いる

rustの winapi-rs での使い方を例に上げると、

use winapi::um::processthreadsapi::GetThreadContext;
use winapi::um::winnt::*;

// 0xCCやSuspendThreadでスレッドを停止させたあとに
let mut regs = CONTEXT::default();
regs.ContextFlags = CONTEXT_ALL;
GetThreadContext(thread, &mut regs)

こんな感じで各種レジスタが取得できる

また、任意の値をレジスタに格納するのも容易で、同じ雰囲気で SetThreadContext を用いることで解決する

998メモリ ロケーションへのアクセスが無効です。 が多発する

なんて便利なAPIを備えているのだろう、中学生の頃の自分がちゃんと勉強してれば喜んだだろうな等と思いながら実装していたが、ちょうどGetThreadContextを叩くタイミングで落ちることが多かった

GetLastErrorで確認してみると、 998 メモリ ロケーションへのアクセスが無効です。 のエラーで落ちているようだった

当時はあまり詳しくなかった(winapiに深入りするのを避けたかった)ので渡しているものの不備を信じてGetThreadContext前後をデバッグしていたところ、printlnをしたりしなかったりすると落ちたり落ちなかったりすることに気づいた

alignment requirements

色々調べた結果たどり着いたのが「データ構造アライメント」という概念だった

いつも高レベルAPIを触っている自分にはにわかに信じ難い話だが、構造体によってはメモリ上の開始位置が揃っていないとうまく動かない場合があるらしい
今回の例で言い換えると、 CONTEXT のアドレスの1の位が0でないとうまく動かなかった

先程のprintlnをつけ外しすることで確立的に落ちるように見えていた部分では、実際はprintlnによってアドレスの1の位が8にずれることで動かなくなっていた
rustのアーキテクチャに詳しくなく、かつ調べていないので予想でしかないが、メモリ管理を厳格にする上でprintlnによってメモリ位置的な副作用が生じてしまったのだろう

また、これは Microsoft Docs の GetThreadContext にも記載されている

The CONTEXT structure is highly processor specific. Refer to the WinNT.h header file for processor-specific definitions of this structures and any alignment requirements. (訳) CONTEXTの構造は非常にプロセッサに特異的です。この構造のプロセッサ固有の定義およびアライメント要件については、WinNT.hヘッダーファイルを参照してください。

winapi-rs での解決策

この問題を解決するには要求されている通りにアライメントをする必要がある

先程のコードの例を挙げるならば次のようにすることで安定して動くようになる

use winapi::um::processthreadsapi::GetThreadContext;
use winapi::um::winnt::*;

// 追加
#[derive(Default)]
#[repr(align(16))]
struct Context(CONTEXT);

// 0xCCやSuspendThreadでスレッドを停止させたあとに
let mut regs = Context::default().0; // .0を追加している
regs.ContextFlags = CONTEXT_ALL;
GetThreadContext(thread, &mut regs)

CONTEXT を先頭に有する Context structをこちらで定義し、そのalignを16に固定することで先頭に存在する CONTEXT の align requirements を満たすことが出来た

この問題に対して既にissueは立てられているのだが、 2021/02/09 現在の winapi-rs ではまだopenのままになっている

github.com

winapi-rs の v0.3 が担保している Rust の最小バージョンでは先ほど説明した repr_align が実装されていないため未解決の状態になっているようだった
v0.4 に上げる際に repr_align に対応しているバージョンにまで引き上げることで解決を狙っているらしい

コード的には次のようになっている 解決されることに期待したい

https://github.com/retep998/winapi-rs/blob/785f7f30a69c70687864ac650aebc123e942c1b3/src/um/winnt.rs#L1040

STRUCT!{struct CONTEXT { // FIXME align 16
    P1Home: DWORD64,
    P2Home: DWORD64,
    P3Home: DWORD64,
    P4Home: DWORD64,
    P5Home: DWORD64,
    P6Home: DWORD64,
    ContextFlags: DWORD,

*1:undoの機構を設けたい