Closed19

Issue 23061 を倒したい

Tomoya TanjoTomoya Tanjo

要約: Alpine Linux で single binary を作成すると、例外がキャッチできなくなる.

https://issues.dlang.org/show_bug.cgi?id=23061

void main(string[] args)
{
	try
	{
		throw new Exception("aaa");
	}
	catch(Exception e)
	{
	}
}

Compile and execute it in Alpine Linux image as follows:

$ docker run --rm -it -v $PWD:/workdir -w /workdir alpine:3.15 sh
# apk --no-cache add dmd gcc musl-dev llvm-libunwind-static
# dmd -c app.d
# gcc -static app.o -o app -lphobos2 -lunwind
# ldd app
/lib/ld-musl-x86_64.so.1: app: Not a valid dynamic program
# ./app
src/rt/dwarfeh.d:330: uncaught exception reached top of stack
This might happen if you're missing a top level catch in your fiber or signal handler
object.Exception@app.d(11): aaa
----------------
??:? <ERROR: Unable to retrieve function name> [0x0]
Aborted
Tomoya TanjoTomoya Tanjo

src/rt/dwarfeh.d:330 近くを確認すると、_Unwind_RaiseExceptionnoreturn のはずなのに普通に制御が戻ってきているように見える。

    auto r = _Unwind_RaiseException(&eh.exception_object);

    /* Shouldn't have returned, but if it did:
     */
    switch (r)
    {
        case _URC_END_OF_STACK:
            ...
            fprintf(stderr, "%s:%d: uncaught exception reached top of stack\n", __FILE__.ptr, __LINE__);
            fprintf(stderr, "This might happen if you're missing a top level catch in your fiber or signal handler\n");
            ...
    }
Tomoya TanjoTomoya Tanjo

druntime での _Unwind_RaiseException の定義はこの辺
_Unwind_Worduintptr_t の alias。

...
else version (X86_Any)
{
    align(16) struct _Unwind_Exception
    {
        _Unwind_Exception_Class exception_class;
        _Unwind_Exception_Cleanup_Fn exception_cleanup;
        _Unwind_Word private_1;
        _Unwind_Word private_2;
    }
}
...

struct _Unwind_Context;

_Unwind_Reason_Code _Unwind_RaiseException(_Unwind_Exception *exception_object);

アラインメント周りの問題?

Tomoya TanjoTomoya Tanjo

libunwind のリポジトリ 上のコードは以下:

struct _Unwind_Exception
  {
    alignas(8) uint64_t exception_class;
    _Unwind_Exception_Cleanup_Fn exception_cleanup;
    unsigned long private_1;
    unsigned long private_2;
  };

Alpine Linux 3.15 (x86_64) の llvm-libunwind-dev にある /usr/include/unwind.h でのコードは以下:

struct _Unwind_Exception {
  uint64_t exception_class;
  void (*exception_cleanup)(_Unwind_Reason_Code reason,
                            _Unwind_Exception *exc);
#if defined(__SEH__) && !defined(__USING_SJLJ_EXCEPTIONS__)
  uintptr_t private_[6];
#else
  uintptr_t private_1; // non-zero means forced unwind
  uintptr_t private_2; // holds sp that phase1 found for phase2 to use
#endif
#if __SIZEOF_POINTER__ == 4
  // The implementation of _Unwind_Exception uses an attribute mode on the
  // above fields which has the side effect of causing this whole struct to
  // round up to 32 bytes in size (48 with SEH). To be more explicit, we add
  // pad fields added for binary compatibility.
  uint32_t reserved[3];
#endif
  // The Itanium ABI requires that _Unwind_Exception objects are "double-word
  // aligned".  GCC has interpreted this to mean "use the maximum useful
  // alignment for the target"; so do we.
} __attribute__((__aligned__));
Tomoya TanjoTomoya Tanjo

llvm-libunwind-dev にある _Unwind_Exception はメンバーがかなり多い。
むしろどうして動的リンクの場合は動いていたのか…

ARM_EABI_UNWINDER 相当のコードを見てました。

Tomoya TanjoTomoya Tanjo

該当環境だと __SEH__ 未定義かつ __SIZEOF_POINTER__ が 8 なので、druntime と同様の定義のはず。

