SystemTap用のemacsメジャーモードを書いてみた

tokyo-emacsustreamで眺めていてElispが書いてみたくなり、SystemTapスクリプトを編集&ついでに実行できるメジャーモードを書いてみました。
これを使うと、EmacsからホストのLinuxカーネルをHackして、いろんな情報をとったりできます。


書いたものは本エントリの最後に。CodeRepos に入れて共有すると良さげだけど、commit権もってない…。id:Yappoにお願いすれば良いのだろうか。

2008-07-26 追記

CodeReposにアップしました。閲覧 & ダウンロード


ろくに数行以上のElispを書いたことも無いくせに、いきなりメジャーモードに挑戦するとか無謀すぎな気もしますが、気にしない気にしない。

実現したいこと:

  • SystemTapスクリプトショートカットで実行開始/停止して結果が確認できる
  • ついでに上記に対応して専用メニューとかもつけてみる
  • 適当にインデントしてくれる
  • 色付け(font-lock対応)

一番上が最もやりたくて、あとは"メジャーモードらしさ"として必要かなーという感じ。

方針

SystemTapスクリプトAwkライクな言語ということになっています。文末にセミコロンが要らないとか。現在のところ、Awkのモードはcc-modeの派生モードとして実現されているようです。そこで、これをさらに派生させて実現してみることにします。
cc-modeはそこそこ強力な構文解析エンジンみたいなものを提供してくれるので、これを使えば比較的楽にfont-lockや自動インデントに対応できるんじゃないかという目論みです。

幸い、cc-modeを派生させる例が http:/cc-mode.sourceforge.net/derived-mode-ex.el に用意されているので、これを真似して書いてみます。

使い方

