kubo39's blog

ただの雑記です。

mysql-nativeで一度に受信できるパケットサイズは16777210バイト

コードを読んでもとくにサイズ上限が規定されていなかったので実験した。

  • 環境(mysql-nativeのバージョン)
$ cat dub.selections.json| grep mysql-native
                "mysql-native": "3.0.0",

以下のようにmax_allowed_packetや受信サイズを調整して試した結果、max_allowed_packetのサイズにかかわらず16777210バイトが上限になるようだ。 というわけでいくらmax allowed packet sizeを16MBより大きくしても効果がないので注意する必要がある。

import mysql.connection;
import mysql.commands;

import std.array : array;
import std.range;

void main()
{
    auto conn = new Connection("host=127.0.0.1;port=3307;db=testdb;user=testuser;pwd=testpassword");
    scope(exit) conn.close();
    auto row = conn.queryRow("SELECT REPEAT('A', 16777210)");

    assert(row[0].get!string == 'A'.repeat.take(16_777_210).array);
    //
    // max_allowed_packet = 16MB,24MB
    //
    // OK: 15_728_640 (15MB)
    // OK: 16_777_210 (16MB - 6)
    // NG: 16_777_211
    // NG: 16_777_216 (16MB)
    //
    // 1MB = 1024 * 1024 = 1048576
    // 15MB = 15 * 1024 * 1024 = 15 * 1048576 = 15_728_640
    // 16MB = 16 * 1024 * 1024 = 16 * 1048576 = 16_777_216
}

1バイトでも超えると以下のようなエラーになる。

$ dub run
Performing "debug" build using /home/kubo39/dlang/dmd-2.094.0/linux/bin64/dmd for x86_64.
taggedalgebraic 0.11.18: target for configuration "library" is up to date.
eventcore 0.9.9: target for configuration "epoll" is up to date.
stdx-allocator 2.77.5: target for configuration "library" is up to date.
vibe-core 1.10.3: target for configuration "epoll" is up to date.
mysql-native 3.0.0: target for configuration "library" is up to date.
large-packet ~master: building configuration "application"...
Linking...
To force a rebuild of up-to-date targets, run again with --force.
Running ./large-packet
core.exception.AssertError@../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/protocol/packet_helpers.d(366): Assertion failure
----------------
??:? _d_assertp [0x56231b799d9d]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/protocol/packet_helpers.d:366 pure nothrow @nogc @safe ubyte[] mysql.protocol.packet_helpers.consume!().consume(ref ubyte[], ulong) [0x56231b6ac06d]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/protocol/packet_helpers.d:723 mysql.protocol.extra_types.SQLValue mysql.protocol.packet_helpers.consumeIfComplete!().consumeIfComplete(ref ubyte[], mysql.protocol.constants.SQLType, bool, bool, ushort) [0x56231b6bfb61]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/protocol/comms.d:673 void mysql.protocol.comms.ctorRow(mysql.connection.Connection, ref ubyte[], mysql.protocol.packets.ResultSetHeaders, bool, out std.variant.VariantN!(32uL).VariantN[], out bool[], out immutable(char)[][]) [0x56231b6bd57b]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/result.d:55 ref mysql.result.Row mysql.result.Row.__ctor(mysql.connection.Connection, ref ubyte[], mysql.protocol.packets.ResultSetHeaders, bool) [0x56231b6af8b1]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/protocol/comms.d:886 mysql.result.Row mysql.protocol.comms.getNextRow(mysql.connection.Connection) [0x56231b6be519]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/result.d:278 void mysql.result.ResultRange.popFront() [0x56231b6aff8f]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/result.d:233 ref mysql.result.ResultRange mysql.result.ResultRange.__ctor(mysql.connection.Connection, mysql.protocol.packets.ResultSetHeaders, immutable(char)[][]) [0x56231b6afe1c]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/commands.d:384 mysql.result.ResultRange mysql.commands.queryImpl(mysql.commands.ColumnSpecialization[], mysql.connection.Connection, mysql.protocol.comms.ExecQueryImplInfo) [0x56231b6b0884]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/commands.d:506 std.typecons.Nullable!(mysql.result.Row).Nullable mysql.commands.queryRowImpl(mysql.commands.ColumnSpecialization[], mysql.connection.Connection, mysql.protocol.comms.ExecQueryImplInfo) [0x56231b69e340]
../../../../.dub/packages/mysql-native-3.0.0/mysql-native/source/mysql/commands.d:453 std.typecons.Nullable!(mysql.result.Row).Nullable mysql.commands.queryRow(mysql.connection.Connection, const(char[]), mysql.commands.ColumnSpecialization[]) [0x56231b69e2cb]
source/app.d:11 _Dmain [0x56231b69d17c]
Program exited with code 1

