The Design and Implementation of
the Gracious Days

このあたりで質問した内容。原因は判明したが、まだ未解決。

DTrace における if-then 文

DTraceのD言語にはif-thenも含めて制御構文がほとんどない。これは無限ループなどを発生させないために言語仕様(厳密にはD言語がコンパイルされた後のバイトコードの仕様)で禁じているためだ。なので、たとえば switch 文などのような条件分岐は、次のように二項演算子で書く必要がある。

inline string afstr[int af] =
    af == AF_UNSPEC ? "AF_UNSPEC" :
    af == AF_LOCAL ? "AF_LOCAL" :
    af == AF_INET ? "AF_INET" :
    af == AF_INET6 ? "AF_INET6" :
    "<unknown>";

DTraceの開発の中心となっているIllumos-gateで、昨年の8月にif-then構文が追加された。FreeBSDでは11から、この変更が含まれている。

if-thenといっても、バイトコードに条件ジャンプ命令が追加されたわけではなく、プローブを自動分割することで疑似的に if-then の機能を実現する構文糖衣である。

:::probe
{
    printf("ENTRY\n");
    if (arg0 == 0) {
        printf("FOO\n");
    } else if {arg1 == 1 && arg2 == 1) {
        printf("BAR\n");
    } else {
        printf("BAZ\n");
    }
    printf("LEAVE\n");
}

上記のようなスクリプトは、下記のスクリプトと等価だ。

:::probe
{
    printf("ENTRY\n");
}
:::probe /arg0 == 0/
{
    printf("FOO\n");
}
:::probe /arg1 == 1 && arg2 == 1/
{
    printf("BAR\n");
}
:::probe /arg0 != 0 && !(arg1 == 1 && arg2 == 1)/
{
    printf("BAZ\n");
}
:::probe
{
    printf("LEAVE\n");
}

if-thenは述語の異なるアクション定義の並びになる。同一のプローブに対する複数のアクション定義は、上から順番に実行されることが保証されている。

メールに出した質問

if-then 構文を使って、とある PID プローブに対して次のように書いた。

pid$target:::probe
{
    this->st = copyin(arg0, 1024);

    if (arg1 == 1) {
        printf("st = %s\n", stringof(this->st));
    } else {
        printf("unknown\n");
    }
}

arg0を1024バイト分抽出して、それを処理するコードである。上記コードは説明を簡単にするため、arg0全体をヌル終端の文字列とみなしてprintf()しているが、実際はarg0が全体の大きさが1024バイトの構造体を指していて、その一部が文字配列になっているので、それを表示したい、という意図のコードの一部だった。

しかしこれを実行すると、arg1 == 1の時に文字列が表示されない。おかしいと思って次のように書き換えてみると、こちらはちゃんと動く。

pid$target:::probe
{
    this->st = copyinstr(arg0);

    if (arg1 == 1) {
        printf("st = %s\n", this->st);
    } else {
        printf("unknown\n");
    }
}

どうにも原因が分からず、冒頭の質問メールで聞いてみたところ、次の事実が分かった。

  • 同一プローブに対するアクション定義はすべて順番に実行されるが、ローカル(clause-local)変数への代入のうち copyin() でカーネルにコピーしたデータは、アクション定義の境界以降、アクセスできなくなる。
  • copyinstr()でカーネルにコピーしたデータは、アクション定義の境界を跨ってもアクセスできる。
  • copyin(), copyinstr()のいずれにも無関係に定義されたデータは、アクション定義の境界を跨ってもアクセスできる。

現在の実装では、if (0) { } のような無意味な if 文を挟んだだけで、copyin() した変数の内容へアクセスできなくなってしまう。arg0 が壊れるわけではないのでもう一度 copyin() すればデータは取得できる。DTraceのマニュアルには、「同一プローブに対するアクション定義は結合される」とあるので、ローカル変数へのアクセスができなくなるというのはバグと呼んで良いと思う。

ローカル変数へのストア・ロードは stlsldls というバイトコードで行なわれる。アクション定義境界で、一時バッファにロードしたデータへのアクセスができなくなってしまうようだ。copyin()copyinstr()では処理が異なっており、後者は一時バッファとは異なる領域に保存されるため、たまたま継続してアクセスができるようになっている。いずれにしても、「アクション定義を分割して書く」ことは、最初は想定していなかったのかも知れない。

現存するすべてのDTrace実装が同じ問題を抱えている。どうやって解決するかは議論中だが、現段階では複数に分割されたアクション定義でcopyin()したデータを使う場合には、プログラムする側が注意するしかなさそうだ。


Next post: uniq(1)の空白

Previous post: macOS Sierra の NFS