1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

起動1秒で落ちるアプリを、デバッガなしでPythonだけで原因特定する

1
Last updated at Posted at 2026-09-20

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%死ぬ」。この違いが後の切り分けを決めます。

発症のタイムライン。前日まで正常に動いていたアプリが、ある瞬間から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件が突出。他アプリも落ちているが日付もシグネチャも別

容疑者の絞り込み。グラフィックドライバ・常駐ソフト・OS更新・DB破損・アプリのバージョンをすべて証拠で外し、最後にデータ側が残る

そして最新版へ上げた(ここが分岐点)

リリースノートに「中断後のファイル転送の復旧」「失敗したアップロード項目の新しい管理システム」とあり、いかにも効きそうだったので 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

決め手となった証拠。バージョンもビルド日時も.NETランタイムも違う2つのビルドで、Windowsが算出したスタックハッシュが完全に一致した

半年ぶんの変更が入った別ビルドで、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 ファイルから直接バイト列を読むのが確実です。

この数え方の但し書き(結論は変わりませんが、書いておくべきものです):

  1. スタックは使い回されるため、すでに戻り終えた呼び出しが書き残した古い値が混じります。出現数は呼び出しの深さではありません
  2. 判定は base <= addr < base+size なので、.rdata / .data を指すデータポインタも数に入ります。「コードアドレス」ではなく「モジュールのイメージ範囲を指す値」です
  3. ミニダンプのスタック領域は切り詰められることがあります

なお 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()

手順:

  1. アプリを終了(プロセスが残っていないことを確認)
  2. data.db / data.db-wal / data.db-shm をバックアップ
  3. 上のスクリプトを実行
  4. 起動

結果: クラッシュゼロ。仮想ドライブも正常マウント。12分監視で異常なし。

失うのは「中断されたアップロード」だけで、クラウド上の既存ファイルには影響しません。退避したキューを書き戻してはいけません(戻すと再発します)。


まとめ:転用できる型

  1. 発症時刻をイベントログで確定する。 「徐々に」か「ある瞬間から100%」かで、その後の切り分けが変わる
  2. 例外アドレスとパラメータを読む。 アドレス0+パラメータ0x8 はアドレス0への命令フェッチ。null 関数ポインタか、スタック破壊による ret か、ここでは区別できない
  3. ログが空なのは情報。 マネージド例外のログが無い=ネイティブ層で即死した、という手がかり
  4. 再インストールで直らないことは証拠。 バイナリを入れ替えて症状もスタックハッシュも不変なら、原因はデータ側
  5. ダンプはデバッガ無しで読める。 minidump + スタック上の値のモジュールマッピングで、支配的なモジュールは特定できる
  6. ダンプは消える前に確保する。 次の起動で消えているアプリがある

予防

仮想ドライブ(クラウド同期のマウントポイント)に、ダウンローダーで直接大きなファイルを書き込まない。 ローカルディスクに落としきってからコピーする。書きかけのファイルを同期対象に置かないのが確実です。

なお、キューの内容が何であれアドレス0へ分岐して即死する(null 関数ポインタにせよスタック破壊にせよ)のはアプリ側の防御不足で、本来はベンダーが修正すべきものです。ダンプはアプリが自動送信済みでした。


この調査で、人間が入力したのはこれだけ

ここまでの調査は、AIエージェント(Claude Code)に実行させています。私が実際に打ち込んだ入力は、全部で4つでした。

1回目

pcloudのアプリが強制終了してしまう

これだけです。バージョンも、エラーの内容も、いつから起きているかも書いていません。

ここからエージェントが、イベントログの照会 → 例外コードの解読 → 発症時刻の特定 → 再現テスト(起動して1秒で死ぬことの確認)まで自分で進めました。

2回目 — 「最新版を上書きインストールする/同期キューを退避して検証する/サポートに出す/未同期データを精査する」の4択を示されたので、1つ目を選びました。

最新版を上書きインストール

3回目

インストールしました

最新版でも同じように死んだ結果を受けて、エージェントは crashes フォルダのダンプを見つけ、pip install minidump を実行し、スタックを解析して pSyncLib.dll を特定しました。

4回目 — キュー退避の実行許可を求められたので、承認しました。

Aでやってください

これで復旧しています。


技術的な指示は一つもしていません。どのログを見るか、ダンプをどう読むか、どのテーブルを疑うか、何ひとつこちらからは言っていない。「どの仮説を先に潰すか」を選んだだけです。

逆に言うと、人間の側に残った仕事もはっきりしました。

  • 止める判断 — 「再インストールで直らなかった」時点で諦めるか、続けるか
  • リスクの判断 — 未同期のアップロード73件を捨ててよいか(これはエージェントが勝手に決めてはいけない)
  • どの仮説から潰すか — 4択のどれを先に試すか

エージェントは調べ切る力を持っていますが、何を失ってよいかは決められません。そこだけは人間が持っておく必要がある、というのが今回いちばんの実感でした。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?