kubo39's blog

ただの雑記です。

DockerとD言語のイメージ

そろそろDockerをまじめにやらないといけなくなってきた。D言語は飾りです。

まずは試す

準公式?のDockerイメージがあるのでpullしてくる。

$ docker pull dlanguage/dmd
(...)

README にあるとおりに試す、ファイル名だけかえて hello.d というファイルを作成しておく。 Dockerfileの内容をみるとベースはUbuntu16.04だった。

コマンドを試すと、あっさりと動いた。

$ docker run --rm -ti -v $(pwd):/src dlanguage/dmd dmd -run hello.d
Hello

イメージのサイズをみると614MBほど。

$ docker images| grep dlang
dlanguage/dmd       latest              51a13a70578f        3 weeks ago         614MB

生成した成果物をローカルに残したかったら -w オプションを使う。 ここでは生成したバイナリはrootユーザ権限で作成されている、カレントのユーザに変更したければ --user $(id -u):$(id -g) をつける。

$ docker run --rm -ti -v $(pwd):/src -w /src dlanguage/dmd dmd -c hello.d
$ ls -a
./  ../  hello.d  hello.o

dubを使ったビルドもできる。

$ dub init -n dockertest && cd $_
(...)
$ docker run --rm -ti -v $(pwd):/src -w /src dlanguage/dmd dub build --build=release
Performing "release" build using dmd for x86_64.
dockertest ~master: target for configuration "application" is up to date.
To force a rebuild of up-to-date targets, run again with --force.
$ ls
dockertest*  dub.json  source/
$ ./dockertest
Edit source/app.d to start your project.

ひとこと

Dockerに対してまだどういう使い方するのかいいのかイメージが沸いてない部分がある。(Dockerだけに、ね)

DCDのめも

多くの言語と同じようにD言語にもコード補完ツールがあり、DCDという。

コード補完ツールは基本の仕組みはほぼほぼ同じなのだが、それなりに個性があったりする。

例えばコンパイラがライブラリとしてパーサを提供していてそれを使っているもの(RustのracerやGoのgocode、Nimのnimsuggest)、別にあるパーサライブラリを使っているもの(Pythonのjediが使っているparso)といった違いがある。DCDは後者に属しており、dsymbol/dparseというライブラリを使っている。このあたりのライブラリをまとめて作っているのがhackerPilot氏である。

DCDの特徴として

  • クライアント-サーバモデル、クライアントがコマンドを投げてサーバが結果を返す仕組み
  • 通信はUNIXドメインソケット通信(posix)かTCPソケット通信(Windows)
  • 通信のメッセージはMessagePackにより圧縮されている
  • 補完以外にもddocの情報をとってこれるようになっている
  • 補完候補を探す対象モジュールをパスを追加することで増やすことができる(減らすことは現状できないようだ)
  • サーバ側ではidentifierやモジュール名をキャッシュする仕組みがある

などが挙げられる。

あとDCDは機能的には結構シンプルで、racerやnimsuggestのようなgoto-definitionの機能はない。tagsでいいじゃんということだろうか。

※追記

company-dcdはgoto-definitionの機能を持っていて、どうやってるのだろうと思ったが dcd-clinet --symbolLocationでうまいこととれるようだ。getSymbolsByTokenChainあたりを読めばわかりそうだがめんどくさくなったので打ち止め。

Dで生成したバイナリ自身のシンボルをみる

完全ではないが、モジュール情報や関数名や型情報などは .symtab をみればとれる。もう少しまじめにsections_elf_shared.dを読めば意味のあるものになるかもしれません。

import core.demangle;

import std.algorithm : map;
import std.file : thisExePath;
import std.format : format;
import std.range : walkLength;
import std.stdio;

import elf;


void main()
{
    auto bin = thisExePath;
    ELF elf = ELF.fromFile(bin);

    ELFSection s = elf.getSection(".symtab");
    writeln("  Symbol table .dynsym contains: ", SymbolTable(s).symbols().walkLength());
    writefln("%-(    %s\n%)", SymbolTable(s)
             .symbols()
             .map!(s => "%s\t%s\t%s".format(s.binding, s.type,
                                            demangle(s.name))));
}

