kubo39's blog

ただの雑記です。

D言語でRISC-Vシミュレータ上でハローワールドする

D言語でもRISC-Vやりたいんだが…? ← できた。そういう話です。

準備

LDCのバージョンは以下のもの。自前ビルドしてる人はRISC-V向けにしてることと、RISC-VはLLVM 9.0.0以降で公式にサポートされていることに注意されたい。

$ ldc2 --version | head -2
LDC - the LLVM D compiler (1.21.0):
  based on DMD v2.091.1 and LLVM 10.0.0
$ ldc2 --version | grep RISC-V
    riscv32    - 32-bit RISC-V
    riscv64    - 64-bit RISC-V

RISC-VのGCCツールチェーンをダウンロードしてPATHの通るところにおく必要がある。 ここではてきとうにgccというディレクトリを作ってそこに展開してる。 バージョンはちょっと古いものを使って試したが、もっと新しいやつもってきても大丈夫だと思われる。

$ mkdir -p gcc
$ curl -L https://static.dev.sifive.com/dev-tools/riscv64-unknown-elf-gcc-8.1.0-2018.12.0-x86_64-linux-ubuntu14.tar.gz | tar --strip-components=1 -C gcc -xz

シミュレータ環境としてこのへんを用意しておく。

やる

ソースコードはこんな感じだよ。

extern (C):

// version(CRuntime_Musl) とか定義してもいい。
int printf(scope const char* format, scope const ...);

int main()
{
    printf("Hello, World!\n");
    return 0;
}

コンパイルオプションはRISC-V 64向けだと以下のようにする。 -mattrの指定は整数乗除算(+m),アトミック命令(+a),16bit圧縮命令(+c)を指定。というかこれ以外だとgccのリンクの時にこけるんだよな。。新しいので試したほうがいいかもしれない。 あとLLVM 10だとCPUタイプをgeneric-rv64指定したときにrvcヒント命令(+rvc-hints)もついてくるけど、今回だと多分関係ない(はず)。

$ ldc2 --mtriple=riscv64-unknown-none-elf -mcpu=generic-rv64 \
  -mattr=+m,+a,+c -betterC -c hello.d
$ riscv64-unknown-elf-gcc -march=rv64imac -mabi=lp64 -o hello hello.o

シミュレータ上で動かす。

$ spike pk hello
bbl loader
Hello, World!
$

おまけ(読まなくてもいいやつ)

-mcpu=generic-rv64 指定ないとgenericというRISC-Vターゲットには存在しないCPUタイプ指定してくるのでこれが必須。upstreamのLDCではすでに修正されている。 あとLDCは--gcc指定でリンク処理するCコンパイラ指定できるんだけど、ArmとMIPS以外の環境だと勝手に-m32/-m64を付けてくるので上記のようにリンクは別コマンドでやる必要がある。 これもパッチはもうできててたぶんそのうち取り込まれる。

一応RISC-V 32版でのコンパイル方法ものせておくけど、spikeがバグってんのか--isa指定いろいろ試してもだめだった。(ここはほんとに読まなくていいとこ)

$ ldc2 --mtriple=riscv32-unknown-none-elf -mcpu=generic-rv32 \
  -mattr=+m,+a,+c -betterC -c hello.d
$ riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -o hello hello.o

D言語もくもく会第17回