Tomoya TanjoTomoya Tanjo

__attribute__((__aligned__)) が気になる。実際にどう整列されているか(自動設定された __aligned__ のパラメータ)を確認するにはどうするんだろう?

Tomoya TanjoTomoya Tanjo

Alpine Linux 3.15 上で dmd+druntime ビルドしたらシングルバイナリでも動くやんけ!
ということはライブラリそのものではなく、ビルド時のオプションなどに問題がある可能性が高そう。

Tomoya TanjoTomoya Tanjo

確認したのは以下の組み合わせ

  • Alpine linux 3.15 + ldc (apk): 動かない
  • Alpine linux 3.15 + dmd (apk): 動かない
  • Alpine linux 3.15 + dmd (自前ビルド): 動く
  • Alpine linux edge + ldc (apk): 動かない
  • Alpine linux edge + dmd (apk): 動かない
  • Alpine linux edge + dmd (自前ビルド): 未確認

注意すべきなのは、dmd(apk) でリンクしているのは ldc-druntime の druntime (i.e., ldc でビルドした druntime) だということ。-> 違った。ldc と dmd で libphobos などは別に用意されている。

原因として考えられるのは以下?

  • druntime (ldc でビルドするとおかしくなるケース) -> こっちっぽい (後述)
  • ldc (druntime のビルド時に変なライブラリが生成される?)
Tomoya TanjoTomoya Tanjo

Alpine Linux のパッケージは ABUILD ファイルから生成される。リポジトリは以下にある。

abuild -r で生成したバイナリで再現することを確認できた。
ただ、

  • -lowmem なしでも再現する? -> した
  • ldc 1.29.0 でも再現する? -> "undefined reference to '__cmsg_nxthdr'" でビルドできない (druntime?)
  • llvm のバージョン(apk の 1.28.1 は llvm12 でビルドされている)を上げても再現する? -> ldc 1.28.1 は llvm13 を未サポート
  • リンカを変更しても再現する? (apk の 1.28.1 は bfd でリンク) -> lld だとビルドできず(オプションの問題?)、gold だと再現する
Tomoya TanjoTomoya Tanjo

dmd+digger 調査しようとすると、dub がアーキテクチャ判定に失敗して動かない…

Tomoya TanjoTomoya Tanjo

Alpine Linux で dmd のパッケージを作成している APKBUILD を確認すると、以下のパッチが適用されていた。

-        case Musl:        return predef("CRuntime_Musl");
+        case Musl:        return predef("CRuntime_Musl"), predef("DRuntime_Use_Libunwind");

手動ビルドで問題なかったのは、libunwind を利用していなかったからだと考えられる。
…ということは druntime の libunwind 周辺が問題ということで確定っぽい。

Tomoya TanjoTomoya Tanjo

ARM 環境だと libunwind 用の構造体の定義が違う (上参照) ようなので、ARM でも再現するかを後で確認する。

-> 再現した。ARM 用コンテナでも例外が catch できない。

Tomoya TanjoTomoya Tanjo

DRuntime_Use_Libunwind 込みでビルドした dmd でも再現するのを確認。

Tomoya TanjoTomoya Tanjo

どうやら alpine:3.13 (ldc: 1.24.0, frontend: 2.094.1) から alpine:3.14 (ldc: 1.26.0, frontend: 2.096.1) の間に入ったリグレッションの模様。

Tomoya TanjoTomoya Tanjo

だと思いきや
v2.100.0 (build with DRuntime_Use_Libunwind) on Alpine Linux edge で以下を確認:

  • リンク時に -luwind なし
$ ./dmd/generated/linux/release/64/dmd -c sample.d || exit 1
$ cc sample.o -o sample -m64 -static -Xlinker --export-dynamic -L./dmd/generated/linux/release/64/../../../../../phobos/generated/linux/release/64 -Xlinker -Bstatic -lphobos2 -lpthread -lm -lrt -ldl || exit 1
$ ./sample
$ echo $?
0
  • リンク時に -lunwind あり
