mixi Advent Calendar の18日目です。
はじめに
このエントリは「ハロー、Hello World!」という素晴らしい書籍読んで思いついたものです。実のところ、普段使用する言語においてHello, World!のような簡単なプログラムであってもどのように実行されているか本当に理解している人は少ないのではないでしょうか。なかなか調べる機会もありませんからね。せっかくなのでこの場を借りて調べてみようと思います。
内容に誤りがある場合は気軽にコメントで指摘してください。
なお、本エントリで参照する dmd/druntime/phobos の各ソースコードは Boost License 1.0 の基で配布されています。
目的
とりあえずこのあたりを追ってみます。
- std.stdio はどこにあるのか
- writeln の先では何が行われているのか
- main の前に何が行われているのか
std.stdio はどこにあるのか
std.stdio
は phobos
というD言語の標準ライブラリ郡の中にあるライブラリのひとつです。
dmdとphobosはどこで関連づけられているのでしょう?
dmdにはコンパイル中に何が行われているかを表すverboseオプション(-vで有効)があるので、これで少し中を覗いてみることにしましょう。
$ dmd -w -g -v helloworld.d
binary dmd
version v2.069.0
config /etc/dmd.conf
parse helloworld
...
gcc helloworld.o -o helloworld -g -m64 -L/usr/lib/x86_64-linux-gnu -Xlinker --export-dynamic -Xlinker -Bstatic -lphobos2 -Xlinker -Bdynamic -lpthread -lm -lrt -ldl
出力は少し長いので省略しています。多くはこのバイナリを生成するために必要となるモジュールのimportです。
さて、上のほうで config /etc/dmd.conf
という行がありますね。このファイルは一体なんでしょうか?
;
; dmd.conf file for dmd
;
; dmd will look for dmd.conf in the following sequence of directories:
; - current working directory
; - directory specified by the HOME environment variable
; - directory dmd resides in
; - /etc directory
;
; Names enclosed by %% are searched for in the existing environment and inserted
;
; The special name %@P% is replaced with the path to this file
;
[Environment32]
DFLAGS=-I/usr/include/dmd/phobos -I/usr/include/dmd/druntime/import -L-L/usr/lib/i386-linux-gnu -L--export-dynamic
[Environment64]
DFLAGS=-I/usr/include/dmd/phobos -I/usr/include/dmd/druntime/import -L-L/usr/lib/x86_64-linux-gnu -L--export-dynamic
どうやらdmdはファイル定義からphobosやdruntimeの場所を参照してるようです。(実際に私の環境ではphobosはこの場所においてあります)
dmd.conf
はmanもあるので、気になる人はそちらも参照してください。
Hello, World!
とにかく、D言語での「Hello, World!」のソースコードを見てみましょう。
import std.stdio;
void main()
{
writeln("Hello, World!");
}
この種のプログラムで言語ごとの比較を論じるのは難しいものですが、CやJavaの構文と似ているんじゃないでしょうか。
writeln の前ではなにが行われているのか
writelnはstd.stdioで定義されている関数です。
stdioのbackendは windows/linux/OSX などOSによってかわります。
Linuxの場合はdmdの中で CRuntime_Glibc
が定義されており、さらに GCC_IO
が std.stdio
で定義され、glibcの関数を使うようになっています。
実際にコードを追う前に、ltraceを使って雑に見てみましょう。
$ dmd -gc helloworld.d
$ ltrace -o dump ./helloworld
結果の一部を抜粋して、writeln と関係する部分を抜き出すとこのあたりになります。
...
fwide(0x7fad0e3b5400, 0)
flockfile(0x7fad0e3b5400, 0, 0x7fad0e3b5400, 0x66ddf0)
fwrite("Hello, World!", 1, 13, 0x7fad0e3b5400)
fputc_unlocked(10, 0x7fad0e3b5400, 13, 1024)
funlockfile(0x7fad0e3b5400, 0x7fad0ea00000, 0x7fad0ea00000, -1)
...
C言語の printf("Hello, World!\n")
は標準出力への書き込みの部分で puts
を呼びますが、D言語では fwrite
を呼んでいることがわかります。
ここで奇妙な関数があることに気がつきます。 flockfile
, funlockfile
はいったいなんでしょうか?
flockfileのmanページをみると、この関数はstdioのFILEオブジェクトのロックを操作するための関数であり、他のスレッドが入出力操作の途中で割り込むことを避けるために使うためのもの、であるようです。ここでは fputc_unlocked
のような関数がロックを行わないバージョンの標準入出力関数であることも同時に説明されています。
さて、ここからは実際にソースコードを読んでみることにしましょう。
phobosでのwriteln関数の定義はこのようになっています。
/***********************************
* Equivalent to $(D write(args, '\n')). Calling $(D writeln) without
* arguments is valid and just prints a newline to the standard
* output.
*/
void writeln(T...)(T args)
{
import std.traits : isAggregateType;
static if (T.length == 0)
{
import std.exception : enforce;
enforce(fputc('\n', .trustedStdout._p.handle) == '\n', "fputc failed");
}
else static if (T.length == 1 &&
is(typeof(args[0]) : const(char)[]) &&
!is(typeof(args[0]) == enum) &&
!is(Unqual!(typeof(args[0])) == typeof(null)) &&
!isAggregateType!(typeof(args[0])))
{
import std.exception : enforce;
// Specialization for strings - a very frequent case
auto w = .trustedStdout.lockingTextWriter();
static if (isStaticArray!(typeof(args[0])))
{
w.put(args[0][]);
}
else
{
w.put(args[0]);
}
w.put('\n');
}
else
{
// Most general instance
trustedStdout.write(args, '\n');
}
}
見慣れない単語はたくさんでてきました。
ネタばらしをしてしまうと、ここで else static if (T.length == 1 &&...
という分岐に入ることになるわけなのですが、なぜそうなるのかさっぱりわからないですね!
ここは焦らず、順を追ってみていきましょう。
まず引数の数がひとつ (T.length == 1)
であることはなんとなくわかります。
次に is(typeof(args[0]) : const(char)[])
ですが、少し厄介なかんじがしますね! typeof
は任意の式の型を取得できる式です。今回のように typeof(文字列) である場合にstringという型情報を返してくれます。
次に is
ですが、これは引数の型が意味的に正しいかどうかを判定するための式です。 is(typeof { exp...})
というのはis式が直接式を引数にとれないためにtypeofをはさむ、というハック的なものです。
次に ... : const(char)[])
という部分ですが、これは一体なんなのでしょうか? 実はこれもis式の特殊なパターンで、implicit conversion
が可能であるかどうかのチェックを行うための構文となります。
次は !is(typeof(args[0]) == enum
ですね。 enum
は列挙体を表します。is(typeof({exp..}) == enum
は名前付き列挙体の要素であるかをチェックします。 今回writelnに渡している文字列型はとくにそのようなことは行っていないため、この条件分岐は通ります。
!is(Unqual!(typeof(args[0])) == typeof(null))
をみてみましょう。 Unqual
は修飾子(const, sharedなど)を取り除くtemplate構文です。ここでやっていることは null
が来ている場合に弾くということだけですが、都度 cast(static)null
のようなコードを書くのは面倒なところでUnqualを使うと楽に処理を書けます。
最後に !isAggregateType!(typeof(args[0])))
ですね。ここでAggregateTypeというのは構造体やクラスのような個々の要素としても集団としても参照可能であるデータの集まりを表す型を意味しています。今回は文字列が渡されているので、この条件分岐も通ります。
さて、これでようやくこの関数がどこへ向かうのかがわかるようになりました。
今回は trustedStdout.lockingTextWriter
が肝となります。
/// Range primitive implementations.
void put(A)(A writeme)
if (is(ElementType!A : const(dchar)) &&
isInputRange!A &&
!isInfinite!A)
{
import std.exception : errnoEnforce;
alias C = ElementEncodingType!A;
static assert(!is(C == void));
static if (isSomeString!A && C.sizeof == 1)
{
if (orientation_ <= 0)
{
//file.write(writeme); causes infinite recursion!!!
//file.rawWrite(writeme);
static auto trustedFwrite(in void* ptr, size_t size, size_t nmemb, FILE* stream) @trusted
{
return .fwrite(ptr, size, nmemb, stream);
}
auto result =
trustedFwrite(writeme.ptr, C.sizeof, writeme.length, fps_);
if (result != writeme.length) errnoEnforce(0);
return;
}
}
// put each character in turn
foreach (dchar c; writeme)
{
put(c);
}
}
ここでようやく fwrite
が出てきました。ここでfwriteを特別扱いしたのは文字列を都度putc_unlockedを実行するコストを避けたいからでしょう。
main の前になにが行われているか
D言語のmain関数はC言語のmain関数と異なるものです。
( ՞ਊ ՞) :~/dev/dlang/helloworld $ dmd -g helloworld.d
( ՞ਊ ՞) :~/dev/dlang/helloworld $ gdb ./helloworld
...
(gdb) b _Dmain
Breakpoint 1 at 0x4351a4: file helloworld.d, line 5.
(gdb) r
...
Breakpoint 1, D main () at helloworld.d:5
5 writeln("Hello, World!");
(gdb) bt
#0 D main () at helloworld.d:5
#1 0x0000000000436227 in rt.dmain2._d_run_main() ()
#2 0x000000000043617d in rt.dmain2._d_run_main() ()
#3 0x00000000004361e3 in rt.dmain2._d_run_main() ()
#4 0x000000000043617d in rt.dmain2._d_run_main() ()
#5 0x00000000004360da in _d_run_main ()
#6 0x0000000000435678 in main ()
_Dmainのアセンブリ表現もみておきましょう。
(gdb) disas
Dump of assembler code for function _Dmain:
0x00000000004351a0 <+0>: push %rbp
0x00000000004351a1 <+1>: mov %rsp,%rbp
=> 0x00000000004351a4 <+4>: mov $0x467940,%edx
0x00000000004351a9 <+9>: mov $0xd,%edi
0x00000000004351ae <+14>: mov %rdx,%rsi
0x00000000004351b1 <+17>: callq 0x4351c0 <_D3std5stdio16__T7writelnTAyaZ7writelnFNfAyaZv>
0x00000000004351b6 <+22>: xor %eax,%eax
0x00000000004351b8 <+24>: pop %rbp
0x00000000004351b9 <+25>: retq
ソースコード的にとっかかりになりそうなところはこのへんでしょうか。
- dmd:src/mars.d
extern (C++) void genCmain(Scope* sc)
{
if (entrypoint)
return;
/* The D code to be generated is provided as D source code in the form of a string.
* Note that Solaris, for unknown reasons, requires both a main() and an _main()
*/
static __gshared const(char)* cmaincode =
q{
extern(C)
{
int _d_run_main(int argc, char **argv, void* mainFunc);
int _Dmain(char[][] args);
int main(int argc, char **argv)
{
return _d_run_main(argc, argv, &_Dmain);
}
version (Solaris) int _main(int argc, char** argv) { return main(argc, argv); }
}
};
...
callstack的には
main -> _d_run_main(druntime) -> ...
となっていますね、gdbのbacktrace表示とあわせてみても正しいよう見えます。
ここで _d_run_main
はdruntimeのほうにもコードがあります。
druntime:rt/dmain2.d
extern (C) int _d_run_main(int argc, char **argv, MainFunc mainFunc)
{
...
void runAll()
{
if (rt_init() && runModuleUnitTests())
tryExec({ result = mainFunc(args); });
else
result = EXIT_FAILURE;
if (!rt_term())
result = (result == EXIT_SUCCESS) ? EXIT_FAILURE : result;
}
tryExec(&runAll);
...
}
tryExecは渡されるdelegateを実行して例外が起きた時のハンドリングをしているだけなのでここでは省略しています。
runAll
の中を見てみましょう。
まず rt_init
と runModuleUnitTests
関数がそれぞれ実行され、それらの実行が成功であったときに mainFunc(args)
すなわち _Dmain
を実行する流れとなっています。
rt_init
はmutex属性オブジェクトやGC、モジュール内のコンストラクタの初期化をしています。
/**********************************************
* Initialize druntime.
* If a C program wishes to call D code, and there's no D main(), then it
* must call rt_init() and rt_term().
*/
extern (C) int rt_init()
{
...
_d_monitor_staticctor();
_d_critical_init();
try
{
initSections();
// this initializes mono time before anything else to allow usage
// in other druntime systems.
_d_initMonoTime();
gc_init();
initStaticDataGC();
lifetime_init();
rt_moduleCtor();
rt_moduleTlsCtor();
return 1;
}
...
}
続いて runModuleUnitTests
はimportしているモジュールのunittestを実行しています。
プログラムの実行前にユニットテストが実行されるというのはなかなか驚きですね。
/**
* This routine is called by the runtime to run module unit tests on startup.
* The user-supplied unit tester will be called if one has been supplied,
* otherwise all unit tests will be run in sequence.
*
* Returns:
* true if execution should continue after testing is complete and false if
* not. Default behavior is to return true.
*/
extern (C) bool runModuleUnitTests()
{
// backtrace
version( CRuntime_Glibc )
import core.sys.linux.execinfo;
...
static if( __traits( compiles, backtrace ) )
{
import core.sys.posix.signal; // segv handler
static extern (C) void unittestSegvHandler( int signum, siginfo_t* info, void* ptr ) nothrow
{
static enum MAXFRAMES = 128;
void*[MAXFRAMES] callstack;
int numframes;
numframes = backtrace( callstack.ptr, MAXFRAMES );
backtrace_symbols_fd( callstack.ptr, numframes, 2 );
}
sigaction_t action = void;
sigaction_t oldseg = void;
sigaction_t oldbus = void;
(cast(byte*) &action)[0 .. action.sizeof] = 0;
sigfillset( &action.sa_mask ); // block other signals
action.sa_flags = SA_SIGINFO | SA_RESETHAND;
action.sa_sigaction = &unittestSegvHandler;
sigaction( SIGSEGV, &action, &oldseg );
sigaction( SIGBUS, &action, &oldbus );
scope( exit )
{
sigaction( SIGSEGV, &oldseg, null );
sigaction( SIGBUS, &oldbus, null );
}
}
if( Runtime.sm_moduleUnitTester is null )
{
size_t failed = 0;
foreach( m; ModuleInfo )
{
if( m )
{
auto fp = m.unitTest;
if( fp )
{
try
{
fp();
}
catch( Throwable e )
{
_d_print_throwable(e);
failed++;
}
}
}
}
return failed == 0;
}
return Runtime.sm_moduleUnitTester();
}
これらの処理が問題なく行われた後にmain関数の中身が実行されるというわけです。
実行環境
最後に、私が本記事の内容を検証した際の実行環境を掲載しておきます。
- OS/Distribution
$ uname -mrv
3.13.0-68-generic #111-Ubuntu SMP Fri Nov 6 18:17:06 UTC 2015 x86_64
$ cat /etc/issue
Ubuntu 14.04.3 LTS \n \l
- D言語
コンパイラとしてDMDを使います。
$ dmd --version
DMD64 D Compiler v2.069.0
Copyright (c) 1999-2015 by Digital Mars written by Walter Bright
明日
明日は GitHub ID: oppai の人 さんこと @kodam の 実践ぼっち駆動開発 です。