ラベル GDB の投稿を表示しています。 すべての投稿を表示
ラベル GDB の投稿を表示しています。 すべての投稿を表示

QEMUでLinuxカーネルをGDBでデバッグする

QEMUの起動

以下の2つのオプションを使う
オプション説明
-sTCP 1234番でGDBのリモートコクションをオープンします
-SGDBにアタッチされるまでゲストの実行を待機します。カーネルの起動後にアタッチしても問題ない場合、不要です。

Linuxの起動オプション

QEMUで起動されるLinuxカーネルのコマンドラインオプションにnokaslrを付与する。通常、GRUBなどのブートローダーに指定する。このオプションは、カーネルの関数の配置アドレスのランダム化を抑止する。このオプションを指定しない場合、カーネルのデバッグ情報に記載されているアドレスと、実際のアドレスがことなり、GDBの操作が期待どおりに行われない。

デバッグ情報の入手

ターゲットのカーネルに対応するデバッグ情報ファイルを取得する。

Debianの場合の例

sudo apt install linux-image-5.10.0-22-amd64-dbg
あるいは、APT repositoryから直接取得する。
wget http://ftp.debian.org/debian/pool/main/l/linux/linux-image-5.10.0-22-amd64-dbg_5.10.178-3_amd64.deb
sudo dpkg -i linux-image-5.10.0-22-amd64-dbg_5.10.178-3_amd64.deb

自分でビルドしたカーネルの場合

カーネルのビルドディレクトリにvmlinuxがあるのでそれを使う。ただし、ビルド時、以下の点に留意
  • 以下のCONFIGを有効化
    • CONFIG_DEBUG_INFO_DWARF5、または、CONFIG_DEBUG_INFO_DWARF4
    • CONFIG_GDB_SCRIPTS
    • CONFIG_FRAME_POINTER
  • 以下のCONFIGを無効化
    • CONFIG_DEBUG_INFO_REDUCED
参考: https://docs.kernel.org/dev-tools/gdb-kernel-debugging.html

GDBの起動

デバッグ情報付きのvmlinuxを引数にしてGDBを起動する
gdb /usr/lib/debug/vmlinux-5.10.0-22-amd64
GDB起動後、以下のコマンドを実行する
(gdb) target remote [QEMU起動マシンのアドレス]:1234

グローバル変数の表示例

(gdb) p jiffies
$1 = 4295462764

ブレークポイントの例

do_sys_openにブレークポイントを仕掛けて、ゲストOSでファイルを開く操作をすると以下のようにブレークする
(gdb) b do_sys_open
Breakpoint 2 at 0xffffffff812d6ff0: do_sys_open. (11 locations)
(gdb) c
Continuing.

Breakpoint 2.4, do_sys_open (mode=0, flags=557056, filename=0x7f153b8cabe7 "/etc/ld.so.cache", dfd=-100) at fs/open.c:1201
1201    fs/open.c: No such file or directory.
(gdb) bt
#0  do_sys_open (mode=0, flags=557056, filename=0x7f153b8cabe7 "/etc/ld.so.cache", dfd=-100) at fs/open.c:1201
#1  __do_sys_openat (mode=0, flags=557056, filename=0x7f153b8cabe7 "/etc/ld.so.cache", dfd=-100) at fs/open.c:1218
#2  __se_sys_openat (mode=0, flags=524288, filename=139729170115559, dfd=4294967196) at fs/open.c:1213
#3  __x64_sys_openat (regs=<optimized out>) at fs/open.c:1213
#4  0xffffffff818f7e00 in do_syscall_64 (nr=<optimized out>, regs=0xffffc9000027bf58) at arch/x86/entry/common.c:46
#5  0xffffffff81a000a9 in entry_SYSCALL_64 () at /build/linux-ts3hOX/linux-5.10.178/arch/x86/entry/entry_64.S:125

実行中のプロセスのtask_structの表示例

上記のブレーク例は、lsを実行したときのものである。このときのプロセスが確かにlsであることと、そのプロセスIDを確認する。
(gdb) set $curr = (struct task_struct **)($gs_base + (void *)&current_task)
(gdb) p $curr
$2 = (struct task_struct **) 0xffff88803d01fbc0
(gdb) p $curr->comm
$3 = "ls\000h\000\000\000)\000\000\000\000\000\000\000"
(gdb) p $curr->pid
$4 = 552
上記について補足する。実行中のプロセスのtask_struct構造体へのポインタは、CPUごとの変数領域の中にあり、グローバル変数current_taskの値は、その領域におけるオフセットである。また、CPUごとの変数領域の開始位置はGSセグメントレジスタに格納されている。そのため、両者を加算したアドレス位置に実行中のプロセスに関するtask_struct領域へのポインタが格納されている。

参考

GDBでシグナルをハンドルする

例えば、SIGWINCHで停止するには以下。
(gdb) handle SIGWINCH stop
停止しないようにするには以下のいずれかをstopの代わりにpassを指定する。
なお、シグナルの現在の補足状態を取得するには次のコマンド。
(gdb) info signal

GDBで長い出力を省略させない