$ ./dmd/generated/linux/release/64/dmd -c sample.d || exit 1
$ cc sample.o -o sample -m64 -static -Xlinker --export-dynamic -L./dmd/generated/linux/release/64/../../../../../phobos/generated/linux/release/64 -Xlinker -Bstatic -lphobos2 -lpthread -lm -lrt -ldl -lunwind || exit 1
$ ./sample
src/rt/dwarfeh.d:330: uncaught exception reached top of stack
This might happen if you're missing a top level catch in your fiber or signal handler
object.Exception@sample.d(9): aa
Aborted
$ echo $?
134

libunwind が重複してリンクされている?

Tomoya TanjoTomoya Tanjo

一方で、ldc 1.28.0 (apk) on Alpine Linux edge では以下のようになる:

  • リンク時に -lunwind なし
$ ldc2 -c sample.d
$ /usr/bin/cc sample.o -static -o sample -fuse-ld=bfd -Xlinker --export-dynamic -L/usr/lib -lphobos2-ldc -ldruntime-ldc -Wl,--gc-sections -lrt -ldl -lpthread -lm -m64
/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld.bfd: /usr/lib/libdruntime-ldc.a(handler.o): in function `_D4core8internal9backtrace7handler16LibunwindHandler6__ctorMFNbNimZCQCoQCmQCgQBzQBu':
handler.d:(.text._D4core8internal9backtrace7handler16LibunwindHandler6__ctorMFNbNimZCQCoQCmQCgQBzQBu+0x78): undefined reference to `unw_getcontext'
/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld.bfd: handler.d:(.text._D4core8internal9backtrace7handler16LibunwindHandler6__ctorMFNbNimZCQCoQCmQCgQBzQBu+0x83): undefined reference to `unw_init_local'
/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld.bfd: handler.d:(.text._D4core8internal9backtrace7handler16LibunwindHandler6__ctorMFNbNimZCQCoQCmQCgQBzQBu+0xa4): undefined reference to `unw_step'
/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld.bfd: handler.d:(.text._D4core8internal9backtrace7handler16LibunwindHandler6__ctorMFNbNimZCQCoQCmQCgQBzQBu+0xd7): undefined reference to `unw_get_proc_info'
/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld.bfd: handler.d:(.text._D4core8internal9backtrace7handler16LibunwindHandler6__ctorMFNbNimZCQCoQCmQCgQBzQBu+0xf4): undefined reference to `unw_step'
collect2: error: ld returned 1 exit status
  • リンク時に -lunwind あり
$ ldc2 -c sample.d
$ /usr/bin/cc sample.o -static -o sample -fuse-ld=bfd -Xlinker --export-dynamic -L/usr/lib -lphobos2-ldc -ldruntime-ldc -Wl,--gc-sections -lunwind -lrt -ldl -lpthread -lm -m64
$ ./sample
rt/dwarfeh.d:354: uncaught exception reached top of stack
This might happen if you're missing a top level catch in your fiber or signal handler
object.Exception@sample.d(9): aa
----------------
<unknown dir>/<unknown file>:10 <ERROR: Unable to retrieve function name> [0x0]
Aborted
$ echo $?
134
Tomoya TanjoTomoya Tanjo

リンカオプションが!!!足りない!!!!(ただし CCgcc を使う場合)

  • dmd: リンク時に -Xlinker --eh-frame-hdr が必要
$ dmd -c app.d
$ gcc app.o -o app -m64 -static -Xlinker --eh-frame-hdr -Xlinker --export-dynamic -L/usr/lib/ -Xlinker -Bstatic -lphobos2 -lpthread -lm -lrt -ldl -lunwind
$ ./app
$ echo $?
0
  • ldc: ldc のオプションに -L--eh-frame-hdr が必要
$ CC=gcc ldc2 -L--eh-frame-hdr -static -run app.d
$ echo $?
0

ちなみに clang を使う場合にはこの問題はそもそも起きない

  • dmd
$ dmd -c app.d
$ clang app.o -o app -m64 -static -Xlinker --export-dynamic -L/usr/lib/ -Xlinker -Bstatic -lphobos2 -lpthread -lm -lrt -ldl -lunwind
$ ./app
$ echo $?
0
  • ldc
$ CC=clang ldc2 -static -run app.d
$ echo $?
0
このスクラップは2022/05/24にクローズされました