はじめに

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

Fetching Data#rdfh

コマンド構文

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