Help us understand the problem. What is going on with this article?

GDBでD言語で作成したバイナリをデバッグできるのか?どのようなサポートがされているのか?調べてみました!

はじめに

D言語のGDBサポートはGNU D コンパイラ開発者のIain Buclaw氏によって2014年頃からなされてきました。
別にGDB側でサポートしなくてもまったく使えないということはないのですが、いろいろ便利になることもあります。

動作環境

  • Dコンパイラ (DMD / GDC)
$ dmd --version
DMD64 D Compiler v2.084.0
Copyright (C) 1999-2018 by The D Language Foundation, All Rights Reserved written by Walter Bright
$ gdc --version
gdc (Ubuntu 8.2.0-1ubuntu2~18.04) 8.2.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  • GDB

GDBはdistroが提供してるやつを使います。

GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
  • OS
$ uname -mrv
4.15.0-43-generic #46-Ubuntu SMP Thu Dec 6 14:45:28 UTC 2018 x86_64
$ cat /etc/issue
Ubuntu 18.04.1 LTS \n \l

書かないこと

一般的なGDBの使い方については特に説明しません(するかもしれない)。

  • backtrace
  • breakpoint
  • continue
  • delete
  • disassemble
  • finish
  • info
  • next
  • print
  • ptype
  • rbreak
  • record
  • reverse-next
  • reverse-step
  • run
  • start
  • step
  • stepi
  • watch

あたりは知っておくとスムーズに操作できるでしょう。

使い方

GDBの基本

Hands-on形式で実際にDのプログラムをデバッグしながらGDBの使い方をみていきましょう。

てはじめに簡単なプログラムを用意します。

int fib(int n)
{
    if (n < 2) return n;
    else return fib(n - 1) + fib(n - 2);
}

void main()
{
    auto result = fib(10);
    assert(result == 55);
}

まずなによりも先にプログラムをデバッグ情報つきでビルドすることに注意をしてください。
デバッグ情報がなくてもGDBを使うこと自体は可能ですが、元のソースコード・シンボル情報・行情報はじめ多くの情報を失った状態でデバッグすることになります。
DMD/GDCともにコマンドライン引数に -g を渡すことでデバッグ情報つきでバイナリを生成することが可能です。

$ dmd -g fibonacci.d

実際にデバッグ情報つきでビルドされているか確認するには file コマンドが便利です。 with debug_info が表示されているかで確認できます。

$ file fibonacci
fibonacci: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=511fc1f6afbf4bb6bd93ba5e3d05da19edd82710, with debug_info, not stripped

GDBを起動してデバッグシンボルつきのバイナリを読み込みます。

Emacsなら M-x gdb でコマンドを実行しミニバッファに起動オプションを渡します。
私はだいたい gdb -i=mi -n -q <実行ファイル> を、実行ファイルが引数を要求する場合は gdb -i=mi -n -q --args <実行ファイル> <引数1> <引数2>... を渡すくらいです。だいたいGDBのコマンドラインと同じと思って差し支えありません。

普通はデバッグ情報から DW_AT_language を読み取って自動でlanguageの設定をやってくれるのだけど、 set langauge で明示的に指定することも可能です。

(gdb) show language
The current language is "auto; currently d".
(gdb) set language d
(gdb) show language
The current language is "d".

Cのプログラムから生成されたバイナリで start を使うと main 関数までで実行を中断しますが、 language d が設定されている場合は D main まですすんでから中断します。
行情報やソースコードの情報が取得できていることが確認できます。

(gdb) start
Temporary breakpoint 1 at 0x32e88: file fibonacci.d, line 9.
Starting program: /home/kubo39/dev/dlang/fibonacci
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Temporary breakpoint 1, D main () at fibonacci.d:9
9       auto result = fib(10);

step でfib関数の中へ入ってみましょう。
ちゃんと関数の中に入っていることが確認できます。

(gdb) step
fibonacci.fib(int) (n=10) at fibonacci.d:3
3       if (n < 2) return n;

