この記事では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のままになっている
winapi-rs の v0.3 が担保している Rust の最小バージョンでは先ほど説明した repr_align
が実装されていないため未解決の状態になっているようだった
v0.4 に上げる際に repr_align
に対応しているバージョンにまで引き上げることで解決を狙っているらしい
コード的には次のようになっている 解決されることに期待したい
STRUCT!{struct CONTEXT { // FIXME align 16 P1Home: DWORD64, P2Home: DWORD64, P3Home: DWORD64, P4Home: DWORD64, P5Home: DWORD64, P6Home: DWORD64, ContextFlags: DWORD,
*1:undoの機構を設けたい