ActiveTK's Note

【技術者向け記事】Windowsの「フリーズ」の本質


作成日時 2024/04/19 19:14
最終更新 2024/04/20 00:43


  • 序章
  • フリーズの種類
  • ユーザーモードのプログラムが応答しなくなったとき
  • ページング処理は作動しているがマウスやディスプレイが一切反応しないとき
  • カーネル全体が完全に停止したとき
  • おまけ: メモリを冷凍して読みだすことは可能か
  • 参考文献

  • 序章

    Windowsを搭載したデバイスを利用していると、多かれ少なかれ、画面が固まって一切の割り込みが行えなくなる「フリーズ」を経験することがあります。

    フリーズ発生時の対処法については「タスクマネージャーから終了」 「Ctrl + Alt + Deleteで割り込む」 「電源長押しによる強制再起動」などが挙げられますが、 本記事ではフリーズのより本質的な部分に焦点を当てて、高度な内容を含めて専門的に解説した上で、技術者向けのデバッグ方法をご紹介させていただきます。


    フリーズの種類

    コンピュータの分野に絞っても「フリーズ」は多義語です。フリーズの対処を行うには、まずどの程度の深さまでプログラムが停止しているのかを調査する必要があります。

    ここでは、フリーズを以下の三種類の段階に分類します。

    ・ユーザーモードのプログラムが応答しなくなったとき

    Windowsで実行しているプログラムが無限ループやスレッド停止命令などにより応答を停止した状態です。ウィンドウをクリックした状態で一定時間経過すると「アプリケーションは応答していません。プログラムは時間が経てばまた応答する可能性があります。このプロセスを終了しますか?」とエラーメッセージが表示されます。

    ・ページング処理は作動しているがマウスやディスプレイが一切反応しないとき

    マウスが動かずディスプレイの画面が膠着しているものの、ディスクのアクセスランプが点滅している状態です。ハードディスクやメモリのやり取りがあるため、カーネルは生きている可能性があります。

    ・カーネル全体が完全に停止したとき

    外部からの入力あるいは出力が完全に停止した状態で、カーネルの最も低レベルな段階でシステムが停止している状態です。

    それぞれの場合について、メカニズムと対処方法を解説します。


    ユーザーモードのプログラムが応答しなくなったとき

    そもそも「ユーザーモード」と「カーネルモード」について理解するには、まず「リングプロテクション」の仕組みについて知る必要があります。

    一般的なプロセッサは特権レベルの段階構造を保有しており、Ring3<Ring2<Ring1<Ring0の順番で権限が高くなります。

    Ring0はほぼカーネルそのものと同値であり、CPUに対する直接的な命令や物理的なメモリの読み出しが行えます。

    一方でより上位のリングで動作するプログラムはハードウェアに対する直接的な操作を行う権限を有しておらず、システムコールと呼ばれるカーネルに予め規定された関数を呼び出す形で低レベルな操作を行います。

    これは権限の低いプログラムからシステムを保護するための機構であり、例えばWindowsではプロセスをカーネルモードとユーザーモードに明確に分けています


    ここで、ユーザーモードで動作していたプログラムが無限ループやスレッド停止命令(Thread.Sleep)などにより応答を停止した状態について考えます。

    実際にプログラムを無限ループに陥らせてみると、以下のようなエラーメッセージが表示されることが分かります。


    実はこの"応答なし"のウィンドウの正体は、Windows Vistaから実装された「ゴーストウィンドウ」と呼ばれるものです。

    Windowsでは、プロセスの持つウィンドウオブジェクトが"ウィンドウメッセージ"に5秒以上応答しないとき、デスクトップウィンドウマネージャー(dwm.exe)がそのウィンドウを隠して全く同じ場所に新しいウィンドウを表示する仕組みがあります。

    新しく表示されるウィンドウの中身は「元のウィンドウが最後に正常に動作してきたときのビットマップ画像」であり、またウィンドウのタイトルは「(応答なし)元のウィンドウの名前」となります。

    このような状態に陥ったときには、プロセス単体をkillすることが有効です。以下にタスクマネージャーからプロセスを強制終了させる手順を示します。


    まずWindows + Rから「ファイル名を指定して実行」を開き、"taskmgr"と入力してEnterを押します。ちなみに、「Ctrl + Shift」も一緒に押した状態でEnterを押すと、指定したプロセスは管理者権限で起動されます。


    もしプロセスがCPUやメモリのリソースを占有していてタスクマネージャーを立ち上げられない場合には、「Ctrl + Alt + Delete」を同時に押して強制的な割り込みを行い、そこからタスクマネージャーを起動してください。 あるいは、オプションから一度サインアウトしてユーザーのプロセスを全て終了させ、再ログインするのも有効です。


    タスクマネージャーを開いた後、プロセスのタブからフリーズしたプロセスを右クリックして「タスクの終了」ができます。


    ページング処理は作動しているがマウスやディスプレイが一切反応しないとき

    マウスが動かずディスプレイの画面が固まりながらも、メモリとディスクの間でページングが続行されている場合です。ディスクのアクセスランプが点滅しているかどうかで判別を行うことができます。

    この状態に陥った場合には、一般的にクライアントサーバーランタイムサブシステム(CSRSS)内の問題が原因であると推測されます。これはWindowsの基となるレイヤーを制御するもので、プロセスやスレッドの作成/削除を担当しています。

    CSRSSのデバッグ方法については、Microsoft Learnに詳しい記事が公開されていますが、 Windows 10以降では、CSRSSは「保護されたプロセス」であり、カーネルモードでのみデバッグを行うことができるようになっています。

    カーネルは一応生きているため、しばらく待機していると復旧する可能性があります。しかし、CSRSSのデバッグはかなり難しいため、改善しない場合には「カーネル全体が完全に停止したとき」と同様の手順を取る必要があります。

    また、この状態ではマウスやキーボードから入力が行えないため、同じネットワークにある別の端末で以下のようなコマンドを管理者権限として実行し、ネットワーク経由の遠隔シャットダウンを試してみてください。

    shutdown /m \\{IPアドレスまたはホスト名} /s /f /c "外部から停止してみる" -t 0

    なお、遠隔の再起動は以下のコマンドで実現できます。

    shutdown /m \\{IPアドレスまたはホスト名} /r /f /c "外部から再起動してみる" -t 0


    カーネル全体が完全に停止したとき

    カーネル全体が完全に停止したときにIT技術者がすべきことは、電源を長押しして強制終了することではなく、ブルースクリーン(BSOD)を発生させることです。

    そもそも一般的に「電源長押し」と呼ばれる操作は、電源ボタンのオーバーライド(Power Button Override)のことであり、4秒など一定の時間電源ボタンを長押しした際にBIOS/UEFIが強制的にシャットダウンを行うようになっています。

    この処理はOS側に通知されず、Windows側では次回起動時にKP41病でお馴染みの「システムは正常にシャットダウンする前に再起動しました。このエラーは、システムの応答の停止、クラッシュ、または予期しない電源の遮断により発生する可能性があります。」というイベントが記録されます。

    そのため、この方法ではメモリなども完全にクリアされ、Windows側にしてみればいつの間にか再起動されて「今北産業」の状態です。

    それゆえに、もしもカーネルまで完全に停止した場合には意図的にBSODを発生させて、メモリ内容をダンプすることが理想と言えます。(もちろんダンプを分析するには技術が求められますので、初心者に推奨される方法ではありませんが)。

    それにしても「Windowsを正常に停止させるためにBSODを発生させる」というのは一見すると矛盾しているようにも思われますが、 これはBSODが「システムが暴走したため発生するもの」ではなく「システムが暴走するのを防ぐために発生するもの」である点に立ち返って考えてみれば、ある意味で当然ともいえます。

    いわばOSがパニックにより暴走してデータ喪失や物理的な破壊を起こしそうになったとき、むしろ自ら強制的かつ安全にシステムを終了させることで、自己破壊を防ごうという思慮がブルースクリーンの存在の根底にあるのです。


    さて、肝心なBSODの発生方法ですが、カーネル全体が完全に停止している状態からユーザーの操作でBSODを発生させるには、予めレジストリで専用のキーを有効化しておく必要があります。

    なお、レジストリの操作を誤ると最悪の場合にはWindowsが起動しなくなる可能性がありますので、十分に注意してください。


    まず、レジストリエディタ(regedit.exe)で以下の二つのキーを開きます。

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\i8042prt\Parameters
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\kbdhid\Parameters

    次に、それぞれのキーに以下のエントリを追加してください。

    値の名前: CrashOnCtrlScroll
    データ型: REG_DWORD
    値: 1

    上記の画像のようにエントリを追加した後、レジストリエディタを閉じて端末を再起動すると手動でBSODを発生させる機能が有効化されます。

    これにより、もし次にPCがフリーズしたとき、右のCtrlキーを押しながらスクロールロックキーを2回押すことで、BSODが発生してメモリのダンプファイルを生成できます。


    そして、メモリのダンプファイルを作成した後は、ダンプの解析を行って原因となるプログラムの特定を行わなければなりません。とはいえ専用のユーティリティが提供されていますので、根気さえあれば知識に乏しくても不可能ではありません。

    ダンプチェックユーティリティ(Dumpchk.exe)は、Windows用デバッグツールに含まれていますので、まずはこちらを入手してください。

    メモリのダンプファイルを用意して、引数にパスを渡すことで解析を行えます。

    DumpChk [-y SymbolPath] DumpFile

    以下は、Microsoft Learnのドキュメントに基づくDumpChkの出力例です。

    C:\Debuggers> dumpchk C:\mydir\dumpfile1.dmp 
    
    Loading dump file C:\mydir\dumpfile1.dmp
    
    Microsoft (R) Windows Debugger Version 6.9.0003.113 X86
    Copyright (C) Microsoft. All rights reserved.
    
    
    Loading Dump File [C:\mydir\dumpfile1.dmp]
    User Mini Dump File with Full Memory: Only application data is available
    
    Symbol search path is: srv*C:\CODE\LocalStore*\\symbols\symbols
    Executable search path is: 
    Windows Vista Version 6000 MP (2 procs) Free x86 compatible
    Product: WinNt, suite: SingleUserTS
    Debug session time: Tue Jun 17 02:28:23.000 2008 (GMT-7)
    System Uptime: 0 days 15:43:52.861
    Process Uptime: 0 days 0:00:26.000
    ...
    This dump file has an exception of interest stored in it.
    The stored exception information can be accessed via .ecxr.
    
    ----- User Mini Dump Analysis
    
    MINIDUMP_HEADER:
    Version         A793 (6903)
    NumberOfStreams 12
    Flags           1826
                    0002 MiniDumpWithFullMemory
                    0004 MiniDumpWithHandleData
                    0020 MiniDumpWithUnloadedModules
                    0800 MiniDumpWithFullMemoryInfo
                    1000 MiniDumpWithThreadInfo
    
    Streams:
    Stream 0: type ThreadListStream (3), size 00000064, RVA 000001BC
      2 threads
      RVA 000001C0, ID 1738, Teb:000000007FFDF000
      RVA 000001F0, ID 1340, Teb:000000007FFDE000
    Stream 1: type ThreadInfoListStream (17), size 0000008C, RVA 00000220
      RVA 0000022C, ID 1738
      RVA 0000026C, ID 1340
    Stream 2: type ModuleListStream (4), size 00000148, RVA 000002AC
      3 modules
      RVA 000002B0, 00400000 - 00438000: 'C:\CODE\TimeTest\Debug\TimeTest.exe'
      RVA 0000031C, 779c0000 - 77ade000: 'C:\Windows\System32\ntdll.dll'
      RVA 00000388, 76830000 - 76908000: 'C:\Windows\System32\kernel32.dll'
    Stream 3: type Memory64ListStream (9), size 00000290, RVA 00001D89
      40 memory ranges
      RVA 0x2019 BaseRva
      range#    RVA      Address      Size
           0 00002019    00010000   00010000
           1 00012019    00020000   00005000
           2 00017019    0012e000   00002000
    
     (additional stream data deleted)   
    
    Stream 9: type UnusedStream (0), size 00000000, RVA 00000000
    Stream 10: type UnusedStream (0), size 00000000, RVA 00000000
    Stream 11: type UnusedStream (0), size 00000000, RVA 00000000
    
    
    Windows Vista Version 6000 MP (2 procs) Free x86 compatible
    Product: WinNt, suite: SingleUserTS
    kernel32.dll version: 6.0.6000.16386 (vista_rtm.061101-2205)
    Debug session time: Tue Jun 17 02:28:23.000 2008 (GMT-7)
    System Uptime: 0 days 15:43:52.861
    Process Uptime: 0 days 0:00:26.000
      Kernel time: 0 days 0:00:00.000
      User time: 0 days 0:00:00.000
    PEB at 7ffd9000
        InheritedAddressSpace:    No
        ReadImageFileExecOptions: No
        BeingDebugged:            Yes
        ImageBaseAddress:         00400000
        Ldr                       77a85d00
        Ldr.Initialized:          Yes
        Ldr.InInitializationOrderModuleList: 002c1e30 . 002c2148
        Ldr.InLoadOrderModuleList:           002c1da0 . 002c2138
        Ldr.InMemoryOrderModuleList:         002c1da8 . 002c2140
                Base TimeStamp                     Module
              400000 47959d85 Jan 21 23:38:45 2008 C:\CODE\TimeTest\Debug\TimeTest.exe
            779c0000 4549bdc9 Nov 02 02:43:37 2006 C:\Windows\system32\ntdll.dll
            76830000 4549bd80 Nov 02 02:42:24 2006 C:\Windows\system32\kernel32.dll
        SubSystemData:     00000000
        ProcessHeap:       002c0000
        ProcessParameters: 002c14c0
        WindowTitle:  'C:\CODE\TimeTest\Debug\TimeTest.exe'
        ImageFile:    'C:\CODE\TimeTest\Debug\TimeTest.exe'
        CommandLine:  '\CODE\TimeTest\Debug\TimeTest.exe'
        DllPath:      'C:\CODE\TimeTest\Debug;C:\Windows\system32;C:\Windows\system;
        Environment:  002c0808
            =C:=C:\CODE
            =ExitCode=00000000
            ALLUSERSPROFILE=C:\ProgramData
            AVENGINE=C:\PROGRA~1\CA\SHARED~1\SCANEN~1
            CommonProgramFiles=C:\Program Files\Common Files
            COMPUTERNAME=EMNET
            ComSpec=C:\Windows\system32\cmd.exe
            configsetroot=C:\Windows\ConfigSetRoot
            FP_NO_HOST_CHECK=NO
            HOMEDRIVE=C:
            NUMBER_OF_PROCESSORS=2
            OS=Windows_NT
            Path=C:\DTFW\200804~2.113\winext\arcade;C:\Windows\system32
            PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
            PROCESSOR_ARCHITECTURE=x86
            PROCESSOR_IDENTIFIER=x86 Family 6 Model 15 Stepping 13, GenuineIntel
            PROCESSOR_LEVEL=6
            PROCESSOR_REVISION=0f0d
            ProgramData=C:\ProgramData
            ProgramFiles=C:\Program Files
            PROMPT=$P$G
            PUBLIC=C:\Users\Public
            RoxioCentral=C:\Program Files\Common Files\Roxio Shared\9.0\Roxio Central33\
            SESSIONNAME=Console
            SystemDrive=C:
            SystemRoot=C:\Windows
            USERDNSDOMAIN=NORTHSIDE.COMPANY.COM
            USERDOMAIN=NORTHSIDE
            USERNAME=myname
            USERPROFILE=C:\Users\myname
            WINDBG_DIR=C:\DTFW\200804~2.113
            windir=C:\Windows
            WINLAYTEST=200804~2.113
            _NT_SOURCE_PATH=C:\mysources
            _NT_SYMBOL_PATH=C:\mysymbols
    Finished dump check

    おまけ: メモリを冷凍して読みだすことは可能か

    もし今お使いの端末がカーネルまで完全にフリーズし、絶対に保存しなければならないデータがあり、しかしBSODの手動発生キーを登録しておらず、カーネルモードのデバッグもしていない…という場合に、最後の救世主となりうるのが「コールドブート攻撃」です。

    これはRAMのチップにおける物理的特性を利用したもので、RAMに保存されているデータは温度に比例する速さで消失(放電)するという性質があります。

    これはある種の冗談のようですが、-50度など極端に低い温度まで急激に冷却することで、メモリを端末から外して放電されるまでの時間を数十秒に延ばすことができます。 そしてこの時間を利用して、端末のマザーボードからRAMチップを取り外し別の端末に取り付け、データを読み取ることでメモリ内のデータを保全することができる‥!!という訳です。

    ちなみに、これを悪用してメモリ内に保存されているパスワードやトークンなどを探索する攻撃こそが「コールドブート攻撃」です。

    これは2008年頃から知られている攻撃手法ですが、事実上ほぼ全ての機種に対して有効であり、また現に実演も行われています。


    参考文献

    「応答なし」のウィンドウなのに“応答する”のはなぜなのか?:その知識、ホントに正しい? Windowsにまつわる都市伝説(77)(1/2 ページ) - @IT

    Windows Internals Seventh Edition Part1

    DumpChk - Windows drivers

    パソコンがフリーズ。Windows 10での対処法を徹底解説 | パソコン工房 NEXMAG