vim + screen + gdbでデバッグしよう

FreeBSD上でvimデバッグする環境を整えよう。
以下、カレントディレクトリはvimのsrcディレクトリであるとします。

gdbでアタッチ

まずは素朴に

$ gdb vim

としてみる。vimが立ち上がるが、端末がvimに占居されて
しまい、gdbを操作できなくなるので当然ダメ。そこで

$ gdb --args vim -g

gvimを起動してみる。するとgvimウィンドウが現れるのだが…

(gdb) run
Starting program: /usr/home/ao/dl/vim70/src2/vim -g

Program exited normally.
(gdb) 

なんとプロセスがすでに正常終了している。
まだウィンドウが目の前に出ているのに。
GUIモードを起動するときfork()しているからだろうか。
ではあらかじめgvimを起動しておき、gdbでアタッチするしかないのかな。


[追記:2006-08-29]

vim -g -f

で fork せずに gvim を起動できることが判明。

$ vim -g
$ gdb -p 61373 

ここで、私の環境では次のようなエラーが出てしまった。

  Attaching to process 61373
  /usr/src/gnu/usr.bin/gdb/libgdb/../../../../contrib/gdb/gdb/solib-svr4.c:1443: internal-error: legacy_fetch_link_map_offsets called without legacy link_map support enabled.
  A problem internal to GDB has been detected,
  further debugging may prove unreliable.
  Quit this debugging session? (y or n) 

詳しい原因はよくわからないが、ググってみたところ

$ gdb -p 61373 /xxx/vim 

というように、プロセスIDに加えて実行ファイルの絶対パスをつけて起動すれば
アタッチできるらしい。

0x2886c697 in poll () from /lib/libc.so.5
(gdb) 

できた。
しかし毎回プロセスIDを指定するのは面倒なので、ちょっとスクリプトを書いてみた。

#!/usr/bin/env ruby

vimpath = Dir.pwd + "/vim" 
vimarg = " -g " +  ARGV.join(" ")
vimcmd = vimpath + vimarg

pid = fork()
if pid
	#puts pid
	r = Process.wait
	if r == pid
		sleep 0.3		# まだ親vimと子vim両方生きてるので少し待つ
		p = IO.popen("ps x")
		cnt = 0
		p.gets		# 見出しの行を捨てる
		backref = {}
		lines = p.readlines()
		lines.each_with_index do |x, i|
			if x.split[4] == vimpath
				puts "#{cnt+1}: " + x 
				cnt += 1
				backref[cnt] = i
			end
		end
		if cnt > 1
			puts "Some processes found."
			print "Which one? : "
			a = STDIN.gets.to_i - 1
			if not backref[a]
				puts "Wrong number."
				exit 
			end
			lineidx = backref[a]
		elsif cnt == 1
			lineidx = backref[1]
		else
			puts "No vim process found."
			exit
		end
		debuggeepid = (lines[lineidx].split)[0]
		cmd = "gdb -p #{debuggeepid} #{vimpath}" 
		puts cmd
		exec(cmd)
	end
else
	exec(vimcmd + ";")
end 

思いのほか複雑になってしまった。
gvimを起動し、gdbでアタッチするところまでを自動的にやってくれる。
GUIでないvimデバッグしたい場合はgvimの代わりにkterm -e /xxx/vim
とすればいいだろう。
これで今、gvimサスペンドし、gdbに制御が移っている。
c(continue)すれば gvim が再開する。

(gdb) c

screenを使ってgdbを操作する

vimでソースを見ながらgdbデバッグしたい。
そこでscreenを使って画面を2分割し、vimからscreenコマンドを発行して
gdbを操作する。力技。

" カレントファイルのカレント行にブレークポイント設定
command! Breakpoint call system("screen -X eval focus 'stuff \"b " . expand("%") . ":" . line(".") . "\"\\015' focus")
" ステップ実行
command! Step call system("screen -X eval focus 'stuff s\\015' focus")
" 実行再開
command! Continue call system("screen -X eval focus 'stuff c\\015' focus")
" ステップ実行(関数内に入らない)。なんと:Nextはすでに予約されている!
command! NextStep call system("screen -X eval focus 'stuff c\\015' focus")
" カレントファイルのカレント行まで実行
command! Advance call system("screen -X eval focus 'stuff \"advance " . expand("%") . ":" . line(".") . "\"\\015' focus")
" 変数の値を表示。なんと:Printはすでに予約されている!
command! -nargs=+ PrintVariable call system("screen -X eval focus 'stuff \"p " . "<args>" . "\"\\015' focus")

とりあえずこれくらいできれば使いものになるだろうか?
頑張ればもっと色々できるかもしれない。

スクリーンショット

[追記:2006-12-9]

上の Ruby スクリプトをやめて、こんなシェルスクリプトを作った。
gdbvim

#!/bin/bash
if [ $# -eq 0 ]; then
	./vim -f -g out &
	vimpid=$!
else
	./vim -f -g "$@" &
	vimpid=$!
fi

echo $vimpid >| ./vimpid

exec gdb -p $vimpid `pwd`/vim

# gdb 内で continue をするとデバッグ開始
# stopvim で vim をサスペンド

stopvim

#!/bin/sh
if [ -f vimpid ]; then
	kill -STOP `cat vimpid`
else
	echo "No file vimpid"
fi

さらに screen を操作するためにこんなシェル関数を作った。

# screen のリージョンを分割してそちらにフォーカスを移し、新しいシェルを開いて、今のリージョンと同じディレクトリに cd
sp() {
	screen -X eval split focus screen 'stuff cd\040'"$PWD"'\015'
}

cl() {
	screen -X remove	# close current window
}

bd() {
	screen -X remove	# close current window
	exit
}