nextfinish もちゃんと動くことが確認できます。

(gdb) next
4       else return fib(n - 1) + fib(n - 2);
(gdb) finish
Run till exit from #0  fibonacci.fib(int) (n=10) at fibonacci.d:4
0x0000555555586e92 in D main () at fibonacci.d:9
9       auto result = fib(10);
Value returned is $1 = 55

おっと、ちょっといろいろはしょよりすぎてしまいました。
reverse-next を使って巻き戻してみましょう。

(gdb) reverse-next
Target multi-thread does not support this command.
(gdb) print $eax
$2 = 55

あらら、Dのランタイムでは特にスレッドを生成していないのですがデフォルトの状態では libpthread.so とリンクしてしまうためにそのままでは使えません。

今回は小さいプログラムなので run して start しなおせばよいですが、大きいプログラムの場合はこまめに breakpoint を設定するか record を設定しておいたほうがよさそうです。

  • breakpointの場合
(gdb) break fibonacci.d:fibonacci.fib(int)
Breakpoint 3 at 0x555555586e47: file fibonacci.d, line 3.
(gdb) r
Starting program: /home/kubo39/dev/dlang/fibonacci
[Switching to thread 1 (process 6888)](running)
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 3, fibonacci.fib(int) (n=10) at fibonacci.d:3
3       if (n < 2) return n;
(gdb) backtrace
#0  fibonacci.fib(int) (n=10) at fibonacci.d:3
#1  0x0000555555586e92 in D main () at fibonacci.d:9
  • recordの場合
(gdb) start
Temporary breakpoint 1 at 0x32e88: file fibonacci.d, line 9.
Starting program: /home/kubo39/dev/dlang/fibonacci
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Temporary breakpoint 1, D main () at fibonacci.d:9
9       auto result = fib(10);
(gdb) record
(gdb) next
10      assert(result == 55);
(gdb) reverse-next
fibonacci.fib(int) (n=2) at fibonacci.d:4
4       else return fib(n - 1) + fib(n - 2);

式評価も可能ですが、 等価に表現できないことに注意してください。
GDB内ではyaccを使ってDのサブセットをパースしています。

  • ふつうにできるケース (castとかpow演算子とかsizeofとかtypeofとか)
(gdb) print cast(ubyte) -1
$1 = 255
(gdb) print 2 ^^ 2
$2 = 4
(gdb) print int.sizeof
$3 = 4
(gdb) ptype typeof(1.0)
type = double
  • かっこならいけるケース (値に対するsizeof)
(gdb) print 4.sizeof
Invalid number "4.sizeof".
(gdb) print (4).sizeof
$5 = 4
  • ぜんぜんだめなケース (連想配列リテラルとかレンジとか)
(gdb) print [1:1]
A syntax error in expression, near `:1]'.
(gdb) print ([1:1])
A syntax error in expression, near `:1])'.
(gdb) print "hoge"[1 .. $]
A syntax error in expression, near `. $]'.
(gdb) print ("hoge"[1 .. $])
A syntax error in expression, near `. $])'.
(gdb) print 0..3
A syntax error in expression, near `..3'.
(gdb) print (0..3)
A syntax error in expression, near `..3)'.

いくつかのプリミティブな型は ptype を使うことででD固有の型情報を取得できます。
たとえば以下のように整数リテラルに _ が入っていたりサフィックスに UL をつけているものを判定できます。

(gdb) ptype 0x0_1
type = int
(gdb) ptype 0UL
type = ulong

delegateなどはCのstruct扱いになっています。

(gdb) ptype dg
type = volatile struct _Delegate {
    void *ctxptr;
    void (*funcptr)(void);
}

watch

普通のGDBの使い方ではありますが、これもD言語対応してる機能です。

まず以下のようなコードを用意してコンパイルしておきます。

void main()
{
    int y = 7;
    y++;
    y++;
}