表示制限を500文字に設定する例
(gdb) set print elements 500
なお、現在の設定値は次のように確認できる。
(gdb) show print elements
Limit on string chars or array elements to print is 500.

mallocやfreeでGLIBCが異常を検出した際、abortさせる

GLIBCが、malloc()やfree()でメモリ破壊や二重開放などを検出した場合、stack traceやmemory mapが表示され、プログラムが終了します。このような現象の原因を調査するために、コアをダンプさせ、GDBで解析したい時があります。そのような場合、以下の環境変数を設定すると、異常検出時、abort()がコールされます。ulimit -c unlimitedなどでコアを出力する設定にしておくと、SIGABRTを受けて、コアダンプが生成されます。
MALLOC_CHECK_=2
ちなみに設定する値によって、次の振る舞いになります。
0: 何も起こさない
1: メッセージを表示
2: abortする
3: 上記1と2の両方を行う

GDBの出力をフィルタする

GDBのPython機能を利用して、大量の出力から目的の行を抽出する方法を2つ紹介します。

1.reモジュールの利用(Pythonコードによる抽出)

1つ目は、backtraceの出力をPythonのreモジュールを使って目的の行を探します。主に青字の部分を目的に応じて変更してください。
(gdb) py out=gdb.execute("i proc map", to_string=True); import re; [re.search("Named", line) and print(line) for line in out.split("\n")];

2.シェルを利用

2つ目は、コマンドラインのgrepを利用しています。単純な検索だとPythonのプログラム(それもワンライナー)を組むより容易に目的を達成できる場合もあるでしょう。
(gdb) py a=gdb.execute("bt",to_string=True); import subprocess; subprocess.Popen("echo '%s' | grep main" % a, shell=True).wait()
なお、上記のいずれも技術的には、Pythonをワンライナを使用しています。 繰り返しはリスト包含(list comprehension)、分岐(if)にはand/orの遅延評価を利用しています。 関数はlamdaで変数にしています。

使用例

通常、i proc mapコマンドを実行すると多くの行が出力されます。catコマンドを使って試してみましょう。
$ gdb -q /bin/cat   
Reading symbols from /bin/cat...(no debugging symbols found)...done.
(gdb) run
Starting program: /bin/cat 
^C <= 実行を中断するためにCTRL + Cを入力
Program received signal SIGINT, Interrupt.
0x00007ffff7afe6a0 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:81
81      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) i proc maps
Too many parameters: maps
(gdb) i proc map 
process 19540
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x40b000     0xb000        0x0 /bin/cat
            0x60a000           0x60b000     0x1000     0xa000 /bin/cat
            0x60b000           0x60c000     0x1000     0xb000 /bin/cat
            0x60c000           0x62d000    0x21000        0x0 [heap]
      0x7ffff7a12000     0x7ffff7bcf000   0x1bd000        0x0 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7bcf000     0x7ffff7dcf000   0x200000   0x1bd000 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7dcf000     0x7ffff7dd3000     0x4000   0x1bd000 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7dd3000     0x7ffff7dd5000     0x2000   0x1c1000 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7dd5000     0x7ffff7dda000     0x5000        0x0 
      0x7ffff7dda000     0x7ffff7dfd000    0x23000        0x0 /lib/x86_64-linux-gnu/ld-2.17.so
      0x7ffff7fcc000     0x7ffff7fcf000     0x3000        0x0 
      0x7ffff7ff8000     0x7ffff7ffa000     0x2000        0x0 
      0x7ffff7ffa000     0x7ffff7ffc000     0x2000        0x0 [vdso]
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x22000 /lib/x86_64-linux-gnu/ld-2.17.so
      0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x23000 /lib/x86_64-linux-gnu/ld-2.17.so
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]
上記では、マップされているライブラリの数がそれほど多くないので、目視で、目的のファイルを探すことも、それほど困難ではありませんが、firefoxやchromeのような大規模なプログラムでは数百行以上表示されます。 これを、stackの入っている行のみを表示させるには次のように入力します。かなりシンプルになることが理解いただけると思います。
(gdb) py out=gdb.execute("i proc map", to_string=True); import re; [re.search("stack", line) and print(line) for line in out.split("\n")];
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]

参考

GDBからPython使用法については下記にも述べられています。 http://transparent-to-radiation.blogspot.jp/2014/01/gdbpython-script.html

GDBでアタッチ中のプロセスのメモリマップを表示

アタッチしているプログラムのメモリマップをinfo proc mapで表示できます。以下では、catコマンド使った例を示します。

引数なしで実行されたcatは、起動したまま標準入力を待ち続けます。その状態で一旦GDBからコマンドを実行できるようにするため、 途中で、CTRL + Cを入力しています。