mysql-nativeでbulk insertをする

最近はMySQLを触っている中で、なにかしらメモを残しておくかという気分になった。

mysql-nativeにはbulk insert用のサポートがあるわけではないが、メタプログラミングコンパイル時にクエリをある程度生成できたりすると便利だなあと思うなどした。

  • 環境
$ dmd --version
DMD64 D Compiler v2.094.0
$ cat dub.selections.json | grep mysql-native
                "mysql-native": "3.0.0",
  • コード (修正版)
import mysql.commands;
import mysql.connection;
import mysql.pool;
import std.algorithm : map;
import std.array :array;
import std.format : format;
import std.range : iota, repeat;
import std.stdio : writeln;
import std.string : join;

// vibe-coreのコネクションプールを使う
version = Have_vibe_core;

__gshared MySQLPool pool;

shared static this()
{
    string connStr = "host=localhost;port=3307;user=testuser;pwd=testpassword;db=testdb";
    // コネクションプールを生成
    pool = new MySQLPool(connStr);

    // デフォルトの最大並列接続数はuint.max
    // ここでは10に設定しておく
    pool.maxConcurrency(10);
}

auto bulkInsert(S, string table, S[] objects)(Connection conn)
{
    enum stmt = "INSERT INTO %s (%s) VALUES ".format(table, [__traits(allMembers, S)].join(","));
    enum rows = "(%s)".format("?".repeat(S.tupleof.length).join(",")).repeat(objects.length).join(",");
    mixin(`return conn.exec("` ~ stmt ~ rows ~ `",` ~
          objects.length.iota.map!(i => "objects[%d].tupleof".format(i)).join(",") ~
          ");");
}