watch -location で変数を監視して内容が変わったら停止する、といった使い方をします。
breakpointと違って変化した後に止まるので注意してください。

(gdb) start
Temporary breakpoint 1 at 0x32de4: file watch.d, line 3.
Starting program: /home/kubo39/dev/dlang/watch
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Temporary breakpoint 1, D main () at watch.d:3
3       int y = 7;
(gdb) watch -location y
Hardware watchpoint 2: -location y
(gdb) info watchpoints
Num     Type           Disp Enb Address            What
2       hw watchpoint  keep y                      -location y
(gdb) continue
Continuing.

Hardware watchpoint 2: -location y

Old value = -9424
New value = 7
D main () at watch.d:4
4       y++;
(gdb) continue
Continuing.

Hardware watchpoint 2: -location y

Old value = 7
New value = 8
D main () at watch.d:5
5       y++;

demangle

GDBではdemangleもサポートしています。
通常作業言語をDに選択していればデフォルトでdemangle機能がはたらきます。

(gdb) demangle _Dmain
D main

GDBのdemangleは DMD 2.077以降の新しいマングリングルール に準拠していないので新しめのDMDで生成したバイナリだとdemangleできないものもあります。
例えば後方参照を含むシンボルはdemangleできません。
あとちんまい問題だとObjective-C linkage属性な関数とvariadic argumentsの組み合わせもできません。

(gdb) bt
#0  _D9scopesegv3barFNaNbNfxDFNfZvZxQi (dg=...) at scopesegv.d:10
#1  0x0000555555586fe9 in D main () at scopesegv.d:15
(gdb) demangle _D9scopesegv3barFNaNbNfxDFNfZvZxQi
Can't demangle "_D9scopesegv3barFNaNbNfxDFNfZvZxQi"

GDCでは古いマングリングルールなので正確にdemangleできています。

(gdb) bt
#0  scopesegv.bar(const(void() @safe delegate)) (dg=...) at scopesegv.d:7
#1  0x00005555555548e2 in D main () at scopesegv.d:15
(gdb) set print demangle off
(gdb) bt
#0  _D9scopesegv3barFNaNbNfxDFNfZvZxDFNfZv (dg=...) at scopesegv.d:7
#1  0x00005555555548e2 in _Dmain () at scopesegv.d:15
(gdb) demangle _D9scopesegv3barFNaNbNfxDFNfZvZxDFNfZv
scopesegv.bar(const(void() @safe delegate))

demangleのコードはGDBとbinutilsで共通なので, addr2line, c++filt, nm, objdump といったツール群でもdemangleが可能です。
ただし現時点でこれらのツールは -C--demangle=auto では勝手にdemangeしてくれないので、

  • addr2line, nm, objdump: --demangle=dlang を使う
  • c++filt: -s dlang もしくは --format=dlang を使う

という感じで明示的にオプションを渡す必要があります。

DMDでうまくいかないんだけど...という場合は ddemangle というツールを使うとだいたい最新のマングリングルールに追従しているはずなのでうまいことdemangleできるかもしれません。投稿時点ではObjective-C linkage属性な関数はできなかったりしますが。。

コンパイラによってmanglingが変わる場合に生成したバイナリがどのコンパイラでコンパイルされたか知りたい、という状況もあります。
ちゃんとしたコンパイラならDWARFの DW_AT_producer というセクションにどのコンパイラで生成されたのかという情報が作成され、 readelf -wi もしくは objdump -g を使って確認ができます。

$ readelf -wi gdc.bin| grep DW_AT_producer
    <c>   DW_AT_producer    : (indirect string, offset: 0x1a0): GNU D 8.2.0 -mtune=generic -march=x86-64 -g
$ readelf -wi dmd.bin| grep DW_AT_producer
    <c>   DW_AT_producer    : Digital Mars D v2.083  # これは単にDMDのバグ

おわりに

いかがでしたか?ここまでお読みいただきありがとうございました。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away