$ gdb /bin/cat
[略]
(gdb) run
Starting program: /bin/cat 
^C <= CTRL + Cを押して実行を中断します。
Program received signal SIGINT, Interrupt.
0x00007ffff7afe6a0 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:81
81      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) info proc map
process 31692
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x40b000     0xb000        0x0 /bin/cat
            0x60a000           0x60b000     0x1000     0xa000 /bin/cat
            0x60b000           0x60c000     0x1000     0xb000 /bin/cat
            0x60c000           0x62d000    0x21000        0x0 [heap]
      0x7ffff7a12000     0x7ffff7bcf000   0x1bd000        0x0 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7bcf000     0x7ffff7dcf000   0x200000   0x1bd000 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7dcf000     0x7ffff7dd3000     0x4000   0x1bd000 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7dd3000     0x7ffff7dd5000     0x2000   0x1c1000 /lib/x86_64-linux-gnu/libc-2.17.so
      0x7ffff7dd5000     0x7ffff7dda000     0x5000        0x0 
      0x7ffff7dda000     0x7ffff7dfd000    0x23000        0x0 /lib/x86_64-linux-gnu/ld-2.17.so
      0x7ffff7fcc000     0x7ffff7fcf000     0x3000        0x0 
      0x7ffff7ff8000     0x7ffff7ffa000     0x2000        0x0 
      0x7ffff7ffa000     0x7ffff7ffc000     0x2000        0x0 [vdso]
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x22000 /lib/x86_64-linux-gnu/ld-2.17.so
      0x7ffff7ffd000     0x7ffff7fff000     0x2000    0x23000 /lib/x86_64-linux-gnu/ld-2.17.so
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]
また、類似のコマンドにinfo sharedがあります。こちらは、マップされている共有ライブラリのみを列挙します。Syms Readという列は、デバッグ用のシンボル情報が読み込まれているか否かを示します。
(gdb) info shared
From                To                  Syms Read   Shared Object Library
0x00007ffff7ddaaa0  0x00007ffff7df5439  Yes         /lib64/ld-linux-x86-64.so.2
0x00007ffff7a313c0  0x00007ffff7b7c39f  Yes         /lib/x86_64-linux-gnu/libc.so.6

GDBでPython scriptを実行する

最近(少なくともversion 7以降)のGDBは、Python scriptを実行できます。実行には、従来のGDBマクロと同じくsourceコマンドを使用します。

早速、試してみましょう。次のようなPython scriptを用意します。
$ cat hello.py
#!/usr/bin/env python
import sys
sys.stdout.write('Hello, my first script.\n')
GDBを起動して、実行します。
(gdb) source hello.py
Hello, my first script.

Python scriptからのGDB機能の利用

次に、Python scriptからGDBの機能を使用する例を紹介します。gdb.execute()関数を使用すれば、GDBのコマンドラインで行う操作を実行できます。

サンプルのデバッグ対象として以下のtarget.ccを使用します。
[target.cc]
#include <cstdio>
#include <cstdlib>
#include <time.h>

int getval(int div)
{
        return time(NULL) / div;
}

int main(void)
{
        printf("val: %d\n", getval(5));
        return EXIT_SUCCESS;
}
これをshow-bt.pyで次の4つのことを行います。
  • break pointをgetval()関数の先頭に設定
  • プログラムを実行
  • break pointで停止時のbacktraceを表示する
  • break pointで停止時のraxレジスタを表示する
[show-bt.py] 
gdb.execute('b getval')
gdb.execute('run')
gdb.execute('bt')
gdb.execute('p/x $rax')
以下はコマンドラインからスクリプトを実行させる例です。 コマンドライン引数の-qは、GDB起動時のcopyrightなどの表示を抑制します。-xで実行するスクリプトファイルを指定します。
$ gdb -q -x show-bt.py  target
Reading symbols from /home/foo/target...done.
Breakpoint 1 at 0x400627: file target.cc, line 7.

Breakpoint 1, getval (div=5) at target.cc:7
7               return time(NULL) / div;
#0  getval (div=5) at target.cc:7
#1  0x0000000000400656 in main () at target.cc:12
#2  0x00007ffff72d2ead in __libc_start_main (main=, argc=, ubp_av=, init=, fini=, rtld_fini=, stack_end=0x7fffffffe128) at libc-start.c:228
#3  0x0000000000400539 in _start ()
$1 = 0x7ffff763aee8

コマンドの出力をPythonの文字列として取得する

上記の例では、通常のGDBコマンドを実行するだけでしたが、この出力をPythonの文字列として取得してみましょう。これは、gdb.execute()関数のto_string引数にTrueを渡すことで実現できます。Pythonは、文字列処理も得意ですから、容易に特定の文字列を抽出したり、整形して表示することが可能になります。

以下の例では、stacktrace出力の各行からframe番号と最後の単語を抽出して表示します。
$ cat show-bt-and-format.py 
gdb.execute('b getval')
gdb.execute('run')
bt = gdb.execute('bt', to_string=True)
for line in bt.split('\n'):
  words = line.split(' ')
  print '%s %s' % (words[0], words[-1])
$ gdb -q -x show-bt-and-format.py  target
Reading symbols from /home/foo/target...done.
Breakpoint 1 at 0x400627: file target.cc, line 7.

Breakpoint 1, getval (div=5) at target.cc:7
7               return time(NULL) / div;
#0 target.cc:7
#1 target.cc:12
#2 libc-start.c:228
#3 ()