gdbの共演

Table of Contents

gdb 15.2 + emacs 30.1

(gdb) b client_main
Breakpoint 1 at 0x4ce76: file client.c, line 243.
(gdb) r ls
The target does not support running in non-stop mode.
(gdb) q

27.6.5.8 マルチスレッドのデバッグ

gdb-mi.el

;; TODO Some commands can't be called with --all (give a notice about
;; it in setting doc)
(defcustom gdb-gud-control-all-threads t
  "When non-nil, GUD execution commands affect all threads when in non-stop mod\
e.
Otherwise, only current thread is affected."
  :type 'boolean
  :group 'gdb-non-stop
  :version "23.2")

最初これをnilにしたんだけど、効き目が無かったので、大局的に下記に設定した。

;; debug
(setq gdb-non-stop-setting nil)

地味にemacsが更新されていくので、苦労するなあ。

.emacs.d/elpa/ddskk-20241227.2223/ddskk-autoloads.el: Warning: `defadvice' is an obsolete macro (as of 30.1); use `advice-add' or `define-advice'
.emacs.d/elpa/ddskk-20241227.2223/ddskk-autoloads.el: Warning: `defadvice' is an obsolete macro (as of 30.1); use `advice-add' or `define-advice'

emacs 30.1になったら、こんなの出てくる様になった。最初 define-adviceを試したら 別の場所で酷いエラーが発生。しょうがないのので。もう一方のadvice-addを確認。 こちらで問題がなくなった。使い方の問題でも孕んでいるのでしょうか? 深入りはしない。

libevent

過去ログを調べたら、 systrace なんてのが有った。10年前には、OpenBSDにもsystraceなんてのが提供 されてたんですな。隔世の感がするぞ。今じゃktraceだもの。

tmuxでどんな風にlibeventを利用してるか軽く当ってみた。

