kubo39's blog

ただの雑記です。

小ネタ: safe functionとcast function of null

おなじみ(?), safe functionを考えてみようの会です.

突然ですが, 以下のコードは合法でしょうか?

void main() @safe
{
    (cast(void function() @safe) null)();
}

結果はおおかた予想がつくとは思いますが、Segmentation Faultになります。

(dmd-2.084.0)$ rdmd safecastnull.d
zsh: segmentation fault (core dumped)  rdmd safecastnull.d

さて、合法かどうかですが, まず safe functionの定義 から照らし合わせてみると

  • No casting from a pointer type to any type other than void*.
  • No casting from any non-pointer type to a pointer type.
  • No pointer arithmetic (including pointer indexing).
  • Cannot access unions that have pointers or references overlapping with other types.
  • Calling any system functions.
  • No catching of exceptions that are not derived from class Exception.
  • No inline assembler.
  • No explicit casting of mutable objects to immutable.
  • No explicit casting of immutable objects to mutable.
  • No explicit casting of thread local objects to shared.
  • No explicit casting of shared objects to thread local.
  • No taking the address of a local variable or function parameter.
  • Cannot access __gshared variables.
  • Cannot use void initializers for pointers.
  • Cannot use void initializers for class or interface references.

一個目と二個目が怪しいですね.

nullの定義 もみてみましょう. 最初の文はnullはポインタやら動的配列やらを表すことができる値と解釈できます.

次ですが, なんと, "it is an exact conversion to convert it to the null value for pointers, pointers to functions, delegates, etc." という文言が確認できます. 正しい変換であるとはどういうことなんなんでしょうか. 必ずsafe? また直前のnullは別の型にキャストされる前だとtypeof(null)という型が与えられるそうですが, これはpointer typeなのでしょうか, それともnon-pointer typeなのでしょうか. 少なくとも現在の仕様の状態から判別することは難しいと思います…

というわけで最初の問題の結論は, 「現時点(2019.02.07)での仕様ではどちらともとれる」でした(これはひどい)

scope parameter storage class と in parameter storage class

(2019/02/07; いくつか追記したのでそちらのほうもみてください)

現在の最新版コンパイラ(DMD 2.084.0)だと, このコードはSEGVで落ちてしまう.

auto foo(scope void delegate() @safe dg) @safe
{
    return dg;
}

auto bar(void delegate() @safe dg) @safe
{
    return foo(() => dg());
}

void main()
{
    bar((){})();
}

これは関数 bar fooがクロージャを生成しないためである. (scope parameter storage classを使わない場合は callq _d_allocmemory がコード生成される, これはクロージャのためのメモリを確保するために使われる)

(追記: たしかにscope使った場合にclosureを生成しないためなんだけど、実際には開放済のはずのスタック領域に対して不正アクセスしてるのが問題なのであり、しかもbarではなくfooのほうだった…)

0000000000032edc <_D9scopesegv3fooFNfMDFNfZvZQh>:
   32edc:       55                      push   %rbp
   32edd:       48 8b ec                mov    %rsp,%rbp
   32ee0:       48 83 ec 10             sub    $0x10,%rsp
   32ee4:       48 89 7d f0             mov    %rdi,-0x10(%rbp)
   32ee8:       48 89 75 f8             mov    %rsi,-0x8(%rbp)
   32eec:       48 8b 55 f8             mov    -0x8(%rbp),%rdx
   32ef0:       48 8b 45 f0             mov    -0x10(%rbp),%rax
   32ef4:       c9                      leaveq 
   32ef5:       c3                      retq

正直バグなのかどうか確定的に言えないが, 関数の定義において scope parameter sotrage class は ~ cannot be escaped といっているのでこれは合法的な振る舞いだと思われる. (追記: 触っちゃいけないスタック領域さわってるからアウトだな…)

この挙動がバグであるかはおいておいて, scope parameter storage classと似たようなものとして in parameter storage classがある. ただしここで scope のかわりに in を使った場合, クロージャを生成するコードがはかれてプログラムは正常終了する.

https://dlang.org/spec/function.html#param-storage によると in parameter storage class は defined as scope const. However in has not yet been properly implemented so it's current implementation is equivelent to const. ~ とある. この説明を読むと in は scope const と定義されているけど, 現状では正しく実装されていなくて const と同じ扱いだよ, なるべく使わないでかわりに scope constconst を使ってね ということだそうだ.

