日経ソフトウエア 2016年5月号に寄稿しました。そして記事の補足

2016年3月24日木曜日

日経BP刊の日経ソフトウエア誌 2016年5月号(3/24発売。今日です!→Amazonで買えます)に「Windowsアプリはなぜ動く ~そのとき、PC上では何が起こっているのか?~」という特集記事を寄稿しました。

  • Windows上でC系の言語から素直に書きだした.exeファイルとC#などの.NET系言語から書きだした.exeファイルの中身の差
  • これらを実行した際のOS側での処理差やランタイム差
  • Universalなアプリ(8.1 Universal, UWP)の登場背景と基本構造
という話を広く書きました*1
[*1] ところどころコラムでMonoへの言及や.NET Nativeの話なども挟まっていますが、いずれも概要のみです。

背景とねらい

話の基盤には同誌2015年9月号に掲載された「特集1 言語はなぜ動く」というものがあります。これは、Windowsプログラムを含めて様々な言語(JSなど)で書いたプログラムが実際に実行されるまでの流れを解説した、なかなかクールなものでした(私が書いたものではありません)。
この流れを一定汲みつつ、よりWindows上そして主にC#で書かれたプログラムへと振ったのが今回の特集です。
もちろん、各分野を専門的にやっている人からするとイントロぐらいの話です。なるべくすらすら読めて、初心者の方でもなんとなく分かる(そして、更に興味を持って書籍やネットで調べたくなる)というのを狙いにしました。
パラッと読んでみて「これざっくり把握するのに良いから読んでみて」と後輩へ渡すようなシーンが生まれると最高に嬉しいです。
内容は分かりやすい範囲で正確性を失わないよう心がけたつもりですが、不正確な点など気付かれましたらmuo [at] muo.jp宛に指摘を頂けると幸いです。
本エントリでは、書いた原稿の中でボツになったものを中心に、記事の補足を記載します。

参考文献

編集過程でなくなっていましたが、実は一番書きたかったのがこれです。
今回の特集では、.NET Frameworkの内部やWindowsの内部処理に関する紹介をおこないました。これらのトピックについては、次に挙げる書籍が大変参考になります。
なかには入手しにくい本も存在しますが、興味が湧いたらぜひ読んでみてください。
  • インサイドWindows 第6版 上巻および下巻 (著: Mark E. Russinovich, David A. Solomon, Alex Ionescu, 訳: 株式会社クイープ)
    • 今回の特集での関連トピック: Windows上でのプログラム実行の仕組みやOSの内部構造について
    • この筋の定番本です
    • 紙版は結構値が張るので、Kindle版をおすすめします
  • プログラミング.NET Framework 第4版 (著: Jeffrey Richter, 訳: 藤原 雄介)
    • 今回の特集での関連トピック: .NETの型システムやメタデータ、セキュリティに関する構造などについて
    • .NETについてより良く知りたい、と感じたらこの本を開くと大体良いトピックが見つかります
  • The Root of .NET Framework (著: 荒井 省三)
    • 今回の特集での関連トピック: .NET Frameworkの構造やCILからネイティブコードへの変換などについて
    • 今回の特集では扱いませんでしたが、SOSを利用してJITコンパイル後のx86コードを追いかけるくだりはシビれます。Windows 10世代では多少事情が異なるポイントもありますが、原則の部分はほとんど通用します
    • 残念ながら絶版本なので、古本在庫が尽きたら異常に値段が上がりそうです。早めに買うことをおすすめします

クラスライブラリのソースコードに迫ってみる

内容の割に長かったためかボツになったのがこれです。
特集の本文で紹介したように、.NET FrameworkではCLRとクラスライブラリの組み合わせによって実行環境で要求される機能を実現しています。
これらの中身にもっと迫ってみることはできないのでしょうか。
Microsoftは2014年の暮れに.NET Frameworkのソースコードを一部MITライセンスという非常に制限の少ない(利用者側の自由度が高い)ライセンスで一般公開しました。この中をたどっていくことで、CLRの実装詳細やクラスライブラリの中身を追いかけることができます。ここでは、.NET Frameworkが提供する機能のソースコードを追いかける例を紹介します。
C#でプログラムを書いていて、プログラムの状態をデバッグ出力することがしばしばあります。Cプログラミングでいうところのprintfデバッグの類です。多く利用される方法はSystem.Diagnosticsパッケージに含まれるDebug.WriteLineメソッドの利用です。この部分はオープンソースで公開されているので、コードを完全に追いかけることができます。
試しに追いかけてみましょう。
以下の説明でのソースコードはMicrosoft社がMITライセンスにてGitHubで公開しているreferencesourceの抜粋です*2
まず、.NET Frameworkが提供するDebug.WriteLineメソッドにはいくつかのオーバーロードがありますが、ここでは
 Debug.WriteLine("Help me!");