ad$ grep event_set *.c
control.c:      bufferevent_setwatermark(cs->write_event, EV_WRITE, CONTROL_BUFFER_LOW,
log.c:  event_set_log_callback(log_event_cb);
log.c:  event_set_log_callback(NULL);
proc.c: event_set(&peer->event, peer->ibuf.fd, events, proc_event_cb, peer);
proc.c: event_set(&peer->event, fd, EV_READ, proc_event_cb, peer);
server.c:               event_set(&server_ev_accept, server_fd, EV_READ, server_accept,
server.c:               event_set(&server_ev_accept, server_fd, EV_TIMEOUT,
tty.c:  event_set(&tty->event_in, c->fd, EV_PERSIST|EV_READ,
tty.c:  event_set(&tty->event_out, c->fd, EV_WRITE, tty_write_callback, tty);
ad$ grep event_add *.c
alerts.c:               event_add(&w->alerts_timer, &tv);
input.c:        event_add(&ictx->timer, &tv);
names.c:                        event_add(&w->name_event, &next);
proc.c: event_add(&peer->event, NULL);
server.c:               event_add(&server_ev_accept, NULL);
server.c:               event_add(&server_ev_accept, &tv);
tty.c:          event_add(&tty->event_out, NULL);
tty.c:  event_add(&tty->event_in, NULL);
tty.c:          event_add(&tty->event_out, NULL);

/usr/include/event.hは必見! こんな親切はヘッダーファイルは初めて見たぞ。 これを参照するだけで、無味乾燥なmanより良く使い方が分る。今日から貴方もevent pro になれる事を保証しますよ。

DNS/http/RPCにも対応しますって、少々がんばりすぎな気がしないでもないですけど。 頭の隅に入れておこう。

tty/pty

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <util.h>

#define NUM_PTY 3

int master_fd[NUM_PTY];
pid_t pids[NUM_PTY];

void spawn_shell(int index) {
    struct termios term;
    tcgetattr(STDIN_FILENO, &term);

    pids[index] = forkpty(&master_fd[index], NULL, &term, NULL);
    if (pids[index] == 0) {
        execlp("ksh", "ksh", NULL);
        perror("execlp");
        exit(1);
    }
}

int main() {
    for (int i = 0; i < NUM_PTY; i++) {
        spawn_shell(i);
    }
    return 0;
}

cc -g mini.c -lutil でコンパイル。

(gdb) p pids
$1 = {89586, 75252, 53008}
(gdb) p master_fd
$2 = {4, 5, 6}

それぞれのプロセスが利用できるマスター・ファイルディスクリリプターが得られる。

ad$ fstat -p 53008
USER     CMD          PID   FD MOUNT        INUM  MODE         R/W    SZ|DV
sakae    ksh        53008 text /         3395539  -r-xr-xr-x     r   797712
sakae    ksh        53008   wd /tmp            2  drwxrwxrwt     r      512
sakae    ksh        53008    0 /         5806587  crw--w----    rw    ttyp6
sakae    ksh        53008    1 /         5806587  crw--w----    rw    ttyp6
sakae    ksh        53008    2 /         5806587  crw--w----    rw    ttyp6
sakae    ksh        53008    4 /         5806582  crw-rw-rw-    rw    ptyp3
sakae    ksh        53008    5 /         5806586  crw-rw-rw-    rw    ptyp5
sakae    ksh        53008   10 /         5807247  crw-rw-rw-  rwep      tty

最後に作成されたプロセスを確認してみると、ttyp6が利用可能となっている事が 確認できる。詳細はpty(4)を参照。

(gdb) bt
#0  fdopenpty (ptmfd=3, amaster=<optimized out>, aslave=<optimized out>, name=<\
optimized out>, termp=<optimized out>, winp=<optimized out>) at /usr/src/lib/li\
butil/pty.c:79
#1  fdforkpty (ptmfd=3, amaster=0x85832793f5c <master_fd+4>, name=0x0, termp=0x\
7a986e673818, winp=0x0) at /usr/src/lib/libutil/pty.c:123
#2  0x0000085a34e2c717 in forkpty (amaster=0x85832793f5c <master_fd+4>, name=0x\
0, termp=0x7a986e673818, winp=0x0) at /usr/src/lib/libutil/pty.c:108
#3  0x0000085832791ae9 in spawn_shell (index=1) at mini.c:16
#4  0x0000085832791b96 in main () at mini.c:26

実際はユーザーが呼び出した関数より更に深い部分で、/dev/ptmの助けを得て、fdを取得 している。

kernel側では、 tty_pty.c/ptcopen() とかが、利用されるのかなあ? ユーザーランド 側とカーネル側でgdbを共演させてみる。OpenBSDを使っていれば容易い事だ。

(gdb) bt 8
#0  ptcopen (dev=1538, flag=<optimized out>, devtype=<optimized out>, p=<optimi\
zed out>) at /usr/src/sys/kern/tty_pty.c:407
#1  0xffffffff81fba9a5 in spec_open (v=0x3) at /usr/src/sys/kern/spec_vnops.c:1\
50
#2  0xffffffff8205114e in VOP_OPEN (vp=<optimized out>, mode=3, cred=0x2000, p=\
0xffff800023938cc0) at /usr/src/sys/kern/vfs_vops.c:138
#3  0xffffffff816d85e7 in ptm_vn_open (ndp=<optimized out>) at /usr/src/sys/ker\
n/tty_pty.c:1025
#4  0xffffffff816d80b5 in ptmioctl (dev=<optimized out>, cmd=<optimized out>, d\
ata=0xffff800023925460 "", flag=<optimized out>, p=0x3c801e1e20efc99e) at /usr/\
src/sys/kern/tty_pty.c:1116
#5  0xffffffff82051557 in VOP_IOCTL (vp=<optimized out>, command=3, data=0x2000\
, fflag=596872384, cred=0x68, p=0xffffffff826efe00 <rw_ops+32>) at /usr/src/sys\
/kern/vfs_vops.c:264
#6  0xffffffff81ec5070 in vn_ioctl (fp=0x5100, com=18446603336817988704, data=0\
x2000 <error: Cannot access memory at address 0x2000>, p=0xffff800023938cc0) at\
 /usr/src/sys/kern/vfs_vnops.c:525
#7  0xffffffff8126b676 in sys_ioctl (p=0xffff800023925460, v=0xffff800023938cc0\
, retval=<optimized out>) at /usr/src/sys/kern/sys_generic.c:505
(More stack frames follow...)

3回コール(fork)されてるから間違い無いだろう。あれ? 標準入出力とエラーが有るけど、同一 デバイスだから、1回でいいのか。デバイスから読むのを入力、書くのを出力。若しくは エラーと、わざわざ3つに分類(FD 0,1,2)してるのは、利便性(shell)を考慮しての事だな。

BSDな人達が、unixにネットワークを追加した時は、そこまで親切にしなかった。通常の 出力とエラー出力の区別がつかないぞ。どうする? で、発明したのがタグなんだろうな。 ここからはエラーのデータですよとかで、受信した方も区別を付ける様に提案をした。 当然その提案は、世界に対して周知徹底されなければ意味をなさない。それで、RFC 文書で、ご意見を拝聴しますよ、とな。

