はじめに
C 言語プログラムを実行するまでになにがおこなわれるか において、 C の実行可能ファイルの実行時の挙動をより詳しく確認したくなりました。
そのため、 gdb ならびに lldb の使い方、として独立したノートにします。
gdb & lldb で扱えるようにコンパイル時にオプションを正しく設定する
gdb & lldb は実行可能ファイルに追加されたデバッグ情報を利用します。
このデバッグ情報には 機械語の命令
がもともとの C ソースコード上のどこにあったのか、関数名・変数名は何だったか?などが記録されています。
よって、 C ソースコードからアセンブリコードを生成するときにデバッグ情報を付与する必要があります。
-g
オプションをつけてデバッグ情報を付与する-g3
にすることで最大のデバッグ情報を付与できる
-O0
オプションをつけて最適化を無効にする
これらオプションをつけてコンパイルするようにしましょう。 つけ忘れるとデバッガを起動してもただの 16 進数のアドレスしか見れず、変数の値も見れません。
url: https://www.clear-code.com/blog/2013/5/8/recommend-to-use-gdb-with-g3-option-for-debugging.html
title: "GDBでデバッグするなら-g3オプション - 2013-05-08 - ククログ"
description: "RubyやPythonなどのスクリプト言語では実行中に例外が発生するとバックトレースを出力してくれます。バックトレースがあるとどこで問題が発生したかがわかるためデバッグに便利です。一方、CやC++では不正なメモリアクセスをすると、バックトレースではなくcoreを残して1終了します2。デバッガーでcoreを解析するとバックトレースを確認できます。limitやulimitでコアファイルのサイズを制限している場合はcoreを残さないこともあります。 ↩catchsegv ./a.outというようにcatchsegvコマンド経由で実行するとC/C++で書いたプログラムでもバックトレースを出力します。 ↩"
host: www.clear-code.com
favicon: https://www.clear-code.com/favicon.png
image: https://www.clear-code.com/images/icon.png
デバッガがどうやってプログラムをデバッグするのか
サブプロセスとして起動する
gdb & lldb は C で書かれた実行可能ファイル
を sub-process として起動できます。
gdb a.out
lldb a.out
そして、 gdb & lldb から ptrace という system-call を呼び出し、実行可能ファイルプログラムのプロセスを操作します。 ptrace による接続によって、一時停止や再開、メモリやレジスタの読み取りが行えます。
url: https://itchyny.hatenablog.com/entry/2017/07/31/090000
title: "ptraceシステムコール入門 ― プロセスの出力を覗き見してみよう! - プログラムモグモグ"
description: "他のプロセスを中断せずに、その出力をミラーリングして新しくパイプで繋ぐ、そんなことはできるのでしょうか。 straceやgdbといったコマンドは一体どういう仕組みで動いているのでしょうか。 ptraceシステムコールを使い、プロセスが呼ぶシステムコールを調べて出力を覗き見するコマンドを実装してみたいと思います。 ptraceシステムコール Linuxを触っていると、いかにプロセスを組み合わせるか、組み合わせる方法をどれだけ知っているかが重要になってきます。 パイプやリダイレクトを使ってプロセスの出力結果を制御したり、コードの中からコマンドを実行して、終了ステータスを取得したりします。 プロセス…"
host: itchyny.hatenablog.com
favicon: https://itchyny.hatenablog.com/icon/link
image: https://cdn.image.st-hatena.com/image/scale/f7675cad2e068781cd1363e68335f34bffe7e73f/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn.blog.st-hatena.com%2Fimages%2Ftheme%2Fog-image-1500.png
すでに起動しているプロセスに attach する
先に C の実行可能プログラムを起動しておきます。
#ps コマンドなどで該当のプログラムの pid を獲得し、 gdb attach [pid]
などで ptrace による接続を行えばいいです。
lldb でデバッグする
url: https://scior.hatenablog.com/entry/2019/07/03/223907
title: "XcodeのLLDBデバッグでよく使う技 - しおメモ"
description: "若干話題になって出尽くしてる感がありますが、XcodeのLLDBを絡めたデバッグでよく使う手法をまとめてみました。 特定の行をスキップ 特定の行を書き換える ブレークポイントをon/offする アドレスからオブジェクトに戻して変数に格納する 組み込みメソッドに対してブレークポイントを仕込む 変数の変更を監視する その他のよく使うコマンド 特定の行をスキップ 行をスキップしたい際は、thread jumpやthread returnを活用します。 # 1行スキップ (lldb) th j --by 1 c th jがthread jumpの、cがcontinueのように一意に決まれば先頭だけで短…"
host: scior.hatenablog.com
favicon: https://scior.hatenablog.com/icon/link
image: https://cdn.image.st-hatena.com/image/scale/78d83380220806970493396d09ceb09ba6bf9783/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Fs%2Fscior%2F20190630%2F20190630233325.png
コマンド構文
url: https://lldb.llvm.org/use/tutorial.html
title: "Tutorial - 🐛 LLDB"
host: lldb.llvm.org
<noun> <verb> [-options [option-value]] [argument [argument...]]
lldb の syntax は上記のようになっています。 help コマンドを打つとわかりやすく説明が出るほか、 tab で補完がなされるため、これらを活用するとよさそうです。
ソースコードを表示する
# ファイル単位で表示する
(lldb) list main.c
1 #include <stdio.h>
2
3 #include "add.h"
4
5 int globalW = 100; // `add.h で宣言された extern int globalW` の実体を定義
6
7 int main(int argc, char **argv)
8 {
9 int sum = add(5, 8);
10
11 printf("Sum = %d\n", sum);
# 関数単位で表示する
# ややこしいが add 関数を表示している
(lldb) list add
File: /Users/ganyariya/develop/playground/c-compile-assemble/add.c
7 }
8
9 /**
10 * x は 2 倍する
11 */
12 int add(int x, int y)
13 {
14 // globalW は add.h で extern 宣言されている
15 return globalW + doubled(x) + y;
16 }
アセンブリコードを表示する
現在実行しているフレームのアセンブリコードを表示できます。 assembly dis のみでも実行できます。
-m でソースコードも絡めて表示できます。
(lldb) disassemble
main`doubled:
0x1000004f8 <+0>: sub sp, sp, #0x10
0x1000004fc <+4>: str w0, [sp, #0xc]
-> 0x100000500 <+8>: ldr w8, [sp, #0xc]
0x100000504 <+12>: lsl w0, w8, #1
0x100000508 <+16>: add sp, sp, #0x10
0x10000050c <+20>: ret
(lldb) dis -m
4 int doubled(int x)
** 5 {
main`doubled:
0x1000004f8 <+0>: sub sp, sp, #0x10
0x1000004fc <+4>: str w0, [sp, #0xc]
-> 6 return x * 2;
-> 7 }
-> 8
-> 0x100000500 <+8>: ldr w8, [sp, #0xc]
0x100000504 <+12>: lsl w0, w8, #1
0x100000508 <+16>: add sp, sp, #0x10
0x10000050c <+20>: ret
ブレークポイントを設定する
url: https://lldb.llvm.org/use/tutorial.html#setting-breakpoints
title: "Tutorial - 🐛 LLDB"
host: lldb.llvm.org
# 特定ファイルの行数を指定する
(lldb) breakpoint set --file main.c --line 9
Breakpoint 1: where = main`main + 32 at main.c:9:15, address = 0x0000000100000578
# 関数を指定する
(lldb) breakpoint set --name main
Breakpoint 2: where = main`main + 32 at main.c:9:15, address = 0x0000000100000578
# 一覧する
(lldb) breakpoint list
Current breakpoints:
1: file = 'main.c', line = 9, exact_match = 0, locations = 1
1.1: where = main`main + 32 at main.c:9:15, address = main[0x0000000100000578], unresolved, hit count = 0
2: name = 'main', locations = 1
2.1: where = main`main + 32 at main.c:9:15, address = main[0x0000000100000578], unresolved, hit count = 0
# breakpoint を消す
(lldb) breakpoint delete
Available completions:
1 -- file = 'main.c', line = 9, exact_match = 0, locations = 1
2 -- name = 'main', locations = 1
3 -- file = 'main.c', line = 10, exact_match = 0, locations = 1
(lldb) breakpoint delete 3
1 breakpoints deleted; 0 breakpoint locations disabled.
ウォッチポイントを設定する
url: https://lldb.llvm.org/use/tutorial.html#setting-watchpoints
title: "Tutorial - 🐛 LLDB"
host: lldb.llvm.org
(lldb) watchpoint set variable globalW
Watchpoint created: Watchpoint 1: addr = 0x100008000 size = 4 state = enabled type = m
declare @ '/Users/ganyariya/develop/playground/c-compile-assemble/main.c:5'
watchpoint spec = 'globalW'
watchpoint resources:
#0: addr = 0x100008000 size = 4
Watchpoint 1 hit:
# watchpoint set expression -- &globalW
new value: 100
(lldb) watchpoint list
Number of supported hardware watchpoints: 4
Current watchpoints:
Watchpoint 1: addr = 0x100008000 size = 4 state = enabled type = m
declare @ '/Users/ganyariya/develop/playground/c-compile-assemble/main.c:5'
watchpoint spec = 'globalW'
watchpoint resources:
#0: addr = 0x100008000 size = 4
Watchpoint 1 hit:
watch modify -c '(globalW==5)'
watchpoint は 特定のメモリ領域の値が変更されたとき
にプログラムを一時停止します。
#breakpoint は行単位ですが、 watchpoint は特定のメモリの値が変更されたとき、かつそれに付与した condition が満たされたときに止まります。
modify -c
で特定の値のときのみ止める、ができます。
プログラムを実行する
(lldb) run
Process 86154 launched: '/Users/ganyariya/develop/playground/c-compile-assemble/main' (arm64)
Process 86154 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 2.1
frame #0: 0x0000000100000578 main`main(argc=1, argv=0x000000016fdfeeb0) at main.c:9:15
6
7 int main(int argc, char **argv)
8 {
-> 9 int sum = add(5, 8);
10
11 printf("Sum = %d\n", sum);
12
Target 0: (main) stopped.
ステップ実行する
run コマンドでプログラムを実行したあと、ブレークポイントやウォッチポイントで停止されたあとに使うコマンドです。
- f (show current frame)
- n/next (step over)
- s/step (step in)
- c/continue (next breakpoint)
- f/finish (finish current function)
- si/step (step in assembler)
- アセンブリコード上で 1 step 進められる
値を表示する
(lldb) print argv
(char **) 0x000000016fdfeeb0
(lldb) print *argv
(char *) 0x000000016fdff190 "/Users/ganyariya/develop/playground/c-compile-assemble/main"
(lldb) print **argv
# アドレスを確認する
(lldb) print &globalW
(int *) 0x0000000100008000
# (lldb) frame variable
(int) argc = 1
(char **) argv = 0x000000016fdfeeb0
(int) sum = 1
print で表示できます。
ポインタの場合、 *
で値を解決できるようです。
&var
でアドレスを確認できます。
frame コマンドで実行中の stack-frame の変数を表示できます。 globalW はグローバル変数であり現在のスタックフレームの値ではないため、表示されていないことがわかります。
backtrace を表示する
backtrace を表示します。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100000578 main`main(argc=1, argv=0x000000016fdfeeb0) at main.c:9:15
frame #1: 0x0000000191c96b98 dyld`start + 6076
(lldb) bt 0
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100000578 main`main(argc=1, argv=0x000000016fdfeeb0) at main.c:9:15
frame #1: 0x0000000191c96b98 dyld`start + 6076
(lldb) bt 1
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100000578 main`main(argc=1, argv=0x000000016fdfeeb0) at main.c:9:15
backtrace で表示されるアドレスは 該当の関数のスタックフレームのアドレス
ならびに 開始アドレスではありません。
一時停止している位置のアドレスです。
たとえば、上記のサンプルだと #0 = 0x0000000100000578
ですが main 関数は 0x0000000100000558 番地から配置されています。
このように backtrace の表示においては
#0
の場合は、該当フレームのアドレス位置#1 ~
の場合は、#0
の関数を呼び出した位置の次のプログラム- =リターンアドレス
という表示になります。
メモリマップ/アドレス を表示する
メモリマップ全体のアドレス配置を確認したい場合は memory region
を利用します。
これで命令やデータなど、全体のメモリアドレスの配置に関する全体像を把握します。
(lldb) memory region --all
[0x0000000000000000-0x0000000100000000) ---
[0x0000000100000000-0x0000000100004000) r-x __TEXT
Modified memory (dirty) page list provided, 0 entries.
[0x0000000100004000-0x0000000100008000) r-- __DATA_CONST
Modified memory (dirty) page list provided, 0 entries.
[0x0000000100008000-0x000000010000c000) rw- __DATA
Modified memory (dirty) page list provided, 0 entries.
[0x000000010000c000-0x0000000100010000) r-- __LINKEDIT
Modified memory (dirty) page list provided, 0 entries.
[0x0000000100010000-0x0000000100018000) rw-
Modified memory (dirty) page list provided, 0 entries.
[0x0000000100018000-0x0000000100020000) r--
Modified memory (dirty) page list provided, 2 entries.
source info で、 C ソースコードと対比させた .text プログラムのアドレスがわかります。
(lldb) source info -f main.c
Lines found for file main.c in compilation unit main.c in `main
[0x0000000100000558-0x0000000100000578): /Users/ganyariya/develop/playground/c-compile-assemble/main.c:8
[0x0000000100000578-0x0000000100000584): /Users/ganyariya/develop/playground/c-compile-assemble/main.c:9:15
[0x0000000100000584-0x0000000100000588): /Users/ganyariya/develop/playground/c-compile-assemble/main.c:9:9
[0x0000000100000588-0x000000010000058c): /Users/ganyariya/develop/playground/c-compile-assemble/main.c:11:26
[0x000000010000058c-0x00000001000005a4): /Users/ganyariya/develop/playground/c-compile-assemble/main.c:11:5
[0x00000001000005a4-0x00000001000005b0): /Users/ganyariya/develop/playground/c-compile-assemble/main.c:13:5
image lookup で module に含まれる情報を確認できます。 これによって、関数のアドレスを取得できます。
objdump と比較したとき、アドレス位置が同じことがわかります。
(lldb) image lookup -n add
1 match found in /Users/ganyariya/develop/playground/c-compile-assemble/main:
Address: main[0x0000000100000510] (main.__TEXT.__text + 24)
Summary: main`add at add.c:13
1 match found in /usr/lib/dyld:
Address: dyld[0x00000001800dc7a0] (dyld.__TEXT.__text + 63392)
Summary: dyld`dyld4::RuntimeState::add(dyld4::Loader const*)
objdump -D main
main: file format mach-o arm64
Disassembly of section __TEXT,__text:
00000001000004f8 <_doubled>:
1000004f8: d10043ff sub sp, sp, #0x10
...
0000000100000510 <_add>:
100000510: d10083ff sub sp, sp, #0x20
100000514: a9017bfd stp x29, x30, [sp, #0x10]
100000518: 910043fd add x29, sp, #0x10
...
0000000100000558 <_main>:
100000558: d100c3ff sub sp, sp, #0x30
...
メモリアドレスの値を表示する
memory read で特定のアドレス位置の値を取得できます。
--size
でよみこむバイト単位を決めます。 1 だと 1byte, 2 だと 2byte ずつ読み込みます。
1 つの番地に 1 byte 入っているので、 size 2 の場合は 2 アドレスずつ1つの値にまとめあげて表示します。
--count
で表示個数を決めます。
(lldb) memory read --size 1 --format x --count 16 $sp+12
0x16fdfe7fc: 0x05 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x16fdfe804: 0x64 0x00 0x00 0x00 0x08 0x00 0x00 0x00
(lldb) memory read --size 2 --format x --count 16 $sp+12
0x16fdfe7fc: 0x0005 0x0000 0x0000 0x0000 0x0064 0x0000 0x0008 0x0000
0x16fdfe80c: 0x0005 0x0000 0xe840 0x6fdf 0x0001 0x0000 0x0584 0x0000
(lldb) memory read --size 2 --format x --count 10 $sp+12
0x16fdfe7fc: 0x0005 0x0000 0x0000 0x0000 0x0064 0x0000 0x0008 0x0000
0x16fdfe80c: 0x0005 0x0000
(lldb) memory read $sp+12
0x16fdfe7fc: 05 00 00 00 00 00 00 00 64 00 00 00 08 00 00 00 ........d.......
0x16fdfe80c: 05 00 00 00 40 e8 df 6f 01 00 00 00 84 05 00 00 [email protected]........
—format s で文字列として取得できます。 最初の NULL 文字に出会うまでアドレスを読み進めます。
(lldb) memory read --format s 0xaaaaaaaa0b69
0xaaaaaaaa0b69: "%s"
(lldb) x/s 0xaaaaaaaa0b69
0xaaaaaaaa0b69: "%s"
省略記法として x
があります。
x/[count][format][size] <address>
という形式です。
h = 2, w = 4, g = 8 です。
# x = 16進数
# b = 1 byte
(lldb) x/10xb $sp+12
0x16fdfe7fc: 0x05 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x16fdfe804: 0x64 0x00
# d = 10進数
(lldb) x/11db $sp+12
0x16fdfe7fc: 5
0x16fdfe7fd: 0
0x16fdfe7fe: 0
0x16fdfe7ff: 0
0x16fdfe800: 0
0x16fdfe801: 0
0x16fdfe802: 0
0x16fdfe803: 0
0x16fdfe804: 100
0x16fdfe805: 0
0x16fdfe806: 0
# w = 4 byte
(lldb) x/12xw $sp+12
0x16fdfe7fc: 0x00000005 0x00000000 0x00000064 0x00000008
0x16fdfe80c: 0x00000005 0x6fdfe840 0x00000001 0x00000584
0x16fdfe81c: 0x00000001 0x0009d71d 0x0fffffff 0x00000000
# h = 2 byte
(lldb) x/12xh $sp+12
0x16fdfe7fc: 0x0005 0x0000 0x0000 0x0000 0x0064 0x0000 0x0008 0x0000
0x16fdfe80c: 0x0005 0x0000 0xe840 0x6fdf
レジスタの値を表示する
レジスタ自体の情報を表示する場合は register info を利用します。
(lldb) register info sp
Name: sp (xsp)
Size: 8 bytes (64 bits)
In sets: General Purpose Registers (index 0)
register read で現在のフレームのレジスタ一覧とレジスタに格納されている値を表示できます。
- x0
- 第1引数ならびに戻り値
- fp = frame pointer
- x29 レジスタと同じ
- 現在のスタックフレームの開始地点を指す
- lr = link register
- x30 レジスタと同じ
- 現在の関数が終わったときの、元の関数に戻るときのアドレスを指す
- sp = stack pointer
- スタックの最上位アドレス
- pc = program counter
- 現在実行中の命令アドレス
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
* frame #0: 0x0000000100000500 main`doubled(x=5) at add.c:6:12
frame #1: 0x000000010000053c main`add(x=5, y=8) at add.c:15:22
frame #2: 0x0000000100000584 main`main(argc=1, argv=0x000000016fdfeeb0) at main.c:9:15
frame #3: 0x0000000191c96b98 dyld`start + 6076
(lldb) register read
General Purpose Registers:
x0 = 0x0000000000000005
x1 = 0x0000000000000008
x2 = 0x000000016fdfeec0
x3 = 0x000000016fdff0d0
x4 = 0x0000000000000001
x5 = 0x0000000000000010
x6 = 0x0000000000000041
x7 = 0x0000000000000000
x8 = 0x0000000000000064
x9 = 0x00000001ffd51450 dyld`lsl::sPoolBytes + 21344
x10 = 0x000000016fdfe328
x11 = 0x0000000000020000
x12 = 0x000000000001ac30
x13 = 0x000000000001ac20
x14 = 0x0000000000000001
x15 = 0x0000000000000055
x16 = 0x000000019206e884 libsystem_platform.dylib`os_unfair_lock_unlock
x17 = 0x00000002010237a8
x18 = 0x0000000000000000
x19 = 0x00000001ffd4c0b0 lsl::sAllocatorBuffer
x20 = 0x00000001ffd4c018 lsl::sMemoryManagerBuffer
x21 = 0x000000016fdfe8d8
x22 = 0x0fffffff0009d71d
x23 = 0x00000001ffd4c018 lsl::sMemoryManagerBuffer
x24 = 0x00000001ffd4c150 dyld`lsl::sPoolBytes + 96
x25 = 0x000000016fdfea40
x26 = 0x0000000000000000
x27 = 0x0000000000000000
x28 = 0x0000000000000000
fp = 0x000000016fdfe810
lr = 0x000000010000053c main`add + 44 at add.c:15:22
sp = 0x000000016fdfe7f0
pc = 0x0000000100000500 main`doubled + 8 at add.c:6:12
cpsr = 0x60000000