ocamldebugの調査

ML言語の一種であるOcamlに付属している、ocamldebugというデバッガについて調べたのでメモ。

かなり間違っている可能性があるので、注意。

  • gdbライクなCUI
    • break,info,helpコマンド等、gdbを知っている人なら何となく使えるようにコマンド名がつけられてます
    • でも、printコマンドで関数呼び出しや代入が行えないなど*1、gdbと違う点もあるので注意
  • breakで停止できるのは、eventと呼ばれる箇所でのみ
  • debuggerとdebugeeは別プロセス
    • Unixだと、BSDソケットを使ってプロセス間通信することでdebugee上に存在する変数の値の参照等を行っている。ここらへんは、Javaデバッガの実装に近い
  • 日本語の処理関係は、まだ弱いらしい
  • mlコード内のprint_int, print_string等の標準出力への出力用関数の処理は、ocamldebugでステップ実行している間は実際には行われない
    • debugeeの実行が終了した段階で出力される
    • 出力をバッファしてるのかな?
    • おそらく、ファイル出力やTCP等によるデータ送信も同様
    • タイムトラベル機能(後述)のためにこのような実装になってる?
    • ファイルからの入力がどうなるかはまだ調べてない
  • 変数の値等の状態も含めて実行を前の地点まで巻き戻して、そこから再実行する機能がある(タイムトラベル機能)
    • ocamldebugが適切なタイミングでcheckpointを取り、それを利用して実行の巻き戻しを行う
    • 巻き戻した地点がcheckpointを取った地点と一致しない場合は、最寄のcheckpointからその地点まで再実行して停止?
    • checkpointを取るための実装にはforkを使ってる*2
    • プロセスIDが巻き戻す前と後で違うなど、完全に状態を巻き戻すことはできないため、いやらしいプログラムの実行を巻き戻すと挙動がおかしくなった

ocamldebugはOcamlで実装されているので、私には読めん・・・

*1http://caml.inria.fr/pub/docs/manual-ocaml.bak/manual030.htmlの16.7項に書かれているBNF記法を見る限りできないよなあ・・・たぶん

*2http://caml.inria.fr/pub/docs/manual-ocaml/manual030.htmlのTurning reverse execution on and offの項目に書いてある

Debug.Traceを使ってprintf風デバッグ

Haskell処理系であるGHCにはDebug.Traceモジュールというのが提供されています*1。その中のtrace関数を使うと、C言語のおけるprintfデバッグのようなことができます。

ただ、Haskellは遅延評価を行う言語なので、traceが呼び出されるタイミングに注意する必要があるそうです。

以下、参考元サイト

*1http://www.haskell.org/ghc/docs/latest/html/libraries/base/Debug-Trace.html :traceの実装自体は難しくないそうなので、他の処理系でも同じようなモジュールがあるかもしれません

Haskellだってバグるよねっていう話し(C言語の関数をHaskellで呼ぶ)

C言語の関数をHaskellから呼び出すための記述に関する仕様が存在します*1

自分が使っているHaskell処理系のGHCは、この仕様に準拠しています。そこで、この機能を使ってバグったC関数をHaskellから呼ぶことでメモリアクセス違反させてみます*2

// foo.c
#include "HsFFI.h"

static int mem;

HsPtr wrong_ptr(int wrong)
{
if(wrong){
return (HsPtr)0xcfcfcfcf;
}else{
return (HsPtr)
}

}

このwrong_ptr関数を呼ぶHaskellコードは以下、

-- main.hs

import Foreign

foreign import ccall "wrong_ptr" wrong_ptr :: Int -> IO (Ptr Int32)

main = do
ptr <- wrong_ptr 0            -- 変数memのアドレスがptrを束縛する
pokeElemOff ptr 0 5           -- ptr[0] = 5
peekElemOff ptr 0 >>= print   -- 5を出力

ptr <- wrong_ptr 1            -- 0xcfcfcfcfがptrを束縛する
pokeElemOff ptr 0 100         -- メモリアクセス違反!!!
peekElemOff ptr 0 >>= print

で、実際にやってみると、*3

> ghc foo.c main.hs -ffi -o foo
> foo
5
<------ここで異常終了
>

この例はわざとらしいですが、Haskellのプログラムも、わかりにくいタイプのバグが入り込む余地があるっていうことがわかると思います。

C言語の関数を使わなきゃ良いっていう意見もあると思いますが、win32apiやらLinux等のシステムコールを直に扱いたい時には使わざる得ないでしょう*4

また、絶対にC言語の関数を使わないっていうスタンスは、既存のライブラリを使ったほうが楽なのに、わざわざHaskellで書き直さなきゃいけないといった問題が発生する点でコストが高いと思います。

個人的には、デバッグ周辺のツールがもう少し充実しないと、普通のプログラムを書くのにHaskellを使う気になれません。上の例よりもっと複雑なコードがバグった時のデバッグ作業を考えると恐ろしすぎます。

