Edited at

インラインアセンブラによるD言語くん


概要

2018/12/21(旧暦)の枠が空いてたのでインラインアセンブラを使ってD言語くんを表示させてみました。

ランタイムライブラリは用いず、システムコールのみを用いた小さな静的バイナリを生成します。

使用するコンパイラによって利用可能な構文が変わります。


検証環境

Archlinuxを利用し、公式リポジトリから入手可能な執筆時最新の各D言語コンパイラで確認しました。


  • DMD(v2.084.0)

DMD64 D Compiler v2.084.0


  • LDC(1.13.0)

LDC - the LLVM D compiler (1.13.0):

based on DMD v2.083.1 and LLVM 7.0.1
built with LDC - the LLVM D compiler (1.12.0)


  • GDC(8.2.1)

gdc (GDC 8.2.1 based on D v2.068.2 built with ISL 0.20 for Arch Linux) 8.2.1 20180831


その1: D言語標準の構文(DMD, LDC)

https://dlang.org/spec/iasm.html

D言語には標準のインラインアセンブラの構文があります。

i386やx86_64限定ですがDMDおよびLDCで使用可能です。

$ cat iasm_dman_std.d

extern(C) void _start(){
immutable s = "< >\n ..\n D\n< >\n";
auto _ptr = s.ptr;
asm{
mov RDX, s.length;
// mov RSI, s.ptr;
// DMD(v2.084.0): "Error: identifier expected"
// LDC(1.13.0): "Error: invalid operand" (2 times)
mov RSI, _ptr;
mov RDI, 1;
mov RAX, 1;
syscall;
mov RDI, 0;
mov RAX, 60;
syscall;
}
}
$ dmd -ofiasm_dman_dmd.o iasm_dman_std.d -c -betterC && ld -o iasm_dman_dmd{,.o}
$ ldc -ofiasm_dman_ldc.o iasm_dman_std.d -c -betterC && ld -o iasm_dman_ldc{,.o}
$ ./iasm_dman_dmd # または ./iasm_dman_ldc
< >
..
D
< >

変数名をアセンブラ上に直接かけるのは便利ですね。

ただasm{ ... }内でmov RSI, s.ptr;という表記をすることができなかったのでダミーの変数を噛ませる必要がありました。

mov RDX, s.length;は問題なくできたのでバグでしょうか、謎です。


その2: GDCの独自構文(GDC)

https://wiki.dlang.org/Using_GDC#Extended_Assembler

GDCは現在のところD言語標準の構文を利用できませんが、

GCC拡張と同様のインラインアセンブラを使用できます。

こちらはアーキテクチャに依存しないで使用できるようです。

$ cat iasm_dman_gdc.d

extern(C) void _d_dso_registry(){};
extern(C) void __stack_chk_fail(){};
extern(C) void _start(){
immutable s = "< >\n ..\n D\n< >\n";
asm{`
mov %1, %%rdx
mov %0, %%rsi
mov $1, %%rdi
mov $1, %%rax
syscall
mov $0, %%rdi
mov $60, %%rax
syscall
`: : `r`(s.ptr), `r`(s.length);
}
}
$ gdc iasm_dman_gdc.d -c && ld -o iasm_dman_gdc{,.o}
$ ./iasm_dman_gdc
< >
..
D
< >

GCCではasm( ... )または__asm__( ... )を用いますが

GDCではasm{ ... }の構文となり、

セミコロンで区切れば一つのasmブロックに複数の文を入れることができます。

また、_d_dso_registryおよび__stack_chk_failをどこかに定義しておかないとコンパイルが通りませんでした。

asmブロック内にラベルの形(_d_dso_registry:)で置くだけでも大丈夫でした。

GDCで変数を扱うにはConstraints(制約)に関する知識が必要となります。

こちらは深追いしていないので詳細を省きますが、asm文の外とデータのやり取りをする際に使用する

レジスタの指定を行う必要があるようです。

http://msyksphinz.hatenablog.com/entry/2016/03/28/020000

http://caspar.hazymoon.jp/OpenBSD/annex/gcc_inline_asm.html

変数の出る順番で%序数を指定します。

そのためレジスタは%%raxのように%を二つ書く必要があります。

用途があるか微妙ですが、Constraintsなしの場合はレジスタへの%は一つですみます

