riscV
Table of Contents
gdb pipe
ご質問の現象 — ` pipe_write()
`が全くヒットせず、` pipe_read()
`が大量に発生する — について、gdbやOpenBSDカーネルの挙動を踏まえて技術的に考察します。
—
## 🔍 背景理解
あなたの環境:
- OS: OpenBSD 7.7
- GDB: 15.2
- デバッグ対象: ユーザープロセス `/tmp/a.out`(gdbから起動)
- カーネルブレークポイント:
- `
pipe_read()
`(15回ヒット) - `
pipe_write()
`(0回ヒット)
- `
`pipe`デバイスは、読み込み時にバッファが空なら書き込み完了を待つ仕様(通常はブロック)ですが、`pipewrite`が呼ばれずに`piperead`が発生しているのは一見矛盾して見えます。
—
## ✅ 考えられる原因とgdbの挙動
### 1. gdb自身が擬似端末(PTY)を使っている
あなたの`fstat`の出力で、gdb(PID 2820)は以下を保持しています:
3~10番FD: 複数のpipe 0~2番FD: ttyp1(仮想端末)
gdbは内部的にデバッグ対象との通信(制御、I/Oのリダイレクト、break通知など)に複数のpipeを使っています。
- これらのpipeは**fork/exec後の子プロセス(デバッグ対象)との通信チャネル**や、**gdbのバックエンド制御**のために使われており、gdb内部のスレッドや処理ロジックが何かを\*\*「読む」ために`piperead`を呼んでいる**が、実際の**書き込みはkernel側でバッファ済み**または**別プロセス/カーネルスレッドにより既に供給済み\*\*である可能性があります。
—
### 2. gdbの`next`コマンドが内部的に`waitpid()`を繰り返す
- `next`の実装では、以下のようなループが走ります:
while (!breakpoint_hit) { waitpid(child, ...); check_status(); consume_io(); // ← ここでpipe_readが走る可能性あり }
このような状態でgdbはしばしば**自分自身のスレッドがselect/pollでpipeを監視し、それを`read()`で取り出して処理**します。例えば以下のような箇所が該当します:
- 子プロセスのstdout/stderr監視
- 内部通信用pipe(たとえばPython拡張やTUIのバックエンド、イベントループ)
→ `piperead()`だけが走るのは**gdbが読み取り役のみであり、書き込みは外部要因**(別スレッドやカーネル)で済んでいるケースがあり得ます。
—
### 3. `pipewrite()`は`copyout`失敗時に再試行されない
OpenBSDの`pipewrite()`は、ユーザーデータのコピーが一気にできる場合、即座に成功して`write`側が一瞬で終了することがあります(そして、それにBPが間に合わない可能性もゼロではないが、通常は発生する)。
しかし、あなたの環境では明示的に \*\*「pipe\writeヒットなし」\*\*ということなので、
- すでにpipeバッファに書かれており、それをgdbが読み出しているだけ
- あるいは\*\*`pipewrite`を呼んでいるのがgdbプロセス以外(たとえばカーネルスレッドやlibthread)\*\*
という可能性が高いです。
—
### 4. gdbが複数のpipeを使い、read側だけがトリガーされている
`fstat`出力から、gdbが8本のpipe(4ペア)を開いています(fd 3〜10)。
FD 3,4: pipe 0xd4609adc FD 5,6: pipe 0xd4609c5c FD 7,8: pipe 0xd4609d1c FD 9,10: pipe 0xd4609a1c
このような複数pipe構成は、**非同期I/O処理ループやインターナルRPC実装**でよく見られる構成です。いずれも「**データがすでに存在しているpipeに対するread**」であれば、`pipewrite()`には至りません。
—
## 🧪 調査のための追加アプローチ
### ✔ 1. `ktrace`/`kdump`でgdbの`write()`呼び出しをトレース
doas ktrace -i -p <gdb-pid>
doas kdump -T
→ `write()`の呼び出しがどのFDに向いているかを確認します。
—
### ✔ 2. `dtrace`(OpenBSDでは`dtrace`は制限あり)または`ktrace`で`pipewrite`システムコールの呼び出しプロセスを限定トレース
—
### ✔ 3. ユーザーランドコードからpipeを明示的にwriteして再実験
- `a.out`側にpipe-writeするコードを埋め込み、ユーザランド起点のpipe\writeを観測できるか確認します。
- 例:
int fds[2]; pipe(fds); write(fds[1], "hello", 5); // pipe_write発生 read(fds[0], buf, 5); // pipe_read発生
—
## 🧩 まとめ
現象 | 考えられる原因 |
------------------- | ------------------------------------ |
`piperead()`が多数ヒット | gdb内部の制御用pipeのread処理、非同期処理の影響 |
`pipewrite()`がゼロ | 書き込みはgdb自身ではなく、他のエンティティ、またはバッファ済みデータ |
`next`での大量発生 | waitループ中のI/Oポーリング、あるいはスレッド間pipeの消費動作 |
—
必要なら、`a.out`のコードも提示いただければ、より対象を特定できます。
何かこじつけっぽい気がするけど、どうよ? AIも政治家と一緒で、自分の都合の良い 様に解釈してマウントを取ってくるんで、ある程度距離を置く事が大事だな。
資料
sakae@lu:tmp$ qemu-system-riscv64 -nographic -machine help Supported machines are: microchip-icicle-kit Microchip PolarFire SoC Icicle Kit none empty machine shakti_c RISC-V Board compatible with Shakti SDK sifive_e RISC-V Board compatible with SiFive E SDK sifive_u RISC-V Board compatible with SiFive U SDK spike RISC-V Spike board (default) virt RISC-V VirtIO board
debug octox
octoxをgdbしようとして、起動スクリプトに -s -S を追加しておいて起動。 やおらgdbしようとしたら、駄目って言われた。 gdb-multiarchは、いかにもって感じだけどな。まぎらわしいぞ。 gcc-riscv64-unknown-elfだと普通のC言語用だしなあ。
これを頂いてきて、riscv64-unknown-elf-gdb を、ぶっこぬいて使う。 結構古い時代のものだけど、とりあえずって事です。
使い方は、最初にoctoxを起動します。
sakae@lu:octox$ cargo run --target riscv64gc-unknown-none-elf Running `qemu-system-riscv64 -s -S ... target/riscv64gc-unknown-none-elf/debug/octox
それから、やおらgdbを起動します。
sakae@lu:octox$ riscv64-unknown-elf-gdb target/riscv64gc-unknown-none-elf/debug/octox GNU gdb (SiFive GDB-Metal 10.1.0-2020.12.7) 10.1 : (gdb) source .gdbinit 0x0000000000001000 in ?? () (gdb) b fork Breakpoint 1 at 0x80007ee8: fork. (2 locations) (gdb) c Continuing. [Switching to Thread 1.2] Thread 2 hit Breakpoint 1, kernel::syscall::SysCalls::fork () at src/kernel/syscall.rs:334 334 fork() (gdb) bt #0 kernel::syscall::SysCalls::fork () at src/kernel/syscall.rs:334 #1 0x0000000080006f94 in kernel::syscall::Fn::call (self=...) at src/kernel/syscall.rs:71 #2 0x0000000080007178 in kernel::syscall::syscall () at src/kernel/syscall.rs:125 #3 0x000000008000de06 in kernel::trap::usertrap () at src/kernel/trap.rs:81 #4 0x00000000000034dc in ?? ()
.gdbinitはちゃんと設定すれば起動時に読み込んでくれるけど、面倒なので手動でロード してます。手入力って方法でも良いです。
sakae@lu:octox$ cat .gdbinit target remote 127.0.0.1:1234
こんな程度ですから。
それからスレッドなんて言われたんで、一応確認。
(gdb) info threads Id Target Id Frame 1 Thread 1.1 (CPU#0 [running]) 0x00000000800185d2 in kernel::proc::Cpu::unlock (self=0x8005ec58 <kernel::proc::CPUS>) at src/kernel/proc.rs:116 * 2 Thread 1.2 (CPU#1 [running]) kernel::proc::fork () at src/kernel/proc.rs:815
ケチケチ作戦で -smp 2 にしてるんで、スレッドは2つなのかな。
octox userlang debug
どうやってユーザーランド側のアプリをgdbするか? 過去ログを漁ったら、こんなのが 出てきた。 xv6-public の sh とか xv6-armv7 (6) ね。進歩無いね。いや、アーキテクチャが次世代の石だし、今を ときめくRustなんで、少しは進歩してるか。
しかしRustを選んだ特有の難しさが有る。あの人に相談ですよ。
user/Cargo.tomlに追加して、デバッグ情報を付与する。
[profile.dev] debug = true opt-level = 0 [profile.release] debug = true
topに有るCargo.tomlにも追加しておく。
[profile.release] debug = true
確認。この状態だとカーネルを起動した時に、initが失敗してパニくってしまう。 よって面倒だけど、一度 _lsを保存。Cargo.tomlの追加した部分をコメントにして 再ビルド。それからデバッグ情報付きなlsを復帰させる事。
sakae@lu:octox$ riscv64-unknown-elf-readelf -S target/riscv64gc-unknown-none-elf/release/_ls | grep debug [ 6] .debug_loc PROGBITS 0000000000000000 00008000 [ 7] .debug_abbrev PROGBITS 0000000000000000 0003f9a3 [ 8] .debug_info PROGBITS 0000000000000000 00043854 [ 9] .debug_aranges PROGBITS 0000000000000000 000c575a [10] .debug_ranges PROGBITS 0000000000000000 000c8e6a [11] .debug_str PROGBITS 0000000000000000 000f2c2a [14] .debug_frame PROGBITS 0000000000000000 0017f890 [15] .debug_line PROGBITS 0000000000000000 001817a0
こんな風にデバック情報がユーザーランドのアプリに付いている事が絶対条件。
それから、gdbにユーザーランドのシンボルを認識させる。0x0は仮想アドレス。
sakae@lu:release$ riscv64-unknown-elf-readelf -l _jell Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000001000 0x0000000000000000 0x0000000000000000 0x0000000000018f8c 0x0000000000018f8c R E 0x1000
(gdb) add-symbol-file target/riscv64gc-unknown-none-elf/release/_jell 0x0 (gdb) b evaluate_list Breakpoint 2 at 0x457e (gdb) b evaluate_conj Cannot access memory at address 0x9e98 (gdb) c Continuing. Breakpoint 2, 0x000000000000457e in _jell::Evaluator::evaluate_list () (gdb) bt #0 0x000000000000457e in _jell::Evaluator::evaluate_list () #1 0x0000000000004270 in _jell::Evaluator::evaluate () Backtrace stopped: frame did not save the PC
ちゃんと停止してくれた。
(gdb) l 1 in /rustc/5e16c662062fd6dee91f0fe2a1580483488d80cf/library/core/src/num/ uint_macros.rs (gdb) f 1 #1 0x0000000000004270 in _jell::Evaluator::evaluate () (gdb) l 1 in /rustc/5e16c662062fd6dee91f0fe2a1580483488d80cf/library/core/src/num/ uint_macros.rs
けど、マクロっぽくて、gdbするには絶望的だな。
(gdb) info source Current source file is src/kernel/exec.rs Compilation directory is /var/my/srcs/octox Located in /var/my/srcs/octox/src/kernel/exec.rs Contains 263 lines. Source language is rust. Producer is clang LLVM (rustc version 1.89.0-nightly (5e16c6620 2025-05-24)). Compiled with DWARF 2 debugging format. Does not include preprocessor macro info. (gdb) directory src/user/bin/ Source directories searched: /var/my/srcs/octox/src/user/bin:$cdir:$cwd
更に悪い事に、ソースが正しく表示されない。一見、dirrectory指令が効力を発揮 してる様に見えるけど。そして更に更に悪い知らせは、ステップ実行もユーザーランド 側では、ままならない事実。こうなったらRust+riscV+gdbの次期黄金コンビは棄却 せざるを得ないな。
xv6-riscv
悔しいのでMIT提供のxv6-riscvでやってみる。勿論ターゲットはユーザーランド側ね。 手間いらずのlsを遡上にする。.gdbinitは一度make qemu-gdbすると自動作成される。
sakae@lu:xv6-riscv$ cat .gdbinit set confirm off set architecture riscv:rv64 target remote 127.0.0.1:26000 symbol-file kernel/kernel set disassemble-next-line auto set riscv use-compressed-breakpoints yes
先にqemuを起動。
sakae@lu:xv6-riscv$ make qemu-gdb *** Now run 'gdb' in another window. qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::26000
指示された様にgdbを起動。
sakae@lu:xv6-riscv$ riscv64-unknown-elf-gdb -q The target architecture is set to "riscv:rv64". warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0000000000001000 in ?? () (gdb) c Continuing.
これでカーネルが起動して、その最後にinitが誕生しshが動作開始。
^C Thread 3 received signal SIGINT, Interrupt. [Switching to Thread 1.3] scheduler () at kernel/proc.c:455 455 intr_on(); (gdb) b exec Breakpoint 1 at 0x8000446e: file kernel/exec.c, line 24. (gdb) c Continuing. [Switching to Thread 1.1] Thread 1 hit Breakpoint 1, exec (path=path@entry=0x3fffff9f10 "ls", argv=argv@entry=0x3fffff9e10) at kernel/exec.c:24 24 {
Ctl-cでgdbを停止させ、execにBPを配置してから継続。sh側でlsするとブレークしてくれる。
(gdb) add-symbol-file user/_ls 0x0 add symbol table from file "user/_ls" at .text_addr = 0x0 Reading symbols from user/_ls... (gdb) b ls Breakpoint 2 at 0x98: file user/ls.c, line 28. (gdb) c Continuing. Thread 1 hit Breakpoint 2, ls (path=path@entry=0xaf8 ".") at user/ls.c:28 28 { (gdb) bt #0 ls (path=path@entry=0xaf8 ".") at user/ls.c:28 #1 0x0000000000000262 in main (argc=<optimized out>, argv=<optimized out>) at user/ls.c:81 #2 0x0000000000000274 in start () at user/ulib.c:13 #3 0x00000000000000da in ls ( path=0x63 <fmtname+99> "\205N\205", <incomplete sequence \357>) at user/ls.c:45 Backtrace stopped: Cannot access memory at address 0x4f98
シンボルテーブルに _ls を追加してから ls()にBPを設定。
(gdb) n 34 if((fd = open(path, O_RDONLY)) < 0){ (gdb) 39 if(fstat(fd, &st) < 0){ (gdb) 45 switch(st.type){ (gdb) 52 if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){ (gdb) n 56 strcpy(buf, path); (gdb) 57 p = buf+strlen(buf); (gdb) 58 *p++ = '/'; (gdb) 59 while(read(fd, &de, sizeof(de)) == sizeof(de)){ (gdb) 60 if(de.inum == 0) (gdb) 62 memmove(p, de.name, DIRSIZ); (gdb) 63 p[DIRSIZ] = 0; (gdb) 64 if(stat(buf, &st) < 0){ (gdb) 68 printf("%s %d %d %d\n", fmtname(buf), st.type, st.ino, (int) st.size); (gdb) 59 while(read(fd, &de, sizeof(de)) == sizeof(de)){
後はステップ実行。
(gdb) info source Current source file is user/ls.c Compilation directory is /var/my/srcs/xv6-riscv Located in /var/my/srcs/xv6-riscv/user/ls.c Contains 87 lines. Source language is c. Producer is GNU C17 13.2.0 -mcmodel=medany -mabi=lp64d -misa-spec=20191213 -march=rv64imafdc_zicsr -ggdb -gdwarf-2 -O -fno-omit-frame-pointer -fno-common -fno-builtin-strncpy -fno-builtin-strncmp -fno-builtin-strlen -fno-builtin-memset -fno-builtin-memmove -fno-builtin-memcmp -fno-builtin-log -fno-builtin-bzero -fno-builtin-strchr -fno-builtin-exit -fno-builtin-malloc -fno-builtin-putc -fno-builtin-free -fno-builtin-memcpy -fno-builtin-printf -fno-builtin-fprintf -fno-builtin-vprintf -fno-stack-protector -fno-pie. Compiled with DWARF 2 debugging format. Does not include preprocessor macro info.
info sourceでちゃんとソースの場所を保持してる事が確認できた。
(gdb) f 2 #2 0x0000000000000274 in start () at user/ulib.c:13 13 main(); (gdb) l 8 // 9 void 10 start() 11 { 12 extern int main(); 13 main(); 14 exit(0); 15 }
ついでにフレーム2を確認。
README
タコペディア にっぽん なんて本を読んだ。
タコの足は何本 ? 正確に描くのはにっぽん人。西洋では余り気にせずに 7本とか6本とかが有るらしい。
タコは多幸に通じるので、願掛けの対称にもなり、その場合は7本で描き 、成願したら、もう一本加えて8本にする。達磨さんのアレと同じ発想 だわな。
タコは強力な吸盤を持っている。その吸盤にあやかって、出来物とか毒の 吸い出し方面のシンボルにもなった。
タコの吸い出しと言う膏薬とか薬ね。昔、お世話になったな。今だったら、 老人性のイボ退治かな。この間、首にできて、それが服に擦れて痛かった。 医者へ行って、取ってもらったぞ。タコの吸い出しではなくて、組織を 凍死させるって方法かな?
白い湯気がでてるディワー瓶に綿棒を浸して、それを1回、2回と数えながら 塗りたくる。十分に魔術的な治療であった。
何でも食べちゃう中国人。さぞかしタコも食べているかと思ったら、人気は 全く無いそうだ。10本足のイカは中国内部まで浸透してるのに、タコは沿岸 地方の極わずかの地域でのみ食されているそうだ。全世界で水揚げされるタコ の60%は日本人が消費してるそうなんで、このアンバランスはどういう訳? 著者も首を捻っておられたぞ。 オイラーも不思議に思うぞ。八が大好き中国人、何故に避けるの?