for Linux

Q: unixの標準出力(fd 1)と標準エラー出力(fd 2)について質問します。 論理的にはfdで区別されてます。しかし実体は/dev/tty00の様に 同一デバイスです。

この出力を受け取る方は、どうやって標準出力だったかエラーだったかを 区別するのでしょうか?

A: とても良い質問です。Unix系OSでのファイルディスクリプタ(fd)とデバイスファイル(たとえば `/dev/tty0`)の関係についての理解が深いことがうかがえます。

### 結論:

出力先(たとえば `/dev/tty` や端末エミュレータ)は、標準出力(fd 1)と標準エラー出力(fd 2)の区別を基本的にはしていません。 区別されるのは、プロセス内部のファイルディスクリプタ番号(1か2)に基づいており、同じデバイスに送られても「どのfdから送られたか」はOSや端末には伝わりません。

## 詳しく解説:

### 1. fdはプロセス内の概念

`fd 1`(stdout)や `fd 2`(stderr)はプロセスが保持している「ファイルディスクリプタテーブル」に記録されています。

たとえば:

write(1, "hello\n", 6); // 標準出力へ
write(2, "error!\n", 7); // 標準エラーへ

この2つは**論理的には別の出力先**かもしれませんが、たいていの場合、両方とも同じファイル(たとえば端末 `/dev/tty`)に出力されています。

### 2. デバイスは「どのfd経由か」を知らない

`/dev/tty` に出力されるとき、端末はただ「このバイト列を出力せよ」と言われるだけです。端末自身はその出力がfd 1から来たのかfd 2から来たのかは知りませんし、区別しません。

### 3. じゃあどうやって区別するの?

区別が必要な場合は、以下の方法が使われます。

#### a. リダイレクトで別のファイルに出力

command > out.txt 2> err.txt

この場合、`fd 1`と`fd 2`は異なるファイルに向いているので、後からログを見て区別できます。

#### b. プログラム内で明示的にマークする

たとえば、stderrに出す内容に `[ERROR]` をつけるなど。

fprintf(stderr, "[ERROR] something went wrong\n");

#### c. パイプやソケットを使うとfdごとに判別可能

`pipe()`や`dup2()`を使ってstdoutとstderrを別のパイプに割り当てると、読み手側でどちらから来たかを区別できます。

## 補足:端末上で見たときの違い

通常、stdoutは**バッファリングされて遅れて出る**のに対し、stderrは**即時に出る**(非バッファリング)ので、順番が逆転することもあります。これも「区別があるように見える」一因です。


色々とやりとりしてたら、Linuxでユーザーランドとカーネルをgdbで共演もできますよ、 なんて言い出したので、お願いしてみた。

以下に、**完全自動化された1本のセットアップスクリプト `setup.sh`** をご提供します。 これ1つで、Linuxカーネルのクローン、`initramfs`作成、QEMU起動、GDB接続まで一通り構築できます。

## ✅ `setup.sh`(完全自動セットアップスクリプト)

#!/bin/bash
set -e

# === 環境設定 ===
KERNEL_DIR=linux
INITRAMFS_DIR=initramfs
BUILD_DIR=build
ARCH=x86_64

echo "🔧 環境初期化..."
mkdir -p "$INITRAMFS_DIR/bin" "$BUILD_DIR"

# === 1. Linuxカーネル取得・ビルド ===
if [ ! -d "$KERNEL_DIR" ]; then
  echo "📥 カーネル取得中..."
  git clone --depth=1 https://github.com/torvalds/linux.git "$KERNEL_DIR"
fi

cd "$KERNEL_DIR"
echo "🛠 カーネル設定..."
make defconfig
scripts/config --enable CONFIG_DEBUG_INFO

echo "🔨 カーネルビルド中..."
make -j$(nproc) bzImage
cd ..

# === 2. initramfs の準備 ===
echo "📦 initramfs 初期化..."

# init スクリプト
cat > "$INITRAMFS_DIR/init" <<'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "[INIT] Launching test..."
/bin/test
exec /bin/sh
EOF
chmod +x "$INITRAMFS_DIR/init"

# test.c(stderr出力)
cat > "$INITRAMFS_DIR/test.c" <<'EOF'
#include <unistd.h>
#include <string.h>

int main() {
    const char *msg = "Hello from stderr\n";
    write(2, msg, strlen(msg));
    return 0;
}
EOF