emacs 22、cc-mode 5.3以降が必要なんだと思います。多分。Fedora 9ならOKっぽい。
本エントリの最後にあるsystemtap-mode.elをload-pathの通った場所にいれて、.emacs

  (autoload 'systemtap-mode "systemtap-mode")
  (add-to-list 'auto-mode-alist '("\\.stp\\'" . systemtap-mode))

とか書けば良いと思います、多分。ちなみにload-pathを ~/.elisp というディレクトリに通したい場合、

(setq load-path (cons "~/.elisp" load-path))

.emacsの先頭あたりに書いておけばOKです。


で、emacsをroot権限で動かしておいて(sudo emacsとして起動するとか。emacsのbufferからsudoでパスワード受け付ける方法が分からない)、

probe kernel.function("schedule").return {
    log(execname())
}

とか書いてファイルに保存し、C-c e をタイプすれば、プロセスの切り替わるたびに、切り替わったプロセス名が別バッファに表示されていきます。
これだとものすごい勢いでメッセージがバッファに追記されてしまうので、止めるには C-c c をタイプすればOK。
10000回くらいでexit()するようにした方が良いでしょうね。。


ちなみにFedora 9でSystemTapを使うには、事前に

# yum install systemtap kernel-devel
# debuginfo-install kernel

くらいが必要です。


以下、systemtap-mode.elの中身について。

内容の説明というかメモ

基本的に http:/cc-mode.sourceforge.net/derived-mode-ex.el とcc-modeに付属のcc-langs.elなどを参考に書いていきます。


まず冒頭で、

(require 'cc-mode)
(eval-when-compile
  (require 'cc-langs)
  (require 'cc-fonts)
  (require 'cc-awk))

(eval-and-compile
  (c-add-language 'systemtap-mode 'awk-mode))

などと書いてあげると、awk-modeから派生してsystemtap-modeを作ってくれる。あとは、

(c-lang-defconst c-primitive-type-kwds
				 systemtap '("string" "long" "function" "global" "probe"))
(c-lang-defconst c-block-stmt-2-kwds
				 systemtap '("else" "for" "foreach" "if" "while"))
(c-lang-defconst c-simple-stmt-kwds
				 systemtap '("break" "continue" "delete" "next" "return"))

などのように、キーワード類をc-lang-defconstを使ってsystemtap-mode用に対応する「定数」を定義してあげれば、色付けやインデントは良きに計らってくれるようです。


肝心のスクリプト実行部分は、自分で関数を定義してあげます。
プロセスの起動は、start-processで実現できます。
適当な名前のバッファを作って表示し、start-processするときに割り当ててあげれば良いのかな。で、"stap -v 現在のファイル" を実行すればよいのかな。

自分で止まらないSystemTapスクリプトの場合、Ctrl-Cでインタラプトする必要があるので、interrupt-process を使って止める関数も用意します。

とりあえず下のような感じにしてみました。ただし名前が固定で複数プロセスの起動は考慮してないのは今後の課題。。。

(defvar systemtap-buffer-name "*SystemTap*"
  "name of the SystemTap execution buffer")

(defun execute-systemtap-script ()
  "Execute current SystemTap script"
  (interactive)
  (if (get-buffer systemtap-buffer-name)
	  (kill-buffer systemtap-buffer-name))
  (get-buffer-create systemtap-buffer-name)
  (display-buffer systemtap-buffer-name)
  (start-process "systemtap-script" systemtap-buffer-name
				 "stap" "-v" (expand-file-name (buffer-name (window-buffer))))
  (message "execution of SystemTap script started."))

(defun interrupt-systemtap-script ()
  "Interrupt running SystemTap script"
  (interactive)
  (interrupt-process "systemtap-script")
  (message "SystemTap script is interrupted."))

あとは、キーマップからこの2関数を呼び出せるようにします。
cc-modeのキーマップは(c-make-inherited-keymap)で取れるので、これを拡張して定義。

(defvar systemtap-mode-map
  (let ((map (c-make-inherited-keymap)))
	(define-key map "\C-ce" 'execute-systemtap-script)
	(define-key map "\C-cc" 'interrupt-systemtap-script)
	map)
  "Keymap used in systemtap-mode buffers.")

ついでにメニューも用意します。こちらも(c-lang-const c-mode-menu systemtap)に付け足します。

(easy-menu-define systemtap-menu systemtap-mode-map "SystemTap Mode Commands"
  (cons "SystemTap"
		(append
		 '(["Execute This Script" execute-systemtap-script t]
		   ["Interrupt Execution of Script" interrupt-systemtap-script (get-process "systemtap-script")]
		   "----")
		 (c-lang-const c-mode-menu systemtap))))

あとは、モード定義の中で、 (use-local-map systemtap-mode-map) とか (easy-menu-add systemtap-menu) として設定してあげればOK。

今後の課題

まだまだ制約多し。今のところ、

  • 埋め込みC %{ %} やカーネルバージョンに応じたスクリプト切り替え %( %? %:%) がちゃんとインデントできない
  • stapコマンドに渡すオプションを指定できない
  • よってどのみち埋め込みCは書けない

といった具合ですか。あと

  • probe kernel.function("〜") { 〜 } といった決まり文句を自動挿入。

とか? これはyasnippet 使えば良いか。

systemtap-mode.elの内容

ライセンスはGPL v2 or laterです。

;;; SystemTap-mode based on cc-mode

(defconst systemtap-mode-version "0.01"
  "SystemTap Mode version number.")

;;
;; Usage:
;;   Add below to your ~/.emacs file.
;;
;;  (autoload 'systemtap-mode "systemtap-mode")
;;  (add-to-list 'auto-mode-alist '("\\.stp\\'" . systemtap-mode))
;;
;; Note:
;;   The interface used in this file requires CC Mode 5.30 or
;;   later.
;;   Only tested in emacs 22.
;;

;; TODO:
;;   - indent embedded-C %{ ... %} correctly
;;   - add parameter for indentation
;;   - ...

(require 'cc-mode)
(eval-when-compile
  (require 'cc-langs)
  (require 'cc-fonts)
  (require 'cc-awk))

(eval-and-compile
  (c-add-language 'systemtap-mode 'awk-mode))

;; Syntax definitions for systemtap

(c-lang-defconst c-primitive-type-kwds
				 systemtap '("string" "long" "function" "global" "probe"))

(c-lang-defconst c-block-stmt-2-kwds
				 systemtap '("else" "for" "foreach" "if" "while"))

(c-lang-defconst c-simple-stmt-kwds
				 systemtap '("break" "continue" "delete" "next" "return"))

(c-lang-defconst c-cpp-matchers
				 systemtap (cons
			 '(eval . (list "^\\s *\\(#pragma\\)\\>\\(.*\\)"
							(list 1 c-preprocessor-face-name)
							'(2 font-lock-string-face)))
			 (c-lang-const c-cpp-matchers)))

(c-lang-defconst c-identifier-syntax-modifications
  systemtap '((?. . "_") (?' . ".")))
(defvar systemtap-mode-syntax-table nil
  "Syntax table used in systemtap-mode buffers.")
(or systemtap-mode-syntax-table
    (setq systemtap-mode-syntax-table
		  (funcall (c-lang-const c-make-mode-syntax-table systemtap))))

(defcustom systemtap-font-lock-extra-types nil
  "font-lock extra types for SystemTap mode")

(defconst systemtap-font-lock-keywords-1 (c-lang-const c-matchers-1 systemtap)
  "Minimal highlighting for SystemTap mode.")

(defconst systemtap-font-lock-keywords-2 (c-lang-const c-matchers-2 systemtap)
  "Fast normal highlighting for SystemTap mode.")

(defconst systemtap-font-lock-keywords-3 (c-lang-const c-matchers-3 systemtap)
  "Accurate normal highlighting for SystemTap mode.")

(defvar systemtap-font-lock-keywords systemtap-font-lock-keywords-3
  "Default expressions to highlight in SystemTap mode.")


(defvar systemtap-mode-abbrev-table nil
  "Abbreviation table used in systemtap-mode buffers.")

(defvar systemtap-mode-map
  (let ((map (c-make-inherited-keymap)))
	(define-key map "\C-ce" 'execute-systemtap-script)
	(define-key map "\C-cc" 'interrupt-systemtap-script)
	map)
  "Keymap used in systemtap-mode buffers.")

(easy-menu-define systemtap-menu systemtap-mode-map "SystemTap Mode Commands"
  (cons "SystemTap"
		(append
		 '(["Execute This Script" execute-systemtap-script t]
		   ["Interrupt Execution of Script" interrupt-systemtap-script (get-process "systemtap-script")]
		   "----")
		 (c-lang-const c-mode-menu systemtap))))

;;;###autoload


;; Execution function of Current Script

(defvar systemtap-buffer-name "*SystemTap*"
  "name of the SystemTap execution buffer")

(defun execute-systemtap-script ()
  "Execute current SystemTap script"
  (interactive)
  (if (get-buffer systemtap-buffer-name)
	  (kill-buffer systemtap-buffer-name))
  (get-buffer-create systemtap-buffer-name)
  (display-buffer systemtap-buffer-name)
  (start-process "systemtap-script" systemtap-buffer-name
				 "stap" "-v" (expand-file-name (buffer-name (window-buffer))))
  (message "execution of SystemTap script started."))

(defun interrupt-systemtap-script ()
  "Interrupt running SystemTap script"
  (interactive)
  (interrupt-process "systemtap-script")
  (message "SystemTap script is interrupted."))


;;

(defun systemtap-mode ()
  "Major mode for editing SystemTap script.

Key bindings:
\\{systemtap-mode-map}"
  (interactive)
  (kill-all-local-variables)
  (c-initialize-cc-mode t)
  (set-syntax-table systemtap-mode-syntax-table)
  (setq major-mode 'systemtap-mode
		mode-name "SystemTap"
		local-abbrev-table systemtap-mode-abbrev-table
		abbrev-mode t)
  (use-local-map systemtap-mode-map)
  (c-init-language-vars systemtap-mode)
  (c-common-init 'systemtap-mode)
  (easy-menu-add systemtap-menu)
  (run-hooks 'c-mode-common-hook)
  (run-hooks 'systemtap-mode-hook)
  (c-update-modeline))


(provide 'systemtap-mode)