...

asm{`
mov $0, %rdi
mov $60, %rax
syscall
`;
}
...

なおGCCと同じくIntel構文の利用も可能です。

筆者はこっちのほうがややこしくないので好きです。

...

asm{`
.intel_syntax noprefix
mov rdx, %1
mov rsi, %0
mov rdi, 1
mov rax, 1
syscall
mov rdi, 0
mov rax, 60
syscall
.att_syntax
`: : `r`(s.ptr), `r`(s.length);
}
...

ちなみに-betterCについてはDMDおよびLDCではつけておかないとundefined referenceが出ちゃいますが

GDC(gdcやgdmdでいろいろ試した)ではうまく機能していないように見えました。


その3: LDCの独自構文(LDC)

https://wiki.dlang.org/LDC_inline_assembly_expressions

LDCは標準のインラインアセンブラに加えて、

アーキテクチャに依存しない独自の文法も扱えます。

import ldc.llvmasm:__asm;

extern(C) void _start(){
immutable s = "< >\n ..\n D\n< >\n";
__asm!()(`
mov $1, %rdx
mov $0, %rsi
mov $$1, %rdi
mov $$1, %rax
syscall
mov $$0, %rdi
mov $$60, %rax
syscall
`, `r,r`, s.ptr, s.length);
}

標準の構文やGDCの構文と違って、テンプレートによる実装がなされています。

そのためimportを使う必要があります。

LDCの構文で変数を扱う場合もまたConstraintsへの知識が必要となります。

こちらはConstraintsなしでの記載はできませんでした。(テンプレートがエラーになる)

GDCと違って変数の出る順番で$序数を指定するため即値の$を二重にしてやる必要があります。ややこしいですね。

同じくIntel構文が使えるかと思い、試しましたがエラーになりました。

変数をレジスタに書き換えるときにAT&T構文で使う%が紛れ込んでしまうようです。

$ cat iasm_dman_ldc_intel.d 

import ldc.llvmasm:__asm;
extern(C) void _start(){
immutable s = "< >\n ..\n D\n< >\n";
__asm!()(`
.intel_syntax noprefix
mov rdx, $1
mov rsi, $0
mov rdi, 1
mov rax, 1
syscall
mov rdi, 0
mov rax, 60
syscall
.att_syntax
`, `r,r`, s.ptr, s.length);
}
$ ldc iasm_dman_ldc_intel.d -c -betterC
<inline asm>:3:18: error: unknown token in expression
mov rdx, %rdx
^
<inline asm>:4:18: error: unknown token in expression
mov rsi, %rax
^
LLVM ERROR: Error parsing inline asm


まとめ

構文の差こそあれ、既存の三大D言語コンパイラすべてで

インラインアセンブラによるD言語くんの表示ができることを確認できました。

ただしそれぞれのコンパイラにはクセがあり、用途や望みの挙動によって使い分ける必要がありそうです。


  • DMD: フロントエンドのアップデードがほか二つより早い

  • LDC: インラインアセンブラに限ればDMDの上位互換か

  • GDC: 機能としてはIntel構文が使える等有利だが余計な宣言が必要かつ少し生成バイナリがでかい

$ stat -c '%n %s' iasm_dman_{dmd,ldc,gdc}

iasm_dman_dmd 8920
iasm_dman_ldc 9144
iasm_dman_gdc 14376
(objdump等してみましたが、D言語くんから離れてしまいそうだったのでサイズについてはこの辺で)


おまけ

JavaがなくてもAndroid(で動作する)アプリ(ケーション)を開発できる。そう、D言語ならね。

$ cat iasm_dman_ldc_aarch64.d

import ldc.llvmasm;
extern(C) void _start(){
immutable s = "< >\n ..\n D\n< >\n";
__asm!()(`
mov x2, $1
mov x1, $0
mov x0, #1
mov x8, #64
svc #0
mov x0, #0
mov x8, #93
svc #0
`, `r,r`, s.ptr, s.length);
}
$ ldc iasm_dman_ldc_aarch64.d -c -betterC -mtriple=aarch64-linux-gnu && aarch64-linux-gnu-ld -o iasm_dman_ldc_aarch64{,.o}
$ adb push iasm_dman_ldc_aarch64 〜適当な場所〜