Windows デスクトップアプリが起動して約1秒でクラッシュするようになり、再インストールしても直らない状態から、原因を特定して復旧させた記録です。
実例は pCloud Drive(クラウドストレージの同期クライアント)ですが、手順自体は「ログを残さずネイティブ層で死ぬ Windows アプリ」全般に使えます。WinDbg も Visual Studio も入れずに、pip install minidump だけで済ませます。
TL;DR
- 症状: 起動1秒で無言終了、エラーダイアログなし、アプリのログにも何も残らない
- 例外:
0xc0000005/ 例外アドレス0x0/ 障害モジュールunknown/ パラメータ[0x8, 0x0]→ アドレス0への命令フェッチ(最も素直な説明は null 関数ポインタの呼び出し) - 最新版に上げても同じ例外・同じスタックハッシュで死んだ → バイナリを入れ替えても不変=原因はデータ側
- ダンプのスタックを解析 → 同期エンジン
pSyncLib.dllのワーカースレッドが単独で死亡 - 原因: ローカルDB (SQLite) の同期キューに、処理すると必ず落ちるタスクが残留
- 対処: キュー4テーブルを JSON に退避してから空にする → 復旧
環境
| 項目 | 値 |
|---|---|
| OS | Windows 11 (build 10.0.26200) |
| 問題のアプリ | pCloud Drive 5.0.20.0 → 5.2.2.0 |
| .NET | 9.0.12 (CoreCLR 9.0.1225.60609) → 9.0.18 (CoreCLR 9.0.1826.31522) |
| Python | 3.10 |
| 追加インストール |
minidump のみ |
Step 1. 「いつから壊れたか」をイベントログで確定する
心当たりを潰す前に、記録から発症時刻を出します。
Get-WinEvent -FilterHashtable @{
LogName='Application'
ProviderName='Application Error'
Id=1000
StartTime=(Get-Date).AddDays(-14)
} -ErrorAction SilentlyContinue | Where-Object { $_.Message -match 'pCloud' } |
Select-Object TimeCreated | Sort-Object TimeCreated
結果、14日間でクラッシュしたのは発症当日のみ、計12回。
Application Error(ID 1000) に絞るのが肝心です。1回のクラッシュにつきWindows Error Reporting(1001) と.NET Runtime(1026) からも1件ずつ記録が出るため、3プロバイダ横断で引くと 12×3 = 36行返ってきて数が合わなくなります。-ErrorAction SilentlyContinueが無いと0件時に例外が飛ぶ点にも注意。
アプリ自身のログを見ると、当日 05:54:59 の定期ログまでは正常で、次に出るはずの 06:54 が無く、その間の 06:15:33 が初回クラッシュでした。発症の20分前まで健全だったことになります。
「徐々に不安定」ではなく「ある瞬間から100%死ぬ」。この違いが後の切り分けを決めます。
アプリ単体の問題かを確かめるには、アプリ名+バージョン別に全件を集計します。
Get-WinEvent -FilterHashtable @{
LogName='Application'; ProviderName='Application Error'
StartTime=(Get-Date).AddDays(-14)
} | ForEach-Object {
($_.Message -split "`r`n")[0] -replace 'Faulting application name: ','' -replace ', time stamp.*',''
} | Group-Object | Sort-Object Count -Descending
実測結果:
11 pCloud.exe, version: 5.0.20.0
5 IceMon.exe, version: 3.3.3.0
2 Setup.exe_Setup.exe / 2 rife-ncnn-vulkan.exe
1 mc-fw-host.exe / 1 uTorrent.exe / 1 Aac3572MbHal_x86.exe
1 pCloud.exe, version: 5.2.2.0
他アプリもゼロではありませんが、日付も例外シグネチャも別物で、同種のクラッシュが集中しているのは pCloud だけと言えます。
このスクリプトは
Faulting application name:という英語のメッセージ本文に依存します。ロケールによっては文言が変わるので、その場合は-replaceを環境に合わせてください。
Step 2. 例外の4つの値を読む
Application Error (ID 1000) の中身がこれでした。
Faulting application name: pCloud.exe, version: 5.0.20.0, time stamp: 0x69340000
Faulting module name: unknown, version: 0.0.0.0, time stamp: 0x00000000
Exception code: 0xc0000005
Fault offset: 0x0000000000000000
そして WER レポート (C:\ProgramData\Microsoft\Windows\WER\ReportArchive\...\Report.wer) の例外パラメータが [0x8, 0x0]。
Fault offsetは本来「障害モジュールのベースからの相対オフセット」であり、絶対アドレスではありません。 今回は障害モジュールが解決できていないため絶対アドレスが入っており、たまたま例外アドレスと一致しています。絶対アドレスが欲しいなら.NET Runtime(1026) のexception addressを見る方が確実です。
読み方は次のとおりです。
| 値 | 意味 |
|---|---|
0xc0000005 |
EXCEPTION_ACCESS_VIOLATION |
例外アドレス 0x0
|
アドレス0へ命令フェッチが起きた |
パラメータ[0] 0x8
|
アクセス種別。0=読み込み / 1=書き込み / 8=命令フェッチでのアクセス違反。NX/DEP が有効な x64 では、未マップのアドレス0への分岐もこの 8 になる |
パラメータ[1] 0x0
|
アクセスできなかったアドレスそのもの |
障害モジュール unknown
|
アドレス0はどのモジュールの範囲でもないため。なお unknown はアドレス0に限らず、JIT生成コードなどモジュールに裏打ちされないアドレス全般で出る |
→ **最も素直な説明は「null になった関数ポインタを call した」**です。
ただし断定はできません。スタックが破壊されて保存された戻りアドレスが0になり ret した場合も、まったく同じシグネチャになります。この2つはここまでの証拠では区別できません(WER がこれを BEX64 に分類しているのも、その両義性の表れです)。決着は Step 4 のスタック解析でつけます。
12回に共通していたもの
WER のバケットIDは3種類に分かれます。バケットIDはアプリの版とタイムスタンプを含むので、版が違えば必ず別値になり、ここは一致しなくて当然です。
一致していたのはこちらでした。
| 項目 | 全12回の値 |
|---|---|
| 障害モジュール名 | StackHash_ac46 |
| 例外データ | 0000000000000008 |
| 追加情報1〜3(スタックハッシュ) |
ac46 / ac462b2fd07cc853b652b5104a1cf381 / 6b3e
|
これらは 5.0.20 と 5.2.2 をまたいでも完全一致します。 この事実は Step 3 で効いてきます。
「例外の記録が無い」ことも手がかり
このアプリは .NET (WPF) 製で log4net のログを吐きます。クラッシュ時刻のログは次のとおりで、例外やスタックトレースの記述が一行もありません(ERROR 行はありますが中身は "Show main window" で、起動のたびに出る無関係な出力です)。
10:47:48,342 [2] INFO App.xaml.cs:235 235- start
10:47:48,785 [2] INFO NotifyIconViewModel:531 531- Set tray baloon
10:47:48,898 [2] ERROR WindowHelper.cs:281 281- Show main window
10:47:49,698 [2] INFO CryptoViewModel.cs:151 151- CRYPTO_STATUS_NEW
過去の別の不具合(2026-07-23 の COMException)では FATAL と例外の型名・スタックトレースがきちんと残っていました。今回それが無い。
一方で .NET Runtime (1026) は12回すべて出ています。ただし中身はこれだけです。
Description: The process was terminated due to an unhandled exception.
Exception Info: exception code c0000005, exception address 0000000000000000
マネージド例外の型名もスタックトレースもありません。 managed 例外が未処理で落ちた場合ここに入るはずの情報が無く、生の SEH コードだけが記録されている——これが「managed 層ではなくネイティブ層で死んだ」ことを示唆します。
ただし「例外ログが無い=ネイティブで死んだ」は一般論としては言い切れません(.NET では managed コード内の AV も corrupted-state 例外としてハンドラに届きませんし、ログがバッファリングされていれば最後の行は失われます)。確定させるのは次の Step 4 のスタック解析であって、ログの不在ではありません。
Step 3. 先に「よくある原因」を証拠で外す
| 仮説 | 検証方法 | 結果 |
|---|---|---|
| GPU / ディスプレイドライバ | System ログを Display|dxgkrnl|nvlddmkm|amdkmdag|igfx で絞る |
該当イベント0件 |
| OS更新 | Get-HotFix | Sort InstalledOn -Desc |
直近更新は5日前。その後4日間正常稼働 |
| ローカルDB破損 | PRAGMA integrity_check |
ok |
| 環境全体の問題 | Step 1 の全アプリ集計 | pCloud 12件が突出。他アプリも落ちているが日付もシグネチャも別 |
そして最新版へ上げた(ここが分岐点)
リリースノートに「中断後のファイル転送の復旧」「失敗したアップロード項目の新しい管理システム」とあり、いかにも効きそうだったので 5.0.20 → 5.2.2 に上書きインストールしました。
結果: 起動1秒で、まったく同じ例外で死亡。
これは失敗に見えて、実は最も価値のある観測でした。
アプリのバイナリも .NET ランタイムも別バージョンに置き換わったのに症状が不変
→ 変数はコードではなくデータ側
置き換わったものを実測で並べるとこうなります。
| 5.0.20 | 5.2.2 | |
|---|---|---|
| アプリのバージョン | 5.0.20.0 | 5.2.2.0 |
| 実行ファイルのタイムスタンプ |
0x69340000 (= 2025-12-06 10:05:52 UTC) |
0x6a300000 (= 2026-06-15 13:37:04 UTC) |
| .NET ランタイム | 9.0.12 | 9.0.18 |
| DB スキーマ |
dbversion 24 |
dbversion 28 |
旧版のタイムスタンプ変換結果は、WER レポートに別途記録されていた TargetAppVer=2025//12//06:10:05:52 と一致しました。
そして Step 2 で保留にしておいたスタックハッシュが、版を跨いで完全一致します。
| 5.0.20 | 5.2.2 | |
|---|---|---|
| 追加情報2(スタックハッシュ) | ac462b2fd07cc853b652b5104a1cf381 |
ac462b2fd07cc853b652b5104a1cf381 |
| 例外データ | 0000000000000008 |
0000000000000008 |
半年ぶんの変更が入った別ビルドで、Windows が算出する指紋が1文字も変わらない。同じ構造のスタックで同じ場所を踏んでいることの直接証拠です。
注意: 落ちているのは
pSyncLib.dllですが、旧版の同 DLL は上書きインストールで失われており、バイト単位の差分は取れていません(現物は FileVersion 5.2.2.0 / 3,448,176 bytes / 2026-08-21)。厳密に言えるのは「アプリ一式が別バージョンに置き換わった」までです。
容疑者がここで半分に落ちます。
Step 4. クラッシュダンプを Python だけで解析する
4-1. ダンプを確保する(時間との勝負)
pCloud 5.2.2 は %LOCALAPPDATA%\pCloud\crashes\*.dmp にミニダンプを出力します。
⚠️ 次回起動時にはフォルダが空になっていました。 クラッシュレポート送信後にアプリ自身が消しているものと思われます(機構は未確認)。いずれにせよ 見つけたら真っ先にコピーしてください。
アプリがダンプを吐かない場合は、WER の LocalDumps を有効化すれば取得できます(要管理者権限)。
$key = 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\<アプリ名>.exe'
New-Item -Path $key -Force | Out-Null
New-ItemProperty -Path $key -Name DumpFolder -Value 'C:\dumps' -PropertyType ExpandString -Force
New-ItemProperty -Path $key -Name DumpType -Value 2 -PropertyType DWord -Force # 2 = full dump
4-2. 例外情報とモジュール一覧
pip install minidump
import os
from minidump.minidumpfile import MinidumpFile
PATH = r"C:\dumps\app.dmp" # 確保したダンプ
mf = MinidumpFile.parse(PATH)
exc = mf.exception
for r in exc.exception_records:
er = r.ExceptionRecord
print("thread id :", hex(r.ThreadId))
print("exception code :", er.ExceptionCode)
print("exception addr :", hex(er.ExceptionAddress))
print("params :", [hex(x) for x in er.ExceptionInformation][:er.NumberParameters])
for m in sorted(mf.modules.modules, key=lambda m: m.baseaddress):
print("%-44s %016x - %016x" % (
os.path.basename(m.name), m.baseaddress, m.baseaddress + m.size))
出力(抜粋):
thread id : 0xfc4
exception code : ExceptionCode.EXCEPTION_ACCESS_VIOLATION
exception addr : 0x0
params : ['0x8', '0x0']
モジュール一覧には、疑っていた画面キャプチャ系DLLやグラフィックDLLも載っていました。ただし「ロードされている」と「クラッシュに関与した」は別です。次でそこを潰します。
4-3. スタックを読む(本命)
シンボルが無くても、スタック上の値をモジュールのアドレス範囲にマップして数えるだけで、どのモジュールが支配的かは分かります。
import os, struct
from collections import Counter
from minidump.minidumpfile import MinidumpFile
PATH = r"C:\dumps\app.dmp"
mf = MinidumpFile.parse(PATH)
raw = open(PATH, 'rb').read()
# 4-2 の例外レコードから、死んだスレッドのIDを取る
CRASH_TID = mf.exception.exception_records[0].ThreadId
mods = sorted(((os.path.basename(m.name), m.baseaddress, m.baseaddress + m.size)
for m in mf.modules.modules), key=lambda x: x[1])
def owner(addr):
for name, base, end in mods:
if base <= addr < end:
return "%s+0x%x" % (name, addr - base)
return None
th = [t for t in mf.threads.threads if t.ThreadId == CRASH_TID][0]
start, size, rva = th.Stack.StartOfMemoryRange, th.Stack.DataSize, th.Stack.Rva
data = raw[rva:rva + size] # RVA はファイル内オフセット
hits = []
for off in range(0, len(data) - 7, 8):
v = struct.unpack('<Q', data[off:off + 8])[0]
o = owner(v)
if o:
hits.append((start + off, v, o))
for a, v, o in hits:
print("[%016x] -> %016x %s" % (a, v, o))
print()
for name, cnt in Counter(o.split('+')[0] for _, _, o in hits).most_common():
print("%-30s %d" % (name, cnt))
ポイント: mf.get_reader() 経由で読むと、ダンプに採取されていないアドレスに当たった時点で Exception: Memory address 0x... is not in process memory space で落ちます。th.Stack.Rva を使って dmp ファイルから直接バイト列を読むのが確実です。
この数え方の但し書き(結論は変わりませんが、書いておくべきものです):
- スタックは使い回されるため、すでに戻り終えた呼び出しが書き残した古い値が混じります。出現数は呼び出しの深さではありません
- 判定は
base <= addr < base+sizeなので、.rdata/.dataを指すデータポインタも数に入ります。「コードアドレス」ではなく「モジュールのイメージ範囲を指す値」です - ミニダンプのスタック領域は切り詰められることがあります
なお MinidumpFile.parse() はダンプによっては ERROR:root:PEB parsing error! を stderr に出しますが、非致命で解析は続行します。
結果:
ntdll.dll+0xacaec
kernel32.dll+0x2cd87
msvcr100.dll+0x21e3b ← CRT のスレッド起動 (_beginthreadex)
pthreadVC2.dll+0x69b0 ← pthreads-win32
pSyncLib.dll+0x1dbc0
... (pSyncLib が延々と続く) ...
pSyncLib.dll+0xaa531
pSyncLib.dll 26
ntdll.dll 7
msvcr100.dll 5
KERNELBASE.dll 3
pthreadVC2.dll 1
kernel32.dll 1
pSyncLib.dll(同期エンジン本体)が26箇所で支配的。 そして——
(念のため: これは「26段の呼び出しが入れ子になっている」という意味ではありません。スタック上の8バイト値のうち26個が pSyncLib.dll の領域を指していた、というだけです。戻りアドレス以外の値も混ざります。それでも「このスレッドが何をしている最中だったか」を特定するには十分でした。)
- 画面描画系DLL: スタックに0回
- キャプチャ系の常駐ソフトDLL: スタックに0回
UIでも外部ソフトの干渉でもなく、同期処理のワーカースレッドが単独で死んでいると確定しました。
Step 5. 原因:残留した同期キュー
同期エンジンはローカルの SQLite (%LOCALAPPDATA%\pCloud\data.db) に作業キューを持っています。
稼働中はロックされるので、コピーを read-only で開いて中身を見ます。
⚠️
data.db/-wal/-shmを3点セットで、書き込み可能な場所へコピーしてから開いてください。-shmを作れない場所だとunable to open database fileになります。また、回避策としてよく紹介されるimmutable=1は WAL を無視するため、エラーを出さずに古い内容を返します(no such tableになったり、消したはずの行が見えたりする)。使わないでください。
import sqlite3
DB = "C:/backup/data.db" # コピーしたもの
con = sqlite3.connect("file:" + DB + "?mode=ro", uri=True)
cur = con.cursor()
for t in ('fstask', 'fstaskupload', 'fstaskdepend', 'fstaskfileid'):
print(t, cur.execute('SELECT COUNT(*) FROM "%s"' % t).fetchone()[0])
for r in cur.execute("SELECT id,type,status,text1 FROM fstask"):
print(r)
fstask 5
fstaskupload 73
fstaskdepend 1
fstaskfileid 0
全タスクが status=0(未処理)。対象は同じフォルダにある書き込み途中の一時ファイルでした。
時系列が噛み合います。
06:09 843MBの一時ファイルが最終更新
06:11-06:14 ローカルキャッシュが最終更新
06:15:33 初回クラッシュ
同期先の仮想ドライブに、外部ダウンローダーで直接大容量ファイルを書いていました。同期エンジンがまだ書き込み中のファイルをアップロードしようとし、リネーム・中断とレースしてキューが壊れたと考えられます(レース条件そのものは直接観測していないので、ここは推測です)。
一度この状態になると、起動のたびに同じキューを読んで同じ場所で死ぬ無限ループになります。
さらに、5.2.2 は DB を dbversion 24 → 28 へマイグレーションしていましたが、このキューはそのまま引き継がれていました。Step 3 で新品のコードが同じように死んだ理由がこれです。
なお「タスクの中身が壊れている」ことは証明できていません。観測できたのは「このキューを積んだまま起動すると必ず死に、空にすると死なない」までです。「行は正常だがアプリ側の処理に穴がある(例: 既に存在しないファイルを参照して lookup が null を返す)」でも同じ観測になります。対処は同じなので実害はありませんが、区別はついていません。
Step 6. 対処
消さずに退避します。判断を誤ったときに戻せることが大事です。
import json, os, sqlite3
DB = os.path.join(os.environ['LOCALAPPDATA'], 'pCloud', 'data.db')
OUT = "C:/backup/queue_parked.json"
TABLES = ['fstask', 'fstaskupload', 'fstaskdepend', 'fstaskfileid']
con = sqlite3.connect(DB)
con.text_factory = lambda b: b.decode('utf-8', 'replace') # 非UTF-8 の TEXT 列対策
cur = con.cursor()
dump = {}
for t in TABLES:
cols = [r[1] for r in cur.execute('PRAGMA table_info("%s")' % t)]
rows = cur.execute('SELECT * FROM "%s"' % t).fetchall()
dump[t] = {"cols": cols, "rows": rows}
with open(OUT, 'w', encoding='utf-8') as f:
json.dump(dump, f, ensure_ascii=False, indent=1,
default=lambda o: {"__bytes_hex__": o.hex()}
if isinstance(o, (bytes, bytearray)) else str(o)) # BLOB 列対策
for t in TABLES:
cur.execute('DELETE FROM "%s"' % t)
con.commit()
print(cur.execute("PRAGMA integrity_check(5)").fetchall())
con.close()
手順:
- アプリを終了(プロセスが残っていないことを確認)
-
data.db/data.db-wal/data.db-shmをバックアップ - 上のスクリプトを実行
- 起動
結果: クラッシュゼロ。仮想ドライブも正常マウント。12分監視で異常なし。
失うのは「中断されたアップロード」だけで、クラウド上の既存ファイルには影響しません。退避したキューを書き戻してはいけません(戻すと再発します)。
まとめ:転用できる型
- 発症時刻をイベントログで確定する。 「徐々に」か「ある瞬間から100%」かで、その後の切り分けが変わる
-
例外アドレスとパラメータを読む。 アドレス
0+パラメータ0x8はアドレス0への命令フェッチ。null 関数ポインタか、スタック破壊によるretか、ここでは区別できない - ログが空なのは情報。 マネージド例外のログが無い=ネイティブ層で即死した、という手がかり
- 再インストールで直らないことは証拠。 バイナリを入れ替えて症状もスタックハッシュも不変なら、原因はデータ側
-
ダンプはデバッガ無しで読める。
minidump+ スタック上の値のモジュールマッピングで、支配的なモジュールは特定できる - ダンプは消える前に確保する。 次の起動で消えているアプリがある
予防
仮想ドライブ(クラウド同期のマウントポイント)に、ダウンローダーで直接大きなファイルを書き込まない。 ローカルディスクに落としきってからコピーする。書きかけのファイルを同期対象に置かないのが確実です。
なお、キューの内容が何であれアドレス0へ分岐して即死する(null 関数ポインタにせよスタック破壊にせよ)のはアプリ側の防御不足で、本来はベンダーが修正すべきものです。ダンプはアプリが自動送信済みでした。
この調査で、人間が入力したのはこれだけ
ここまでの調査は、AIエージェント(Claude Code)に実行させています。私が実際に打ち込んだ入力は、全部で4つでした。
1回目
pcloudのアプリが強制終了してしまう
これだけです。バージョンも、エラーの内容も、いつから起きているかも書いていません。
ここからエージェントが、イベントログの照会 → 例外コードの解読 → 発症時刻の特定 → 再現テスト(起動して1秒で死ぬことの確認)まで自分で進めました。
2回目 — 「最新版を上書きインストールする/同期キューを退避して検証する/サポートに出す/未同期データを精査する」の4択を示されたので、1つ目を選びました。
最新版を上書きインストール
3回目
インストールしました
最新版でも同じように死んだ結果を受けて、エージェントは crashes フォルダのダンプを見つけ、pip install minidump を実行し、スタックを解析して pSyncLib.dll を特定しました。
4回目 — キュー退避の実行許可を求められたので、承認しました。
Aでやってください
これで復旧しています。
技術的な指示は一つもしていません。どのログを見るか、ダンプをどう読むか、どのテーブルを疑うか、何ひとつこちらからは言っていない。「どの仮説を先に潰すか」を選んだだけです。
逆に言うと、人間の側に残った仕事もはっきりしました。
- 止める判断 — 「再インストールで直らなかった」時点で諦めるか、続けるか
- リスクの判断 — 未同期のアップロード73件を捨ててよいか(これはエージェントが勝手に決めてはいけない)
- どの仮説から潰すか — 4択のどれを先に試すか
エージェントは調べ切る力を持っていますが、何を失ってよいかは決められません。そこだけは人間が持っておく必要がある、というのが今回いちばんの実感でした。


