Claude CodeでクラッシュログipsをdSYMで読み解く — atosで該当ソース行へ
iOSのクラッシュログ(.ips)は、dSYMとatosで関数名と行番号に解決できます。Claude Codeに渡す前に落とす項目と、Bashで解決を回す手順をまとめます。
シンボリケートとは何か — 16進のアドレスを関数名と行番号に戻す作業
シンボリケート(symbolication)は、クラッシュログのスタックにある16進アドレスを、関数名・ソースファイル名・行番号へ戻す作業です。
iOSのクラッシュログは、拡張子が .ips のファイルで届きます。スタックの各行が 0x1022cbfa8 0x1022c0000 + 49064 のようなアドレスだけなら、未解決の状態です。Appleのドキュメントは、未解決のログは「ほとんど役に立たない」と位置づけ、診断には少なくとも一部が解決されたログが要るとしています。
解決に必要なのは、ログと、そのビルドに対応するdSYM(デバッグシンボルを収めたバンドル)の2つです。SwiftUIアプリの開発にClaude Codeを使っている場合は、Claude CodeとXcodeでSwiftUI iOSアプリを開発する手順の環境がそのまま前提になります。ここにClaude Codeを噛ませると、次の作業を任せられます。
- ログから対象フレームを取り出し、
atosを1行ずつ回す - 解決できた行のソースを読み、クラッシュの原因を絞る
- 解決できなかった理由(UUIDの不一致、dSYMの欠落)を切り分ける
一方で、.ips にはデバイスやアプリの識別情報が入っています。Claude Codeに渡す範囲を先に決めておくのが、この作業の肝です。
.ipsは2つのJSONでできている — どこを読めばよいか
iOS 15とmacOS 12以降、クラッシュログはJSONで保存されます。Appleのドキュメントによると、.ips には2つのJSONオブジェクトが入っています。
- 1行目: IPSメタデータ(
name、bundleID、bug_typeなど) - 2行目以降: クラッシュレポート本体
クラッシュログの種別は bug_type が 309 です。一般的なJSONパーサーは単一のオブジェクトを想定するため、そのまま json.load に渡すと失敗します。1行目と残りを分けて読みます。
シンボリケートで使う場所は、レポート本体の次の3つです。
| キー | 中身 | 使い道 |
|---|---|---|
threads[].frames[] | 中身imageIndex と imageOffset | 使い道どのバイナリの何バイト目か |
usedImages[] | 中身base(ロードアドレス)、uuid、arch、name | 使い道atosの -l と、dSYM探しのUUID |
faultingThread | 中身クラッシュしたスレッドの番号 | 使い道最初に読むスタックの特定 |
フレームのランタイムアドレスは、usedImages の base に imageOffset を足した値です。JSONの数値は10進数なので、atosに渡すときは16進に直します。
dSYMはUUIDで探す — 合わないと解決しない
dSYMは、ビルドごとに固有のUUIDを持ちます。.ips の usedImages[].uuid と、dSYM側のUUIDが一致したときだけ、シンボルが引けます。アプリのバージョン番号が同じでも、ビルドをやり直せばUUIDは変わります。
dSYM側のUUIDは dwarfdump で見られます。
dwarfdump --uuid MyApp.app.dSYM出力の例は次の形です(手元で小さなCプログラムをビルドして確認した値です)。
UUID: E2331AAE-7248-38D8-8B6C-A0EF9557F774 (arm64) crash.dSYM/Contents/Resources/DWARF/crashログの uuid は小文字でハイフン付きです。大文字小文字だけを揃えて見比べます。
Appleのドキュメントは、dSYMの探し方も案内しています。見つからないときはSpotlightで探し、サードパーティのフレームワークやパッケージを使っているなら、その開発元にdSYMを問い合わせる流れです。App Store Connectにデバッグシンボルを含めてアップロードしていれば、XcodeのCrashesオーガナイザが自動で解決します。
atosで1フレームずつ解決する
コマンドラインで1アドレスだけ解決するなら atos です。Appleのドキュメントが示す書式は次のとおりです。
atos -arch <arch> \
-o <dSYM>/Contents/Resources/DWARF/<binary> \
-l <load-address> -i <address>-o には、.dSYM バンドルの中にあるDWARFファイルまでのパスを渡します。バンドルのパスだけでは足りません。-i はインライン展開された関数も表示させるフラグで、-l はログの base を16進にした値です。
手元で確かめた結果
動作を確かめるため、次の手順で小さなバイナリを作りました(macOS 26.3.1、Xcode 26.6)。
clang -g -O0 -arch arm64 -c crash.c -o crash.o
clang -arch arm64 crash.o -o crash
dsymutil crash
atos -arch arm64 \
-o crash.dSYM/Contents/Resources/DWARF/crash \
-l 0x100000000 -i 0x100000534結果は次の1行でした。
pick (in crash) (crash.c:2)関数名と、ソースの crash.c:2 まで引けています。
最初は clang -g -o crash crash.c と一発でビルドしたところ、dsymutil が「no debug symbols in executable」と警告し、atos は main (in crash) + 48 のように関数名とオフセットしか返しませんでした。一発ビルドでは中間のオブジェクトファイルが消え、dsymutil がデバッグ情報を集められなかったためです。手元のスクリプトでdSYMを作るときは、行番号が出るかを最初に確かめておくと安心です。
Appleが案内するほかの解決方法
atos のほかに、Appleのドキュメントは次の3つを案内しています。
- Xcodeで
.ipsを開く。プロジェクトを選ぶと、全スレッドのアドレスを解決しようとする xcrun crashlog <path-to-crashReport>。LLDB経由で関数名・ファイル名・行番号まで出すCrashSymbolicator.py <crashReport> -d <dSYM>。JSON形式のログに対応し、-oで出力先を指定できる
Xcodeの symbolicatecrash(Perlスクリプト)も、Xcodeアプリ内に残っています。--dsym でdSYMを足し、--output で出力先を指定する書式です。ただし、現行のAppleの解説は上の3つを軸にしています。JSON形式の .ips を扱うなら、CrashSymbolicator.py か atos を第一候補にすると迷いません。
Claude Codeに解決を回させる
Claude CodeのBashツールは、シェルコマンドをそのまま実行します。atos や dwarfdump は、初回の実行時に承認を求められることがあります。繰り返しの承認を省くなら、.claude/settings.json の permissions.allow に Bash(atos *) と Bash(dwarfdump *) を足します。そうすれば、アドレスが数十個あっても、依頼1回で順に回せます。
最初に、フレームの取り出しをスクリプトにしておきます。次は、.ips からクラッシュしたスレッドのフレームを取り、自分のアプリのバイナリだけを atos に渡す最小例です。
import json, subprocess, sys
head, body = open(sys.argv[1]).read().split("\n", 1)
rep = json.loads(body)
images = rep["usedImages"]
for t in rep["threads"]:
if not t.get("triggered"):
continue
for i, f in enumerate(t["frames"]):
im = images[f["imageIndex"]]
if im["name"] != sys.argv[2]:
continue
addr = im["base"] + f["imageOffset"]
out = subprocess.run(
["atos", "-arch", im["arch"], "-o", sys.argv[3],
"-l", hex(im["base"]), "-i", hex(addr)],
capture_output=True, text=True).stdout.strip()
print(i, hex(addr), im["uuid"], "->", out)動作確認には、上のバイナリに合わせて自作した最小の .ips を使いました。実機のクラッシュではなく、構造だけをAppleの説明に合わせた確認用のファイルです。結果は次の形でした。
0 0x102a6453c e2331aae-7248-38d8-8b6c-a0ef9557f774 -> pick (in crash) (crash.c:2)ログ側のロードアドレス(0x102a64000)を -l に渡すと、ビルド時のアドレスとは別の位置でも、同じ関数に解決されます。
CLAUDE.mdに書いておく手順
毎回同じ手順を口頭で説明する代わりに、プロジェクトの CLAUDE.md に書いておけます。次は書き方の一例です。
## クラッシュログの解析
- 解析対象は `crash-slim/` の `.ips` だけ。`crash-raw/` は読まない
- 手順は、(1) `usedImages` のUUIDと `dwarfdump --uuid` の出力を突き合わせる
(2) 不一致ならそこで止まり、どのビルドのdSYMが要るか報告する
(3) 一致したら `tools/ips_frames.py` で自アプリのフレームを解決する
- 解決後は、該当ファイルの該当行の前後を読み、原因の仮説を述べる
- 仮説は事実と分けて書く。コードの修正は依頼があるまでしない(2)で止まる指示が効きます。UUIDが合わないまま解決を試みても、信頼できる結果は得られないからです。
解決した行から原因へ辿る
関数名と行番号が出たら、Claude Codeにそのファイルを読ませます。依頼は具体的なほうが実りがあります。
@crash-slim/Demo.ips の faultingThread をシンボリケートした結果が
ViewController.swift:121 を指しています。
その行と呼び出し元を読んで、クラッシュ条件の仮説を3つまで挙げてください。
EXC_BAD_ACCESS と SIGSEGV の情報も根拠に使ってください。@ でファイルを参照する書き方は、Claude Codeの公式ドキュメントにある基本機能です。.ips の exception と termination も、仮説の材料になります。
Appleの解説には、解決結果の読み方も載っています。Claude Codeに読ませるときも、人間が読むときも、次の3つは覚えておくと役立ちます。
- ファイル名が
<compiler-generated>なら、コンパイラが作ったコードです。クラッシュしたスレッドの最上段なら、直前の数フレームに手がかりがあります - 行番号が
0なら、最適化(インライン展開など)で特定の行に対応しない状態です。関数名は手がかりになります - フレーム0は実行中の命令、フレーム1以降は「関数から戻ったあとに実行される最初の命令」のアドレスです
最後の点は、呼び出し元の行を読むときに効きます。フレーム1以降の行番号は、呼び出し文のすぐ次を指す場合があります。
ログを渡す範囲 — 個人情報を落とす
.ips は、そのままClaude Codeに読ませないほうが無難です。Appleのドキュメントに載っている項目には、端末やユーザーに結びつく値があります。
| 項目 | 中身 | 判断 |
|---|---|---|
crashReporterKey | 中身端末ごとの匿名ID。同じ端末のログは同じ値 | 判断解析に不要。落とす |
deviceIdentifierForVendor | 中身TestFlightビルドで、端末と開発元の組み合わせのID | 判断解析に不要。落とす |
procPath、usedImages[].path | 中身バイナリのパス。macOSがユーザー名の部分を置換する | 判断落とすか置換する |
vmSummary、vmregioninfo | 中身メモリ領域の情報 | 判断メモリ系のクラッシュ以外は不要 |
asi | 中身アプリ独自のログ文字列 | 判断内容を確認する。アプリが書いた文言がそのまま入る |
最後の asi はAppleが「追加のアプリ固有ログ」と説明する領域です。アプリのコードが何を書き込んでいるかは、アプリごとに違います。メールアドレスやトークンを書き込むコードがないか、一度見ておく価値があります。
落とす作業もスクリプトにできます。次は、解析に必要なキーだけを残す最小例です。
import json, sys
head, body = open(sys.argv[1]).read().split("\n", 1)
rep = json.loads(body)
keep = ["procName", "bundleInfo", "exception", "termination",
"faultingThread", "lastExceptionBacktrace", "asi",
"usedImages", "threads", "osVersion", "cpuType", "captureTime"]
out = {k: rep[k] for k in keep if k in rep}
for im in out.get("usedImages", []):
im["path"] = "<redacted>"
print(json.dumps(out, indent=1))asi は残していますが、内容の確認後に残すかどうかを決めてください。
生ログをClaude Codeから遠ざける
生の .ips を crash-raw/ に置き、.claude/settings.json に Read の拒否ルールを足します。
{
"permissions": {
"deny": [
"Read(./crash-raw/**)"
]
}
}Read(./secrets/**) の形は、権限ドキュメントが示す書き方です。ルールを書いたあとに警告が出たときは、Claude Codeの警告「matches no tool」の意味で原因を切り分けられます。このルールは、Claudeの組み込みのファイルツールと、cat や head などBash内のファイルコマンド、プロンプト中の @ 参照に効きます。
一方で、ファイル名を渡さずに読むコマンドや、PythonやNodeのスクリプトがファイルを開く場合には、効きません。たとえば、slim.py crash-raw/Demo.ips をBashで走らせると、スクリプト自身は生ログを読めます。「Claudeが生ログを直接読まない」という線引きには使えますが、「どのプロセスも読めない」保証ではありません。後者が必要なときは、権限ドキュメントがサンドボックスの有効化を案内しています。
運用の流れは、次のとおりです。
- 生の
.ipsをcrash-raw/に置く(Claudeからは読めない) slim.pyで必要なキーだけのcrash-slim/版を作る- Claude Codeには
crash-slim/だけを見せる
ソースコードそのものが社外秘のときは、.ips の扱いとは別の話になります。コードを読ませてよいかは、契約や社内規程で決めます。
つまずきやすい点
UUIDが合っているのに解決されない。 -o に .dSYM バンドルのパスだけを渡していないか確認します。DWARFファイル(Contents/Resources/DWARF/<binary>)まで指定します。
アドレスがずれる。 -l に、ログの base を16進にした値を渡します。base はJSONでは10進です。python3 -c "print(hex(4339417088))" のように変換します。
OSのフレームが解決されない。UIKitCoreなど、OS側のフレームは、そのOSバージョンのデバイスシンボルが要ります。Appleのドキュメントは、Xcodeでデバイスシンボルをダウンロードするよう案内しています。自分のアプリの原因を探すだけなら、OS側のフレームは未解決でも進められる場合があります。
スタックがOSのフレームばかりになる。OSのフレームが並んでいても、アプリの呼び出しがきっかけになっていることがよくあります。lastExceptionBacktrace や他スレッドのフレームを遡って、アプリのフレームを探します。見つかったフレームの行を読むのが、次の一手です。
まとめ
.ips の解決は、UUIDの突き合わせ、ロードアドレスの指定、DWARFファイルまでのパスという3つの条件が揃えばatosで済みます。Claude Codeには、この手順を CLAUDE.md に書き、UUIDが合わなければ止まるよう指示するのが堅実です。
渡す前に生ログを絞り、端末ごとのIDとパスを落とし、asi の中身を確かめてから crash-slim/ だけを読ませる運用なら、解析を速く回しつつ、情報の持ち出しを抑えられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/common-workflows
- code.claude.com/docs/en/permissions
- code.claude.com/docs/en/tools-reference
- developer.apple.com/documentation/xcode/adding-identifiable-symbol-names-to-a-…
- developer.apple.com/documentation/xcode/interpreting-the-json-format-of-a-cras…
- developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report