というコードを書いた場合のクラスライブラリ側コードを追いかけてみます。
まず、Debug.WriteLineメソッドはSystem/compmod/system/diagnostics/ディレクトリ以下のDebug.csを起点として呼び出されます。この中でstringを受け取るオーバーロードを探すと
 [System.Diagnostics.Conditional("DEBUG")]
 public static void WriteLine(string message) {
     TraceInternal.WriteLine(message);
 }
が見つかります。
TraceInternalクラスは同ディレクトリにあるTraceInternal.csで定義されています。その中で、呼びだされているWriteLineメソッドは次のとおりです。
 public static void WriteLine(string message) {
   if (UseGlobalLock) {
     lock (critSec) {
       foreach (TraceListener listener in Listeners) {
         listener.WriteLine(message);
         if (AutoFlush) listener.Flush();
       }
     }
   }
   else {
     foreach (TraceListener listener in Listeners) {
       if (!listener.IsThreadSafe) {
         lock (listener) {
           listener.WriteLine(message);
           if (AutoFlush) listener.Flush();
         }
       }
       else {
         listener.WriteLine(message);
         if (AutoFlush) listener.Flush();
       }
     }
   }
 }
ここではロックの取得方法によって処理が2パターンありますが、いずれもListenersからTraceListenerを取り出してそれぞれに対する出力処理をしていることがわかります。
同じファイル内を読むと、Listenersはプロパティとして宣言されており、このプロパティを最初に取得するタイミングでデフォルトの内容が作成されることがわかります。ここで実際に生成されるのはDefaultTraceListenerのインスタンスです。
DefaultTraceListenerのコードはDefaultTraceListener.csにあります。WriteLineメソッドの中でWriteメソッドを呼び、その中でinternalWriteメソッドを呼び出しています。
internalWriteメソッドの記述は次のとおりです。
 void internalWrite(string message) {
   if (Debugger.IsLogging()) {
     Debugger.Log(0, null, message);
   } else {
     if (message == null)
       SafeNativeMethods.OutputDebugString(String.Empty);
     else
       SafeNativeMethods.OutputDebugString(message);
   }
 }
デバッガでのログ出力が有効であればメッセージをデバッガへ流し、そうでなければSafeNativeMethodsOutputDebugStringメソッドを呼び出しています。
この実体はSystem/compmod/microsoft/win32/SafeNativeMethods.csファイルの次の部分です。
 [DllImport(ExternDll.Kernel32, CharSet = System.Runtime.InteropServices.CharSet.Auto, BestFitMapping = true)]
 [ResourceExposure(ResourceScope.None)]
 public static extern void OutputDebugString(String message);
ここでWindows用のDLLへのバインドをおこなう処理が登場しました。これで、あとはWindows APIの領域です。OutputDebugString APIのリファレンスはhttps://msdn.microsoft.com/ja-jp/library/cc428973.aspxにあります。
なお、GitHubのreferencesourceリポジトリに存在しないコードでも、.NET Frameworkのライブラリは2008年から閲覧専用のライセンスで公開されています*3。原稿執筆時点では、最新の.NET Frameworkである4.6.1のソースコードが公開されています。
たとえば本文のPart 1で作成した.NET Frameworkでメッセージボックスを表示するプログラムについて、挙動がおかしいとします。その問題を調査したり、フレームワークとの適合度を高めるための用途であれば、ここのソースを読むことができます。しかしコードをコピー&ペーストして利用したり再配布するような行為は禁止されている(GitHubのreferencesourceリポジトリにあるものと違って、ライセンス上の制限がかなり厳しい)ので注意が必要です。
[*2] ライセンスの全文はhttps://github.com/Microsoft/referencesource/blob/master/LICENSE.txtで公開されています。