void main()
{
    // プールからひとつコネクションをとってくる
    auto conn = pool.lockConnection();
    scope(exit) conn.close();

    // 一時テーブルを作成
    conn.exec("CREATE TEMPORARY TABLE `payment` (
        `customer_id` int not null,
        `amount` int not null,
        `account_name` text
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

    struct Payment
    {
        int customer_id;
        int amount;
        string account_name;
    }
    const payments = [
        Payment(1, 2, null),
        Payment(3, 4, "foo"),
        Payment(5, 6, null)];

    const affectedRows = conn.bulkInsert!(Payment, "payment", payments);

    enum select = "SELECT %s FROM payment".format([__traits(allMembers, Payment)].join(","));
    const result = conn.query(select)
        .map!(row => Payment(row[0].get!int, row[1].get!int, row[2].get!string))
        .array;

    writeln(result);
}

テストするのに最近はdockerを使っている。これは便利。 このくらいのクエリなら意味はないけど、bulk insertするときは max_allowed_packet を大きめに指定しておく。

$ docker run --rm -d -p 127.0.0.1:3307:3306 --name=mariadb10 -e MYSQL_DATABASE=testdb -e MYSQL_USER=testuser -e MYSQL_PASSWORD=testpassword -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mariadb:10.1 --max_allowed_packet=16MB
  • 実行結果
$ dub run -q
[const(Payment)(1, 2, ""), const(Payment)(3, 4, "foo"), const(Payment)(5, 6, "")]

Prologを使ってD言語のtraitsを表現してみる

D言語traitsのうちいくつか簡単に実装できそうなものを表現してみた。 といっても非常に不完全な状態であるが。

SWI-Prologを使ったが、他の実装でもおそらく問題ない。

$ swipl --version
SWI-Prolog version 7.6.4 for amd64

ソースコードは以下のような感じになっている。isScalarとかも実装したかったのだけれど、なかなか綺麗な感じにならなかった。

%  If the arguments are all either types that are arithmetic types.
traits(isArithmetic, Type1, Type2) :-
    traits(isArithmetic, Type1), traits(isArithmetic, Type2).

traits(isArithmetic, Type) :-
    traits(isIntegral, Type);
    traits(isFloating, Type).

%  Works like isArithmetic, except it's for integral types.
traits(isIntegral, byte).
traits(isIntegral, short).
traits(isIntegral, int).
traits(isIntegral, long).
traits(isIntegral, cent).
traits(isIntegral, ptrdiff_t).
traits(isIntegral, Type) :- traits(isUnsigned, Type).

%  Works like isArithmetic, except it's for floating types.
traits(isFloating, float).
traits(isFloating, cfloat).
traits(isFloating, ifloat).
traits(isFloating, double).
traits(isFloating, cduble).
traits(isFloating, idouble).
traits(isFloating, real).
traits(isFloating, creal).
traits(isFloating, ireal).

%  Works like isArithmetic, except it's for unsigned types.
traits(isUnsigned, bool).
traits(isUnsigned, char).
traits(isUnsigned, wchar).
traits(isUnsigned, dchar).
traits(isUnsigned, ubyte).
traits(isUnsigned, ushort).
traits(isUnsigned, uint).
traits(isUnsigned, ulong).
traits(isUnsigned, ucent).
traits(isUnsigned, size_t).

%  If the type's default initializer is all zero.
traits(isZeroInit, void).
traits(isZeroInit, byte).
traits(isZeroInit, short).
traits(isZeroInit, int).
traits(isZeroInit, long).
traits(isZeroInit, cent).
traits(isZeroInit, ptrdiff_t).
traits(isZeroInit, bool).
traits(isZeroInit, ubyte).
traits(isZeroInit, ushort).
traits(isZeroInit, uint).
traits(isZeroInit, ulong).
traits(isZeroInit, ucent).
traits(isZeroInit, size_t).

REPLで問い合わせてみる。

$ swipl traits.pl 
(...)
?- traits(isArithmetic, int).
true .

?- traits(isArithmetic, int, float).
true .

?- traits(isUnsigned, short).
false.

今の段階ではとくにできることがおもいつかないが、もうちょっと別のtraitsの表現も試してなんかおもしろいことができないか試していきたい。

D言語の名前修飾はUnicode識別子をエンコードしない

D言語はIdentifierとしてUnicode Codepointを使うことができる。

従って、以下のようなコードを書くことができる。

void ほげ() {}  // これは合法

上記の関数が名前修飾されると、以下のように修飾された名前の中にUnicode識別子が登場する。

$ dmd -c hoge.d
$ nm hoge.o
0000000000000000 t 
0000000000000000 R _D4hoge12__ModuleInfoZ
0000000000000000 W _D4hoge6ほげFZv
                 U __start_minfo
                 U __stop_minfo
                 U _d_dso_registry

これは名前修飾のABI仕様上合法である。

実際にUnicode識別子をIdentifierとして用いなければ問題とはならないが、例えばこういった文字集合を含んだシンボル名を解釈しないプラットフォームなどがある場合には問題が起きる。 (EDIt: 正確にいうと、D言語ソースコードUTF-8で書かれていると仕様で規定されているので、UTF-8エンコードされたシンボルが常に入っていることになると思われる。)

SwiftやRustの新しい名前修飾(v0)ではpunycode方式でUnicode識別子をエンコーディング(実際はRustの場合はさらに別のエンコーディング方式も用いている)することで対応する文字集合の範囲を狭めている。

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をサポートするパッチを書いた。