(追記: なんやかんやでWASI Tutorialまでできるようになった: https://github.com/kubo39/ldc-wasi-tutorial )

はじめて記事書くな、17です(威圧)。

これにだいたい参加しています。

雑にWASIをwasmtimeで動かせないかな、と試すやつをやりました。

まずWASI対応してるっぽいLDCのビルドからやる。skoppe氏がいろいろやっているはずなのでそこから持ってくる。

$ git submodule add -b wasm https://github.com/skoppe/ldc
$ cd ldc
$ git rev-parse HEAD
828926064c52eba905d9bbbf9d4d57f64a2cd267
$ git submodule init
$ git submodule update --recursive
$ cd ..
$ mkdir build-ldc && cd build-ldc
$ cmake -G Ninja ../ldc \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX=$PWD/../install-ldc
$ ninja -j$(nproc)

動かすにはCのランタイムとか必要なので、wasi-sdkをダウンロードする。ここではバージョン10をダウンロードしている。

$ wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-10/wasi-sdk-10.0-linux.tar.gz

WASIでコンパイル、やってみた!(プリチャン風)

import core.stdc.stdio;

extern(C) int main()
{
    printf("Hello, WASI!\n");
    return 0;
}

コンパイルオプションは勘ですがたぶんこんな感じになるはず。

$ ./build-ldc/bin/ldc2 -mtriple=wasm32-unknown-wasi -betterC --gcc=./wasi-sdk-10.0/bin/clang --linker=./wasi-sdk-10.0/bin/wasm-ld main.d
emit _start
/home/kubo39/dev/dlang/ldc-wasi-tutorial/ldc/runtime/druntime/src/core/internal/entrypoint.d(32): Error: only one `main` function allowed

はいエントリポイントまわりがいい感じになっていないぽいですね。

しゃあないのでパッチ書きます。

$ git diff
diff --git a/src/core/internal/entrypoint.d b/src/core/internal/entrypoint.d
index eb00b156..be3b204c 100644
--- a/src/core/internal/entrypoint.d
+++ b/src/core/internal/entrypoint.d
@@ -45,7 +45,8 @@ template _d_cmain()
                 return main(argc, argv);
             }
         }