サブシステムの概念を簡単に補足

特集の中では細かな事情として省くことになったのですが、Windowsの構造について話をしていくと避けて通れないのがサブシステムです。
Windowsはもともと多様なアーキテクチャの実行ファイルを実行できるように設計されています。Windows向けのexeファイルは素直に実行されますが、たとえばUNIXの仕様に基づくPOSIXサブシステム(以前SFUと呼ばれていたもので、最近のWindowsではSUAと呼ばれます)を利用するものはposix.exeというプロセスがホストする形で実行し、psxdll.dllをライブラリとして利用します。
WindowsなのになぜUNIX標準にもとづくプログラムを?と思うかもしれません。これには歴史的な事情があり、1980年代の米国政府が業務に利用するOSの調達要件としてPOSIX.1というUNIX標準への準拠を必須としていたためです。Windows 8の時代まで、Enterprise版のWindowsはSUAをサポートしていました。Windows Server 2012 R2およびWindows 8.1ではSUAが削除されました。代わりにUNIX系の機能をWindows上で利用したい場合にはHyper-Vで仮想化されたUNIX系OSを利用したりCygwinのような仕組みを利用することを推奨するようになりました。
また、Microsoftが2015年の4月に発表した「AndroidアプリをWindows上で動作させる」仕組みであったProject Astoriaは、Windows上にAndroidサブシステムを実装してプログラムを実行するという構造をとっていました。これも残念ながら2016年2月にプロジェクトの終了が発表されました。
このように、Windowsはシステムコンポーネント次第でさまざまな役割を果たすことができるように設計・実装されてきました(ビジネス的に成功しているかとは別に、このような拡張をおこないやすい内部アーキテクチャというのが肝要です)。

補足やセルフツッコミ

以下では、もう少し書きたかったけれど紙面の都合上削ることになったものや、少々蛇足で雑誌媒体には合わずカットとなったものなどを挙げていきます。
原則的に紙面の各文言へセルフツッコミを入れていく形で書いてみます。

p.9 テキストかバイナリか的な話