というわけだが, いくつか疑問なところがでてくる.

  • inの現在の実装は正しくなくてconstと同じ実装だよ, と仕様に書かれている場合に段階的にscope constと同じようにコンパイラ内部の実装を変更するべきだろうか
    • constともscope constとも異なる実装になる? stableでおきなかったらいいか
    • ユーザは仕様の注釈をみて const と同じ振る舞いを期待してコードを書いているかもしれない, さすがに無視してよさそうだが...
  • さらに上の問題のように, in parameter storage classの実装をscope constに近づけたことによっておきる問題が発生した場合, どのように対応すべきだろうか
    • 仮に今回のscope parameter storage classの挙動(SEGV)が正しい場合があるとして, ユーザがコードを変更してないのにコンパイラのバージョンをあげたらSEGVる, とか.
    • そもそもコンパイラのBug Fixなので正しいし, ユーザが in parameter storage class の使い方を間違えているケースと考えてることはできる.
    • とはいえわりと使われていそうなので, かなり影響がでそうだし慎重にやるべきだろうな.

とりあえず現時点で in paramter storage class は使わないほうがよさそうだ.

DMDでprofileオプションを使ったときだけスタックオーバーフローする不思議なバグの調査

なんかおもしろいコンパイラのバグに遭遇したのでめも。

もともとはpeggedを使ったプロジェクトで dub build --build=profile でビルドしたバイナリを実行するとSEGVに遭遇するんだけど・・・、という問題だった。

とりあえずgdbにかけてみると

>>> bt 10
#0  0x0000555555928ecc in trace_pro ()
#1  0x000055555580cbb8 in _D4core8internal6string__T7dstrcmpZQjFNaNbNiNeMxAaMxQeZi ()
#2  0x0000555555955b62 in trace_addsym ()
#3  0x0000555555928fee in trace_pro ()
#4  0x000055555580cbb8 in _D4core8internal6string__T7dstrcmpZQjFNaNbNiNeMxAaMxQeZi ()
#5  0x0000555555955b62 in trace_addsym ()
#6  0x0000555555928fee in trace_pro ()
#7  0x000055555580cbb8 in _D4core8internal6string__T7dstrcmpZQjFNaNbNiNeMxAaMxQeZi ()
#8  0x0000555555955b62 in trace_addsym ()
#9  0x0000555555928fee in trace_pro ()
(More stack frames follow...)