今回はelf-dというライブラリを使用している。これはELFフォーマットのパーサ+一部DWARF(.debug_abbrevとか.debug_lineとか)あたりをサポートしている、Dでは貴重な静的ライブラリを扱うためのライブラリなんですがDWARF対応が中途半端なので本格的にごにょごにょやろうと思うと.debug_infoというかcompilation unitsを自前でがんばる、みたいになるのでかなりめんどくさい。Rustはgoblin/gimli-rsあってこのへんやるのに向いてるような気がする。Sentryとかでも利用実績あるしなあ。。

D言語の数値まわりの字句解析のはなし

仕様はここにある https://dlang.org/spec/lex.html#Integer

これをみると、 0b_ はBinaryIntegerとして表現可能なリテラルとなる。

import std.stdio;

void main()
{
    writeln("0b_: ", 0b_);
    writeln("0b________: ", 0b________); // _ がいくつあってもよい
}

結果は _ が何個あっても0になる。

(dmd-2.076.1)$ rdmd bin.d
0b_: 0
0b________: 0

しかし、 BinaryIntegerは 0b だけでも評価できてしまう。そのためこんなコードもコンパイルが通ってしまう。

import std.stdio;

void main()
{
    writeln("0b: ", 0b);
    writeln("0buL: ", 0buL);
}

結果はやはり0となる。

(dmd-2.076.1)$ rdmd bin.d
0b: 0
0buL: 0

また、 0x_HexadecimalInteger として表現できないリテラルであるが、実際にはコンパイルできてしまう。

import std.stdio;

void main()
{
    writeln("0x_: ", 0x_);
}

0b_ 同様、結果は値0となる。

(dmd-2.076.1)$ rdmd hex.d
0x_: 0

こちらも 0x0xuL のようなコードが実行できる。

import std.stdio;

void main()
{
    writeln("0x: ", 0x);
    writeln("0xuL: ", 0xuL);
}

結果:

(dmd-2.076.1)$ rdmd hex.d
0x: 0
0xuL: 0

社内勉強会でD言語とマイコンのはなしを発表した

スライド

Q&A

  • 組込みでDを使うメリットがあまりなかった、Rustのほうが筋がいいのでは?
    • たしかにそうなんですよね
  • Cのライブラリを使わなかったのはなぜ?
    • プリプロセッサマクロがふんだんに使われているので、どうせなら全部自前で実装したほうがはやかった

雑談

ちょっと話をどうふるべきか迷った感がある。メモリ安全も例外もラインタイムも標準ライブラリもない状態なのでエレガントにコードが書けるよ、みたいなのに全般的にふるべきだった気が。やはり文字列ミックスインは強力。

GC

そもそもGCは組み込み向きではないので、どうしたものかな。組込み向けのGCを作ればいいんじゃないか、という話があるがなんにせよ優先度は下がりそう。

例外

クラスベースの例外システムはほんとうに欲しいものなんだろうか。 それと例外の実装そのまま流用するのはdruntimeのオレオレ実装にしろucontext(3)にしろsetjmp(2)/longjmp(2)にしろaltstackを用意して、という話なので組込みと相性悪いんじゃないかと思っている。 assertくらいはほしくて、ユーザ定義な例外ハンドラに飛ばせるように、というイメージ。

ランタイム

GCも例外もランタイムなのだが。。まあdruntimeをそのままもってくるのはできないという話。少なくとも__tls_getaddr相当のものを実装する必要がある。libcに依存してしまえばいいのでは、というのもあるがその場合でもdruntime側にそうとう手を入れる必要があるので、なんともなあ。。

標準ライブラリ

標準ライブラリはアーキテクチャサポートの問題、libc依存の問題、バイナリサイズの問題、だいたいの実装でヒープが必要(これは実装すればいいだけ)な問題がある。全部サポートする気はないけどstd.mathはほしい。