でも、テキストエディターで記述したソースコードは、あくまでテキスト形式です。このままでは、コンピュータは実行できません。
EICARテストファイルというものがあります(http://www.eicar.org/86-0-Intended-use.html)。これは完全にASCIIの範囲のテキストのみで書かれた、アンチウィルスソフトの動作確認用ファイルです。
COMという(Component Object Modelでないほう)実行形式として正当なものであり、32-bit版のWindowsでは実行可能です(64-bit版のWindowsではCOMファイルを実行できないので、残念ながら最近の多くの環境では実行して試せないのですが)。こういう変わり種もありますが、バイナリ芸のエリアですね。

p.13 フレームワークのくだり

Webアプリを作成できるようなライブラリ群を指します。
もちろんWebアプリに用途を限定しているわけではありません。文の収まりの都合です。

p.13 コラム コンパイルして実行か逐次実行かというくだり

ただし、現在のスクリプト言語では、すべてを逐次実行しているわけではありません。プログラムの起動の高速化や、実行速度の改善のために様々な処理を実行前に施しています。
そういうわけで、JSがJITコンパイルの領域で世界の先端エリアまで行っていたり、PyPyがCPythonより(コード種にもよりますが)何倍も速くコードを実行できる現代に我々は住んでいます。ASP.NET Coreなどでは原則的に事前のCIL生成・アセンブリ生成を捨てて「実行時コンパイルでいいよね?」と来ているわけですし、扱い方の容易さでいうともはやほぼJSです。
個人的には、事前コンパイルをおこなって中間言語コード・アセンブリを出力しておく言語群とそれ以外との切り分けは近年ほぼ意味を成さなくなってきていると思っています。実行までにかかる計算資源をどのフェーズへ押し付けるかという判断にすぎないので(例示するとGolangにはすばらしいgo runというサブコマンドがあり、実行時に一時ディレクトリでバイナリをビルドしてから実行するスタイルもとれるわけです)。

p.13 フレームワークについて

本文のどの場所というわけではないのですが、フレームワークというフレーズは本当に界隈と人によって持つイメージが違うものです。
たとえばiOSだとずいぶん小さめの粒度でフレームワーク(framework)という言葉を使います。インポート可能なパッケージの、パッケージングフォーマットというレベルでの利用です。いわゆるシステムフレームワークはかなり大きな規模で切り分けられたものですが、ユーザが好きに作って配布できるフレームワーク(*.framework)はnpmやgemのパッケージレベルです。

p.15 コラム

「corflags.exe」です
結局本文では使いませんでしたね。dumpbin.exeで事足りるケースも多いのです。書き換えをしたい時にはcorflags.exeを利用します。

p.16 バイナリを読むのはむりという話


こういう人も居るので、世の中面白いのです。

p.18 CILディレクティブとCILオペコードのくだり

CILディレクティブは、コンパイラやランタイムによるコードの最適化に利用されます。処理内容ではありません。
最適化だけではなくてもちろん.entrypointとか、実行するコードパスを決定するのにも利用されます。

p.20 SxSマニフェストのくだり

.NETアプリのプログラムでは、後述するマニフェスト(SxSマニフェスト)もこのセクションに格納します。
SxSマニフェストは.NET固有事情によるものではないのですが、確認した限りはVSからの標準ビルドで常時出力されているのでこのように記載しました。

p.21 マニフェストたち

ややこしい話ですが、マニフェストには2種類あります。
実際は、まだあります。.NETとWindows界隈、便利な言葉ゆえ「マニフェスト」というのを多用します。

p.22 DLLへ処理を切り出す動機のくだり

DLLが多用される理由は、ライブラリによって勝手に再配布することが許されないものがあるからです。
アプリの配布サイズが増えることを許容すればバージョン差異による問題を避けられるのになんで、という話を先回りした話です。たとえばPhotoshopの内部DLLとかを勝手に同梱して配布したら猛烈に怒られますからね…。

p.22 DLLにコードを切り出す恩恵

つまり、メモリーの節約につながるのです
これは主にシステムコンポーネントの話ですね。ユーザコンポーネントだと、Chromeのように大量のプロセスを生成するプログラムでは恩恵大きいはずですが、普通はせいぜい2-3しかプロセスを立ち上げないのでメリットそこまで大きくありません(無理に切り分けるほどでもないの意)。

p.24 カーネルというかntoskrnl.exeの話

「ntoskrnl.exe」 または「ntkrnlpa.exe」
ほかにもntkrnlmp.exeやntkrpamp.exeがあります。なんと、このあたりはWikipediaに分かりやすいサポート特性リストがあります(Ntoskrnl.exe)。
ちなみに手元の64-bit Windows 10環境では ntkrnlmp.exeです。PAE不要ですしね。

p.28 CILコードからWindowsのAPI呼び出しまでの話

この処理は、CLRの内部でWindowsのMessageBoxWというAPIの呼び出しに変換されています。
CLRの内部というのは嘘ではないですが、実際にこの変換をしているのはFX側ですね。
このことはソースを参照するとわかります。この部分のコードは残念ながらGitHubのreferencesourceリポジトリには含まれないので、制限付きのほう(前述の、ソースコードを探っていくくだりを参照してください)で読むことになります。

p.31 なんでWinRTが出てきたのかという話

これらの処理を実装するために、従来のWin32 APIとは異なる新しいAPIセットが必要だったのです(図1)。そこで登場したのが、Windows 8 で導~
アプリライフサイクルが大きく違うというのもありますね。なにぶんモバイルではバッテリ消費をおさえやすい構造にする必要があったのだし。
タブレットでは、うん。その後にWinRT専用タブレットが消滅したのを見ると、WinRT専用へ振り切るのが早すぎたのは間違いありません。Win32APIの重要度というか、「デスクトップが今まで通りにあってOfficeがこれまで通りウィンドウ状態で動く」というあって然るべきUXを激変させるのがキツかったという話かなと。

p.31 2種類のWindows

と、Windows Phone向けの「Windows Phone 8」という2 種類のWindows系OSが存在しました。
もちろんCEというかWEC(Windows Embededed Compact)もね!

p.33 UWPのパッケージングの話

つまり、2 種類のバイナリファイルを生成してパッケージ内に含めることになります。
もっとも、だいたいx64も別にするので3種類ですけれどね!MIPS増えないかなー(増えない)。

0 件のコメント:

コメントを投稿