という感じでどうも相互関数呼び出しによるstack overflowをしている。 そのあとごにょごにょ回り道をしたあげく、DWARFの吐いている core.internal.string.dstrcmp のソースでは存在していないけれど、disassemble では callq trace_pro するようになっていてなんだこれ?ということでDMDのソースをみることに。 すると src/dmd/glue.d に以下のようなコードとコメントが。

        /* Doing this in semantic3() caused all kinds of problems:
         * 1. couldn't reliably get the final mangling of the function name due to fwd refs
         * 2. impact on function inlining
         * 3. what to do when writing out .di files, or other pretty printing
         */
        if (global.params.trace && !fd.isCMain() && !fd.naked)
        {
            /* The profiler requires TLS, and TLS may not be set up yet when C main()
             * gets control (i.e. OSX), leading to a crash.
             */
            /* Wrap the entire function body in:
             *   trace_pro("funcname");
             *   try
             *     body;
             *   finally
             *     _c_trace_epi();
             */
            StringExp se = StringExp.create(Loc.initial, s.Sident.ptr);
            se.type = Type.tstring;
            se.type = se.type.typeSemantic(Loc.initial, null);
            Expressions *exps = new Expressions();
            exps.push(se);
            FuncDeclaration fdpro = FuncDeclaration.genCfunc(null, Type.tvoid, "trace_pro");
            Expression ec = VarExp.create(Loc.initial, fdpro);
            Expression e = CallExp.create(Loc.initial, ec, exps);
            e.type = Type.tvoid;
            Statement sp = ExpStatement.create(fd.loc, e);

つまり dstrcmp 関数がtrace_pro を先頭で呼ぶように書き換えられた結果、 trace_pro -> trace_addsym -> dstrcmp -> trace_pro -> ... のようにひたすら関数呼び出しをし続けてstack overflowになる、ということだ。

peggedを使わない場合の再現コードをなかなか用意できなかったが文字列の大小比較 ("aaa" >= "bbb") は内部で dstrcmp を呼ぶので非常に簡単な再現コードに落とし込むことができた。 つまり文字列の大小比較のあるすべてのコードは -profile オプションを用いると同様にstack oveflowになる、ということである。だれも-profileオプションを使っていないのだろうか。

今回の話の教訓としては、「コードは書いたとおりに動くというのは嘘、コンパイラは隙があればおまえのコードを勝手に書き換える」ということですね。

C言語のmainでめちゃくちゃできる、ってネタみたけど別にDでもできるよな。 main is usually a function はでないけど。

extern (C) void main() pure nothrow @nogc
{
    asm pure nothrow @nogc
    {
        naked;
        db 0x31; db 0xC0;  // xor EAX, EAX;
        db 0xFF; db 0xC0;  // inc EAX;
        db 0xC3;           // retq;
    }
}

これでGNU coreutilsのfalseコマンド的な動作になる。

64bitだとxor EAX, EAX は嘘でmov RAX, 0 のほうがいいかもしれないのでみなさんはちゃんとしてください(?)

druntimeのcore.syncについて

いろいろいけてないよな。

  • Mutexの実装がデフォルトでre-entrant
  • 条件変数およびセマフォタイムアウトつきwaitがmonotonicなclockじゃなくシステムクロック
    • なので、システムの時刻変更によって予期せぬ振る舞いが起こりえる

このあたりはC++ではあたりまえに認知されている(デフォルトのmutex実装と別にrecursive mutexを用意、タイムアウトつきの待ちではstd::chrono::steady_clock使う)のと、Rustも実装では同じようになっているのになぜDは微妙な実装になってるのだろう。

ErupteDを使ってDからVulkan触ろうと思ったけどできなかった話

最近しょうもない記事ばっかり書いてるけど、無職だとこうやってメリハリつけないとだめかなーと思っての行為なので許容して。

DでVulkanさわろうとおもうと https://github.com/ParticlePeter/ErupteD というライブラリが一番スターついてるっぽいのでこれを使うことにする。

もろもろ必要そうなやつを入れて

$ apt install libvulkan-dev vulkan-utils

git cloneしたeruptedでexampleを動かそうとしたら、無事に死んだ。

$ dun run :devices
(...)
erupted:devices 2.0.15+v1.1.91: building configuration "application"...
Linking...
Running ./erupted_devices 
object.Exception@examples/devices.d(13): VK_ERROR_INCOMPATIBLE_DRIVER
----------------
/home/kubo39/dlang/dmd-2.083.0/linux/bin64/../../src/phobos/std/exception.d:515 pure @safe void std.exception.bailOut!(Exception).bailOut(immutable(char)[], ulong, scope const(char)[]) [0x7b31f62]
/home/kubo39/dlang/dmd-2.083.0/linux/bin64/../../src/phobos/std/exception.d:436 pure @safe bool std.exception.enforce!().enforce!(bool).enforce(bool, lazy const(char)[], immutable(char)[], ulong) [0x7b31ede]
examples/devices.d:13 void devices.enforceVK(erupted.types.VkResult) [0x7b2c6c4]
examples/devices.d:34 _Dmain [0x7b2c80a]
Program exited with code 1

手元の環境だとだめそうですね。

LDC向けのSIMDライブラリを作っているけど、いろいろつらい

x86intrin

勉強がてらにLDCでx86intrinsicっぽくSIMDを書けるライブラリを書いてみている。まだまだ全然途中なので使わないでください。

github.com

_m128iとかじゃなくてそのままbyte16とかを直接 _mm_XXX の引数にとったりするようにしてるけど、将来的にはたぶん変えます。

そういう作業をしてる中でいくつか問題にあたったので、書き残しておきます。

LDCでunittestブロックとDFLAGSの指定を共存できない

そういう問題があるので dub test --compiler だと DFLAGS="-mattr=+sse4.2 つけてもsse4.2環境のテストができない。別にスクリプトを作ってそっちを実行するようにして回避。

intrinsicがundefined reference

clangやrustと同じバージョンのLLVMを使っているけどLDCだけundefined referenceといわれるintrinsicがあったりする。cvt系とか全部死亡です、ありがとうございました。

Comment out convert intrinsics since undefined reference error · kubo39/x86intrin@4c831f9 · GitHub

普通の関数がundefined reference

sse2でintrinsic使ってない関数を呼び出そうとしたらundefined referenceで無事死亡。

おわりに

LDCSIMDけっこうつらいのではないか