echo "🧪 test.c コンパイル..."
gcc -static -o "$INITRAMFS_DIR/bin/test" "$INITRAMFS_DIR/test.c"

echo "📦 initramfs アーカイブ作成..."
cd "$INITRAMFS_DIR"
find . | cpio -o -H newc | gzip > "../$BUILD_DIR/initramfs.cpio.gz"
cd ..

# === 3. gdb スクリプト生成 ===
cat > gdb-init <<EOF
file $KERNEL_DIR/vmlinux
target remote :1234
break tty_write
continue
EOF

# === 4. QEMU 起動スクリプト ===
cat > run-qemu.sh <<EOF
#!/bin/bash
qemu-system-x86_64 \\
  -kernel $KERNEL_DIR/arch/x86/boot/bzImage \\
  -initrd $BUILD_DIR/initramfs.cpio.gz \\
  -append "console=ttyS0 init=/init nokaslr" \\
  -nographic \\
  -s -S
EOF
chmod +x run-qemu.sh

echo "✅ セットアップ完了"
echo ""
echo "次の手順で起動できます:"
echo ""
echo "1️⃣ ./run-qemu.sh"
echo "2️⃣ 別のターミナルで: gdb -x gdb-init"
echo ""
```

## 🧪 使用方法

chmod +x setup.sh
./setup.sh           # 全自動セットアップ

# 起動:
./run-qemu.sh        # QEMU起動(gdb待機状態)

# 別ターミナルで:
gdb -x gdb-init      # GDB起動・接続・tty_writeで停止

## 🧰 必要な依存パッケージ(事前に入れておいてください)

Ubuntu/Debian:

sudo apt install build-essential flex bison libncurses-dev \
  gcc g++ make git gdb qemu-system-x86 cpio

## 📌 できること

  • `write(2, …)` → `ttywrite()` の流れをgdbで完全に追跡可能
  • stderr(fd=2)とstdout(fd=1)の挙動の違いをカーネルレベルで確認
  • 必要に応じて `ksyswrite` や `vfswrite` にもブレーク可能

## 📁 生成されるファイル一覧

  • `linux/` → カーネルソース&ビルド結果(`vmlinux`, `bzImage`)
  • `initramfs/` → `/init`, `test.c`, `/bin/test`
  • `build/initramfs.cpio.gz` → initramfs
  • `run-qemu.sh` → QEMU起動スクリプト
  • `gdb-init` → GDB自動ブレークスクリプト

これで準備は完了です! 環境のチューニングや特定関数の追跡など、ご希望があればさらに深掘りできます。どうしますか?

もっと手軽にやる方法として、gdbでカーネル空間の追跡をするには、gdb + qemu + カーネルシンボル が必要です。簡易な方法は kprobes や ftrace、あるいは bpftrace を使う なんて事もできるみたい。わざわざ苦労をしょいこむ事は無いけどね。 OpenBSD何それって言う人へのささやかなプレゼントです。

bpftrace

qemuを持ち出さなくても、こんな手軽な機構が用意されてて、即利用できるみたいだ。

eBPF - bpftraceチュートリアル 編

これって源流はSolarisだよな。それがFreeBSDに移植された。そして権利の関係も あって、リナでも同様な機構を新たに実装したとな。

バグ調査やパフォーマンス改善に役立つ!eBPFを用いたトレーシングについて

さくらさんの所にも紹介があった。

README

バルセロナで豆腐屋になった

新聞記者という第一の人生を上がって、バルセロナで第二の人生として 豆腐屋になった方の体験記である。

なんでバルセロナ? 記者時代に取材の関係で立ち寄ったバルセロナが 自分をそっと受け入れてくれた事。そんな記憶が残っていて、リフレッシュ 休暇で、奥さんと再び訪れたそうな。食い物に満足した。だったら住んで みてもいいな。奥さんも同感。ただし、大好きな豆腐や納豆、油あげは 現地でも手にはいったけど、中国人とかが作っていて信用ならん。 だったら、自分で豆腐屋を始めるしかないっしょ。

こうして第二の人生は始まる。それを称して長女は、豆腐アドベンチャーだと 言う。言いえて妙。やらないでクヨクヨするより、飛び込んでしまえ。

帰国して、こういう探検記を収録するサイトを作った。 一身ニ生倶楽部

番外編の記事に スペイン大停電は予想外の原因で起きた こんなのが出てた。日本で同様な事が 発生したらどうなるだろね?


This year's Index

Home