emacsのD言語設定をひさしぶりにいじった

emacs 25.1.1でd-modeを使おうとするとなんかエラーが出てたので今までjava-modeを使っていたが、d-mode.elをM-x byte-recompile-fileすると動くようになったので、ついでにD言語設定をえいやっとやってしまった。 大した設定はしていなくて、

  • auto-completeベースのac-dcからcompany-modeベースのcompany-dcdへ移行した
  • DCDにパスを通して補完は以前と同じようにやってる

といった具合。

(require 'd-mode)

(setq auto-mode-alist (cons '("\\.d$" . d-mode) auto-mode-alist))
(setq load-path (cons "~/DCD/bin" load-path)) ;;;   DCDに load-path を通す

(require 'company-dcd)
(add-hook 'd-mode-hook
          (lambda ()
            (c-set-style "bsd")
            (setq c-basic-offset 4)
            (setq indent-tabs-mode nil)
            (setq tab-width 4)))

(provide 'init-d)

LDCのsanitizeオプション

LDCはバックエンドがLLVMであるので、sanitizerの機能が一部使える。 一応 address, fuzzer, memory, thread とあるけれど、手元のLDC 1.4.0でまともに使うことができたのはThreadSanitizerのみであった。

AddressSanitizer

asan/outofbounds.d

void main()
{
    auto arr = [0, 1, 2, 3];
    auto y = arr.ptr + 4;
}

これLeakSanitizerでひっかかってるけど、どうなんだ。

(ldc-1.4.0)$ ( cd asan && ldc2 -fsanitize=address -boundscheck=off outofbounds.d && ./outofbounds )

=================================================================
==7758==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 72 byte(s) in 1 object(s) allocated from:
    #0 0x55ecd0e3f5b0  (/home/kubo39/dev/dlang/ldc-san/asan/outofbounds+0xb75b0)
    #1 0x55ecd0e87ddc  (/home/kubo39/dev/dlang/ldc-san/asan/outofbounds+0xffddc)

Direct leak of 16 byte(s) in 1 object(s) allocated from:
    #0 0x55ecd0e3f3b8  (/home/kubo39/dev/dlang/ldc-san/asan/outofbounds+0xb73b8)
    #1 0x55ecd0e8876a  (/home/kubo39/dev/dlang/ldc-san/asan/outofbounds+0x10076a)
    #2 0x55ecd0d9b5e4  (/home/kubo39/dev/dlang/ldc-san/asan/outofbounds+0x135e4)

SUMMARY: AddressSanitizer: 88 byte(s) leaked in 2 allocation(s).

どうやらGCが入るとそもそもLeakSanitizerでひっかかるのでだめらしい。

void main()
{
}
(ldc-1.4.0)$ ( cd asan && ldc2 -fsanitize=address emptymain.d && ./emptymain )

=================================================================
==19717==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 72 byte(s) in 1 object(s) allocated from:
    #0 0x55934f6ffd90  (/home/kubo39/dev/dlang/ldc-san/asan/emptymain+0xb2d90)
    #1 0x55934f74cafc  (/home/kubo39/dev/dlang/ldc-san/asan/emptymain+0xffafc)

Direct leak of 16 byte(s) in 1 object(s) allocated from:
    #0 0x55934f6ffb98  (/home/kubo39/dev/dlang/ldc-san/asan/emptymain+0xb2b98)
    #1 0x55934f74d48a  (/home/kubo39/dev/dlang/ldc-san/asan/emptymain+0x10048a)
    #2 0x55934f660304  (/home/kubo39/dev/dlang/ldc-san/asan/emptymain+0x13304)

SUMMARY: AddressSanitizer: 88 byte(s) leaked in 2 allocation(s).

それでは、とGCを外してみるとAddressSantizerはちゃんと動いていないように見受けられる。

extern(C) void main()
{
    int[4] arr = [0, 1, 2, 3];
    auto y = arr.ptr + 10;
}
(ldc-1.4.0)$ ( cd asan && ldc2 -fsanitize=address outofbounds2.d && ./outofbounds2 )
(ldc-1.4.0)$

結論からいうと、-fsanitize=address はLeakSanitizerを有効にし、GCを起動するようなプログラムの場合は必ずリーク検出が発生してしまうようだ。

ちなみに -sanitize=memory の結果は以下のようになった。

(ldc-1.4.0)$ ( cd asan && ldc2 -fsanitize=memory emptymain.d && ./emptymain )
gcc: error: unrecognized argument to -fsanitize= option: ‘memory’
Error: /usr/bin/gcc failed with status: 1

ThreadSanitizer

tsan/datarace.d

import core.thread;

__gshared static int ANSWER = 32;

void f()
{
    ANSWER = 42;
}

void main()
{
    auto t = new Thread(&f).start();
    ANSWER = 24;
    t.join();
}

表示は質素だけど、競合条件を判定できている。

(ldc-1.4.0)$ ( cd tsan && ldc2 -fsanitize=thread datarace.d && ./datarace )
FATAL: ThreadSanitizer: unexpected memory mapping 0x56288abe3000-0x56288ac1b000

追記

いろいろ追試したところ、ThreadSanitizerも信頼できない。

(ldc-1.4.0)$ ldc2 -fsanitize=thread emptymain.d
(ldc-1.4.0)$ ./emptymain
FATAL: ThreadSanitizer: unexpected memory mapping 0x562d389a0000-0x562d389d7000

AddressSanitizerに至っては、いつのまにかリンカエラーでこけるようになっている。

(ldc-1.4.0)$ ( cd asan && ldc2 -fsanitize=address emptymain.d && ./emptymain )
/usr/bin/ld: /home/kubo39/dlang/ldc-1.4.0/lib/libldc_rt.asan-x86_64.a(asan_allocator.cc.o): 誤った再配置シンボル索引 (0x1fd8ca57 >= 0x11f) (オフセット 0x1fd8000000027079、セクション `.debug_loc' 内用) です
/usr/bin/ld: /home/kubo39/dlang/ldc-1.4.0/lib/libldc_rt.asan-x86_64.a(asan_allocator.cc.o): 誤った再配置シンボル索引 (0x1fd8ca57 >= 0x11f) (オフセット 0x1fd8000000027079、セクション `.debug_loc' 内用) です
/usr/bin/ld: 最終リンクに失敗しました: 不正な値です
collect2: error: ld returned 1 exit status
Error: /usr/bin/gcc failed with status: 1

GNU ld側の問題な気がするが、追求するのがおっくうになっている。どうせ現時点では使い物にならないだろう。

(ldc-1.4.0)$ ( LANG=C; cd asan && LD_DEBUG=reloc ldc2 -fsanitize=address emptymain.d && ./emptymain )
...
     22218:     relocation processing: /usr/lib/gcc/x86_64-linux-gnu/6/liblto_plugin.so
     22218:
     22218:     calling init: /usr/lib/gcc/x86_64-linux-gnu/6/liblto_plugin.so     
     22218:                                                                  
/usr/bin/ld: /home/kubo39/dlang/ldc-1.4.0/lib/libldc_rt.asan-x86_64.a(asan_allocator.cc.o): bad reloc symbol index (0x1fd8ca57 >= 0x11f)
 for offset 0x1fd8000000027079 in section `.debug_loc'                       
/usr/bin/ld: /home/kubo39/dlang/ldc-1.4.0/lib/libldc_rt.asan-x86_64.a(asan_allocator.cc.o): bad reloc symbol index (0x1fd8ca57 >= 0x11f)
 for offset 0x1fd8000000027079 in section `.debug_loc'                        
/usr/bin/ld: final link failed: Bad value                 
     22218:                                                                                  
     22218:     calling fini: /usr/lib/gcc/x86_64-linux-gnu/6/liblto_plugin.so [0]
     22218:                                       
...