Issue 23061 を倒したい
要約: Alpine Linux で single binary を作成すると、例外がキャッチできなくなる.
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
src/rt/dwarfeh.d:330 近くを確認すると、_Unwind_RaiseException
が noreturn
のはずなのに普通に制御が戻ってきているように見える。
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");
...
}
druntime での _Unwind_RaiseException
の定義はこの辺。
_Unwind_Word
は uintptr_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);
アラインメント周りの問題?
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__));
llvm-libunwind-dev にある _Unwind_Exception
はメンバーがかなり多い。
むしろどうして動的リンクの場合は動いていたのか…
ARM_EABI_UNWINDER
相当のコードを見てました。
該当環境だと __SEH__
未定義かつ __SIZEOF_POINTER__
が 8 なので、druntime と同様の定義のはず。
__attribute__((__aligned__))
が気になる。実際にどう整列されているか(自動設定された __aligned__
のパラメータ)を確認するにはどうするんだろう?
Alpine Linux 3.15 上で dmd+druntime ビルドしたらシングルバイナリでも動くやんけ!
ということはライブラリそのものではなく、ビルド時のオプションなどに問題がある可能性が高そう。
確認したのは以下の組み合わせ
- 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 のビルド時に変なライブラリが生成される?)
Alpine Linux のパッケージは ABUILD
ファイルから生成される。リポジトリは以下にある。
-
https://gitlab.alpinelinux.org/alpine/aports
- ldc などの
ABUILD
ファイルは/community/ldc
にある
- ldc などの
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 だと再現する
dmd+digger 調査しようとすると、dub がアーキテクチャ判定に失敗して動かない…
Alpine Linux で dmd のパッケージを作成している APKBUILD
を確認すると、以下のパッチが適用されていた。
- case Musl: return predef("CRuntime_Musl");
+ case Musl: return predef("CRuntime_Musl"), predef("DRuntime_Use_Libunwind");
手動ビルドで問題なかったのは、libunwind を利用していなかったからだと考えられる。
…ということは druntime の libunwind 周辺が問題ということで確定っぽい。
ARM 環境だと libunwind 用の構造体の定義が違う (上参照) ようなので、ARM でも再現するかを後で確認する。
-> 再現した。ARM 用コンテナでも例外が catch できない。
DRuntime_Use_Libunwind
込みでビルドした dmd でも再現するのを確認。
どうやら alpine:3.13
(ldc: 1.24.0, frontend: 2.094.1) から alpine:3.14
(ldc: 1.26.0, frontend: 2.096.1) の間に入ったリグレッションの模様。
だと思いきや
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 が重複してリンクされている?
一方で、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
リンカオプションが!!!足りない!!!!(ただし CC
に gcc
を使う場合)
- 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
aport の ldc 1.29.0 の MR で上記変更を反映してもらえたので、マージされれば ldc2
に手動で -L--eh-frame-hdr
を付ける必要はなくなりそう。