GHC6.7からはデバッガが付属するそうなので、正式にリリースされたら使ってみようと思います。

*1http://www.cse.unsw.edu.au/~chak/haskell/ffi/ffi.pdf

*2:このコードを書く際には、こちらの日記を参考にさせていただきました。http://d.hatena.ne.jp/E_Mattsan/20070616

*3:WindowsにmsiパッケージからGHCをインストールした場合、cc1が存在しないっていわれますが、C:/ghc/ghc-6.6.1/gcc-libにPATHを通せば動きます(GHC6.6.1をデフォルトのパスにインストールした場合)

*4:win32apiは、ある程度Haskellの標準ライブラリとして用意されてるようです

Haskell本の感想とMonad

教養のために「ふつうのHaskellプログラミング」を読みました。Haskellを触ったことがない私のような人が読むのにちょうど良い本でした。

ただ、Monad則については読んでも良くわかりませんでした。Maybeモナド等、Monadインスタンスの使い方はわかるのですが、Monad則がどのようにして出てきて、どんな利点があるのか等の理論的な部分はページ量の関係か本に詳しく書いてありません。

そこで、Monadについて解説しているWebサイトを探して読んでみましたが、いまいち理解できてません。

ただ、IO処理等の副作用を持つ操作にMonadが利用できる理由については、以下のサイトを読むとなんとなくわかった気になれます。

スレッドの関数一覧(pthread, win32スレッド)

輪講でpthreadについて、ゼミ担当の人の話を聞きました*1。せっかくなので、windowsのスレッド用 apiと大体対応する関数を並べてみます。

なお、個々のスレッドをあらわすデータ構造の型は、pthreadではpthread_t, windowsスレッドではHANDLEです。各関数の機能詳細は、ググれば色んなサイトが出てくるのでそちらを見てください。

機能 pthread関数名 win32 スレッドapi名
スレッドの作成 pthread_create CreateThread, _beginthreadex*2
自スレッドの終了 pthread_exit ExitThread, _endthreadex*3
他スレッドを終了 pthread_cancel TerminateThread*4
スレッド終了時に自動的にスレッド作成時に確保したデータ(stack領域等)を解放するように指示 pthread_detach デフォルトでこの設定
他スレッドの終了を待つ pthread_join WaitForSingleObject, WaitForMultipleObjects
使い終わったスレッドid(handle)を解放する 解放する必要なし CloseHandle
現在実行中のスレッドのid(handle)を返す pthread_self GetCurrentThread
mutexの作成 pthread_mutex_init CreateMutex
mutexのロックを取る pthread_mutex_lock WaitForSingleObject
mutexのロックを解除する pthread_mutex_unlock ReleaseMutex
mutexの破棄 pthread_mutex_destroy CloseHandle
スレッド固有の大域変数(TSD*5 or TLS*6 )を指す領域を確保 pthread_key_create TlsAlloc
TSD(TLS)を指す領域を解放 pthread_key_delete TlsFree
TSD(TLS)のアドレスを得る pthread_getspecific TlsGetValue
TSD(TLS)のアドレスをセット pthread_setspecific TlsSetValue

注意点

pthread_cancelで終了を要求されたスレッドは、その要求に抵抗することができます(pthread_setcancelstate関数でPTHREAD_CANCEL_DISABLEをセットする)。また、終了要求が実際に処理されるのは、要求を出されたスレッドでcancellation point(pause、waitシステムコールなど)を通過したときだけです。

これに対して、TerminateThreadで終了要求をだされたスレッドは、その要求に抵抗することができず、終了するタイミングもシステム依存になります。

また、pthread_cancelで終了したスレッドに対してpthread_joinしたり、あらかじめpthread_detachしていれば、終了したスレッド用のstack領域等が適切に解放されるのにたいして、TerminateThreadはstack領域が解放されません。Windowsがこのような実装になっているのは、実行中の他のスレッドがTerminateThreadで終了させられたスレッドのスタックを参照したときにメモリアクセス違反が起きるのを避けるためです*7

追記

pthread_cancelを呼んだ後、呼ばれたスレッドを非同期に終了させる(cancellation pointを使わない)ようにすることもできます(pthread_setcanceltype関数)。ただ、この方法はTerminateThread同様に、あまり使うべきやりかたではないそうです。以下のブログで細かく説明されています。

http://d.hatena.ne.jp/yupo5656/20040724/p1

*1:輪講でつかった書籍は以下。Advanced Programming in the Unix Environment :ISBN 978-0201433074

*2:Cの標準ライブラリを使うスレッドを作成する場合はこちらを使う

*3:_beginthreadexで作成したスレッドの場合、こちらを使う

*4:このapiを使った場合、stack領域等は解放されません。緊急用のapiらしく、使用もあまり推奨されてません

*5:thread specific dataの略

*6:thread local storageの略。Windowsでは、こっちの呼び方をする。

*7:Advanced Windows 6.5.3:ISBN 4-7561-3805-5