-        version (WebAssembly)
+        version (WASI) {}
+        else version (WebAssembly)
           {
             pragma(msg, "emit _start");
             import ldc.attributes;

で、まあこのままだといろいろシンボル定義違うとか_startないよとかprintfないよとか怒られるのでコードも修正します。

import core.stdc.stdio;

extern(C):

void __prepare_for_exit() {}
void __wasi_proc_exit(int i) {}

pragma(mangle, "__original_main")
extern(C) int main()
{
    printf("Hello, WASI!\n");
    return 0;
}

動くとこまでいきました。やったぜ。

$ ./build-ldc/bin/ldc2 -mtriple=wasm32-unknown-wasi -betterC -L./wasi-sdk-10.0/share/wasi-sysroot/lib/wasm32-wasi/crt1.o -L./wasi-sdk-10.0/share/wasi-sysroot/lib/wasm32-wasi/libc.a --linker=./wasi-sdk-10.0/bin/wasm-ld main.d
$ wasmtime main.wasm
Hello, WASI!

partialRELRO/fullRELRO、あるいは-fno-pltの話

relroはすでにけっこう有名だけど、はわりとこのへんの話は知られていない気がしてきた。

relro

relro、すなわちrelocation read onlyは再配置情報を格納している領域を読み取り専用にして実行時に書き換えられないようにすることだ。

そもそも再配置(リロケーション)とはなにかというと、再配置情報に応じて適切なリンク後(リンカがアドレス解決した後)の値をオブジェクトファイルにいれる処理を指す。 紛らわしいね。

(TODO: ここでリロケーションの説明をしたい、、いや読者への課題にするか??) リロケーションの説明はこの資料がめちゃめちゃ詳しい。

再配置情報を格納している領域は .plt とか .got とかがそれにあたる。 ようはこのへんを読み取り専用にするというオプションであると覚えておけばいい。

ではなぜここを読み取り専用にする必要があるかというと、セキュリティ上の問題があるためだ。 バッファオーバーフローと並びよく知られている攻撃手法としてformat string attack(書式文字列攻撃)というものがある。 これはもとはメモリ内容の読み出しができるというものだったが、任意の位置のメモリ内容を書き換えてしまうことも可能である(GOT overwrite攻撃)。 ここで再配置領域は書き換え可能領域としてよく狙われてしまうのでGCCには -Wl,-z,relro を渡すことでリロケーションを読み取り専用にするオプションは追加される運びとなった。

lazy binding

上でrelroについて書いたが、実はこれは完全ではない。relroについてちゃんと知るためにはlazy bindingについて知る必要がある。

lazy bindingが使われるとき、動的リンカ(あるいはローダ)はシンボル名の検索をロード時まで遅延させ、関数呼び出しが行われるまでシンボル名の検索およびアドレス解決(リンカによるものではなくディスパッチ呼び出しのことが言いたい、、通じるか、、?)が遅延される。 これはいろいろ遅延読み込みにすることで初期のメモリ読み込みの時間をカットしてプログラムの起動時間を節約するというものだが、最近だとその利点が疑わしくなっているらしい(ここはよく知らない)。

で、現代においてはlazy bindingはセキュリティの足かせになっている。なぜならこの機構を機能させるためには仕組み上上で説明した再配置領域であるGOTが書き込み可能になっている必要があるためだ。正確にいうと、lazybindingを行うPLTのGOTスロットにかんしてはは書き換え可能になっていなければいけない。

lazy bindingを無効にする場合はGCC-Wl,-z,now を渡す。 一般的に -Wl,-z,relro をpartial RELRO、 -Wl,-z,relro -Wl,-z,now をfull RELROと呼んでいるようだ。

PLT

PLT(Procedure Linkage Table)はLazy Bindingを行うための機構である。 PLTを使った関数呼び出しが行われる場合関数本体に飛ぶのではなくPLT上の対応するコードに飛び、そこでGOTの対応するスロットから本体の絶対アドレスを取得してそこに飛ぶようになっている(間接呼び出し)。こうすることで未解決シンボルがあってもリンクすることが可能となる。

実行バイナリはレガシーなライブラリを呼び出す可能性を考慮する必要があるのでPLTを消すことはできないが(要検証)、 少なくとも x86 および x86_64 においてPICはPLTを必要とすることはない。x86 であればGOTにラベルの絶対アドレスが入っているし(GOTへの参照はプログラムカウンタとGOTへのオフセット計算を実行前にリンカがよしなにやる)、 x86_64 はRIP相対参照でプログラムカウンタとのオフセットによって解決する(CPUがサポートをしてやってくれる)。 PICの説明はこの資料がめちゃめちゃ詳しい。

(と思ったのだが、現代のLinux環境においてはGCC hardenedの影響によってほとんどのライブラリはPICでリコンパイルされ、コンパイラ/リンカもデフォルトでPIE/PICが渡されるようになっておりPLTの存在価値はなくなってきている)

そうなるとライブラリでPLTを使う理由はlazy bindingのためだが、上で書いたようにもはやメリットも疑わしく、relroを使う現代においてはもはや時代遅れとなっている。 さきに説明したGCC hardenedの影響でコンパイラもfull RELROをデフォルトで渡すようになっている。

というようなわけでGCCやClangは -fno-plt オプションでPLTを経由するような関数を生成しないようにできる。

PLT経由しないことによるメリットにはパフォーマンスの向上もある。 GCCやRustによると、1~3%ほどのパフォーマンス向上が見込めるとのことだ。

直感的にはPLTを経由しない場合コードのキャッシュローカリティが高まるので速くなるとはわかる。

(TODO: 理由を調べて書く、、?)

LDCで実験

LDC 1.21.0で実験してみる。

実験環境はUbuntu 18.04なのでGCC hardenedの影響を受けているので生成されるバイナリは全部PIC(かつ)PIEになっている(はず)。

特別なコードを書く必要がないのでhello worldでやる。

import std.stdio;

void main()
{
    writeln("Hello, World!");
}

ldc2コマンドでコンパイルしたときにfull RELROになることが確認できる。

$ ldc2 helloworld.d
$ readelf -l helloworld | grep GNU_RELRO
  GNU_RELRO      0x000000000009bc10 0x000000000009cc10 0x000000000009cc10
$ readelf -d helloworld | grep BIND_NOW
 0x000000000000001e (FLAGS)              BIND_NOW

といってもLDC側でデフォルトで特別にリンカになにか渡しているわけではない。

$ ldc2 -v helloworld.d | tail -1
/usr/bin/cc helloworld.o -o helloworld -fuse-ld=gold -L/home/kubo39/dlang/ldc-1.21.0/bin/../lib -lphobos2-ldc -ldruntime-ldc -Wl,--gc-sections -lrt -ldl -lpthread -lm -m64

リンカの渡してるオプションをみると、デフォルトで -Z now -Z relro あたりを渡していることが確認できる。

$ ldc2 --Xcc=--verbose helloworld.d
(...)
COLLECT_GCC=/usr/bin/cc
(...)
gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
(...)
COLLECT_GCC_OPTIONS='-o' 'helloworld' '-fuse-ld=gold' '-v' '-L/home/kubo39/dlang/ldc-1.21.0/bin/../lib' '-m64' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper -plugin-opt=-fresolution=/tmp/ccT0zIvB.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -fuse-ld=gold -z relro -o helloworld /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/home/kubo39/dlang/ldc-1.21.0/bin/../lib -L/home/kubo39/dlang/ldc-1.21.0/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/home/kubo39/dlang/ldc-1.21.0/lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. helloworld.o -lphobos2-ldc -ldruntime-ldc --gc-sections -lrt -ldl -lpthread -lm -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o

つまり我々は特に意識しないでもfull RELROなバイナリを生成していたのだ。(あえて-Wl,-z,relro -Wl,-z,nowを書いている意味とは、、?まあデフォルトでない環境もまだあるし)

Gentoo WikiをみるとGCC HardenedはSSP,PIE,RELRO,BIND_NOWがby defaultだった、なのでfull RELROになるわけか。

さて、 -fno-plt はリンカ側で対応できるわけではなくコンパイラがそういうふうなコード生成をしないとだめなんだけど、LDCは意外とうまく吐いてくれるかもしれない。

extern (C) char* getenv(const(char)* name);

char* callThroughPLT()
{
    return getenv("\0".ptr);
}

たぶんLLVM IRに Function Attrs: nonlazybind がついていればいい。

確認してみよう。

$ldc2 --output-ll plt.d
(...)
; [#uses = 1]
declare i8* @getenv(i8*) #0

; [#uses = 0]
; Function Attrs: uwtable
define i8* @_D3plt14callThroughPLTFZPa() #1 comdat {
  %1 = call i8* @getenv(i8* getelementptr inbounds ([2 x i8], [2 x i8]* @.str, i32 0, i32 0)) #0 ; [#uses = 1]
  ret i8* %1
}
(...)

なかった。

ただ上で述べたようにLDCはデフォルトでfull RELROであるのでlazy bindingなコードは不要である。 生成されるバイナリもPLTは使っていない。 そんなことはなかった。

追記

せっかくなのでLDCno-pltをサポートするパッチを書いた。

Linux環境でSharedObjectsを使う

とくになにかあるわけじゃなくてSharedObjectsの使い方の備忘録として。まあこういうのドキュメントないので。。

SharedObjectsというのはLinuxでだけ使えるやつで、実行プログラムがロードしている共有ライブラリを取得するために使うものである。 ようは dl_iterate_phdr(3) といえば通じる人には通じる。

以下はプロセスがロードしている共有ライブラリの名前と各セグメントの仮想メモリ上の位置を表示するプログラム。 界隈あるあるとして(??)、relocationを考慮して実際の仮想アドレスを表示するようにしている。

// druntimeのSharedObjectsを使えるのはLinuxだけ
version(linux):

import core.stdc.stdio;

// SharedObjectsを定義している
import core.internal.elf.dl;

// プログラムヘッダの定義とかが欲しいので
import core.sys.linux.elf;

void main()
{
    foreach (object; SharedObjects)
    {
        // std.stdioはここでは使えない
        printf("%s\n", object.name.ptr);

        foreach (ref phdr; object)
        {
            string name = () {
                final switch (phdr.p_type)
                {
                case PT_NULL: return "NULL";
                case PT_LOAD: return "LOAD";
                case PT_DYNAMIC: return "DYNAMIC";
                case PT_INTERP: return "INTERP";
                case PT_NOTE: return "NOTE";
                case PT_SHLIB: return "SHLIB";
                case PT_PHDR: return "PHDR";
                case PT_TLS: return "TLS";
                case PT_NUM: return "NUM";
                case PT_LOOS: return "LOOS";
                case PT_GNU_EH_FRAME: return "GNU_EH_FRAME";
                case PT_GNU_STACK: return "GNU_STACK";
                case PT_GNU_RELRO: return "GNU_RELRO";
                }
                assert(false);
            } ();

            // dl_iterate_phdr(3) を参照
            ulong actualVMA = phdr.p_vaddr + object.info.dlpi_addr;
            printf("    %p: segment %s\n", cast(void*)actualVMA, name.ptr);
        }
    }
}

getFunctionAttributesでlive関数属性をとれるようにした

これ https://github.com/dlang/dmd/pull/11049

単にtraits.dを修正するだけではだめだった、というのはlive関数属性は名前修飾の定義も実装もされていなかったので、typeof内で普通の関数と区別されていなかったのである。

具体的に言うと以下のようになってしまう。

alias tuple(T...) = T;

struct S1
{
    int f1() @live { return 42; }
    int f2() { return 42; }
}

static assert(__traits(getFunctionAttributes, typeof(S1.f1)) == tuple!("@live", "@system"));
static assert(__traits(getFunctionAttributes, typeof(S1.f2)) == tuple!("@live", "@system"));

struct S2
{
    long f2() { return 0; }
    long f1() @live { return 42; }
}

static assert(__traits(getFunctionAttributes, typeof(S2.f1)) == tuple!("@system"));
static assert(__traits(getFunctionAttributes, typeof(S2.f2)) == tuple!("@system"));

という小ネタがおもしろかったというだけの話。

D言語のvulkan bindingであるEruptedDをいれる

以前挑戦してだめだった vulkan 利用、実はIntel HD用のパッケージを入れればいけるのでは?となったので試す。

$ sudo apt install -y mesa-vulkan-drivers

おお、どうやら動いたっぽい。

$ dub run :devices
Performing "debug" build using /home/kubo39/dlang/dmd-2.091.0/linux/bin64/dmd for x86_64.
erupted:devices 2.0.54+v1.2.134: building configuration "application"...
Linking...
Running ./erupted_devices 
Before vkEnumeratePhysicalDevices
After vkEnumeratePhysicalDevices

Found 1 physical device(s)
==========================

Physical device 0: Intel(R) HD Graphics 520 (Skylake GT2)
API Version: 1.1.102
Driver Version: 79699976
Device type: VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU

Queue Family 0
        Queues in Family         : 1
        Queue timestampValidBits : 36
        VK_QUEUE_GRAPHICS_BIT
        VK_QUEUE_COMPUTE_BIT
        VK_QUEUE_TRANSFER_BIT

VK_QUEUE_GRAPHICS_BIT found at queue family index 0

Logical device created
Graphics queue retrieved

Scope exit: draining work and destroying logical device
Scope exit: destroying instance

これからはvulkanや!

emacsのD言語環境 (2020.03.07版)

【2020.04.08追記】 dlsが非推奨になったので以下の内容はすべて古くなってしまいました。なんてこった。。

https://forum.dlang.org/post/usqhsdtwfhrterzpstst@forum.dlang.org


注意: この内容はもう古いです。

またまたemacsD言語環境をいじってLSP仕様にしました。時代はLanguage Server Protocolや!

設定は以下のような感じです。 コーディングスタイル/インデントルールはdmdやdruntimeで使われている書式にあわせています。

(use-package d-mode
  :ensure t
  :hook
  (d-mode . (lambda ()
              (c-set-style "bsd")
              (setq c-basic-offset 4)
              (setq tab-width 4)
              (lsp)))
  :commands d-mode
  )

emacs側に関してはlsp-modeuse-packageの使用を前提としています。 lsp-modeはLSPを使うので必要として(まあeglotでもいいのですが)、use-packageを使うと可読性が高い感じで設定が書けるのでよいです。

D言語のLSPサーバとしてはdlsを使っています。 LSPと通信する必要があるので dub fetch dls して dub run dls:bootstrap してできたバイナリにパスを通してやる必要があります。 好きなとこにおいてもいいのですが、dlsが最新のバイナリに追従しやすいようにdls-latestにsymlinkを作ってくれるので普通にそこにパスを通しています。

ちなみにlsp-register-clientまわりの設定は現時点でlsp-modeが公式にdlsをサポートしてくれていないので書いていますが、サポートされれば要らなくなるはずです。

dls対応のパッチがマージされたので ~/.dub/packages/.bin/dls-latest/dls にdlsを置いている場合は設定を書く必要はなくなりました。

追記(2020.3.9):

いろいろあった結果PATHで設定して通して、って感じになり、通したければユーザに設定してもらうかんじになった。 ドキュメントは https://emacs-lsp.github.io/lsp-mode/lsp-mode.html#lsp-dls