ウィンドウをきっかけに Windows の内部の仕組みを探る (後半)システムコールからカーネルデバイスドライバまで
「ウィンドウとは何なのか?」をテーマに,前回の記事までで,以下の事を学んだ。
- Windows でウィンドウを表示するための手段はたくさんあるが,実はどれも user32.dll を呼び出している。
- アセンブラのプログラムから直接 user32.dll を呼び出し,ウィンドウを表示する事が可能。
ここからさらに深めて,ウィンドウの正体を突き詰めてみよう。
方向性
マイクロソフト社製のDLLを逆アセンブルする事なく,DLLの機械語の内容を推測したい。
そのためには,「ウィンドウを生成する際に,DLLが実際にどんな処理をしているか」を分解して考えて,分解された単位処理ごとにそれぞれ実現方法を調べて行けばよい。
DLLもまたルーチンを実行しているに過ぎないのだから,DLLの中を見るという段階は飛ばしてもよいのだ。
(1)ウィンドウ生成とはつまり何をすることか?
前回も取り上げたMASMアセンブラの解説サイト中に,こんな一節がある。
Tutorial 3: A Simple Window
http://www.interq.or.jp/chubu/r6/masm32/tute/tute003_Jp.html
ウィンドウクラスを登録している…。
WNDCLASSEX構造体のメンバ変数のうち、最も重要なものは lpfnWndProc である。…
ウィンドウプロシジャは、ウィンドウに送られてくる全てのメッセージに対して処理を行う責務がある。
ユーザがキーボードを押したとか、マウスをクリックした、という作成したウィンドウが処理を行うべき重要なイベントを報告するため、 Windowsはメッセージをウィンドウプロシジャに送っているのである。
Windowsから送られてくるメッセージに対して、的確に反応するかどうかは、このウィンドウプロシジャ次第である。
要約するとつまり,ユーザからの操作(メッセージ)を受け付けるための準備をしている。(まだ実際に受け付け可能になってはいない。)
さらに内容は下記のように続く。
ウィンドウクラスが登録されれば、 CreateWindowEx 関数でそのウィンドウクラスの設定そのままのウィンドウを作成できる。…
そして、この CreateWindowEx 関数で作成したウィンドウは、実は自動的に表示されるわけではなく、今取得したウィンドウハンドルと、画面上にどのように表示するかを指定したものを引数として ShowWindow 関数をCALLしなければならない。
そして、次に UpdateWindow 関数で自分の作成したウィンドウのクライアントエリアを再描画する。 …
でもこれではまだ、何もできないので、 Windowsからやってくるメッセージを受け付けるようにしてあげないといけない。これは、メッセージループを作成することにより、可能となる。
ウィンドウという物の存在を,何ともわかりやすく分解して説明している文章ではないだろうか。
ウィンドウとは
- (特徴1)画面に描画された領域である。そして,
- (特徴2)メッセージの受付方法の定義である。
この2点で,すべてが説明される。
確かにウィンドウ上にはボタン・ボックス・画像など多様な要素が存在しうる。
でも,ボタンとは何か?と考えると,
- (特徴1)四角い描画領域であって,
- (特徴2)その四角い領域の上でマウスのクリックメッセージが発生すると特定のイベントを起こす
ようなコントロールの事だ,という答えになる。
「描画」と「メッセージ受付方法の定義」の2点だけで,説明が網羅されているのだ。
メッセージを受け付けた後で具体的にどういう内容のイベントが発生するか,というのは,ウィンドウ自体とはまた別の話になる。
例えばウィンドウ上でクリックしたら何かの「計算」が行なわれたという場合,計算の処理自体はコンソールアプリケーションだって同じ事を行なえるわけで,ウィンドウという切り口とは関係ない。
したがってウィンドウ生成にあたり,user32.dll ないし Windows API は,
大まかに言って上記の2つ(描画+メッセージ受理方法定義)の処理を行なっているのだ,という事がわかる。
そして,次に知りたい事は,これら2つの処理に相当する機械語は何なのかという疑問。
DLLファイルの中身も機械語である。
また Windows というOS自体が,メモリ上に読み込まれる機械語に過ぎない。
よって,つきつめて掘り下げて行けば必ずどこかで,「上記2つの処理に相当する機械語」を発見できるはずである。
そうすれば,それこそ初回の記事で述べたようにバイナリエディタだけを使って,ラッパークラスを何一つ介さない,真の意味でスクラッチから書き起こしたウィンドウ生成プログラムを作れるはずである。
この考えは正しいだろうか。
(2)機械語でグラフィック描画は可能か …CPUとVRAMとGPU
メッセージを受け付けるためには,まず受け皿となる領域(例えば四角形)が必要となる。
そこで,上記2点のうち,先に「画面上のグラフィック描画」に絞って考えてみよう。
機械語がハードディスクからメモリ上へ読み込まれた際,メモリ上のその機械語データを最終的に解釈するのはCPUだ。
ここでCPUにやらせたい事は,例えばディスプレー上の特定の画素に,特定の色で,1ドットだけグラフィック描画を行なうということ。
さて,ディスプレーが画面上に描画を実行できるのは,ディスプレーが何らかの形で電気信号を受信して制御されているからだ。
CPUは描画命令を実行するときに,ディスプレーに直に制御信号を渡すのだろうか?
現在のWindowsでは,そのような仕組みにはなっていない。
VGA(ビデオボード)の仕組みについて
http://sapporo.cool.ne.jp/tak_viper/pc/vga.html
昔のパソコン(MS-DOS 機まで)では、ビデオチップの役割は現在より低く、CPU が実際の描画計算と、ビデオメモリへのアクセスまでをこなしていました。…
ところが Windows では広い解像度と多色化が求められビデオ回路の負担が急激に増大しました。これにより、CPU は命令だけしてあとの実際の作業は専用チップに任せてしまおう、という発想が主流になっていきます。…
これがいわゆる「ビデオグラフィックアクセラレータ(略してVGA)」と呼ばれる仕組みです。
VRAM は、VGAのメモリです。…
VRAM(デュアルポート RAM)はその名の通り、2つのポートを持っています。つまり入口と出口が分離している駐車場のイメージです。
そのため、読み出し要求と書き込み要求が重なっても、それぞれが独立しているので待たされることがありません。…
RAMDAC はVGAメモリ(VRAM)から読み出された信号をアナログ信号に変換してディスプレイに送り込む役目を果たしています。
上記にある通り,ディスプレーのために専用のメモリ領域としてビデオメモリ(VRAM)が存在し,VRAM上のデータがディスプレーに渡される。
では,アセンブラを使って直接VRAM上にデータを書き込むことは可能なのか。
[low] VRAMにアセンブラで直書き
http://d.hatena.ne.jp/rudeboyjet/20080108/p1今日やってみたのがBIOSを使わずVRAM(メモリ上のディスプレイ領域)に直書きしてみること。
ソースはこんな感じ。
〜〜(アセンブラのソース)
これを見ると一見,アセンブラのmov命令を使ってVRAMに直接書き込めるように思える。
しかし,実は上記はエミュレータ上での動作であって,実環境ではない。
実環境のPCでは,CPUから直接VRAMに書き込むことはできず,
- GDI(Graphics Device Interface)
- バッファのようなもの
- GPU(Graphics Processing Unit)
- 描画専用のプロセッサ
のどちらかを介することになる。
流れは下記のよう。
- メモリ上で描画に関係した機械語命令が発生。
- CPUは描画データを…
- 処理したうえで,GDI経由でデバイスコンテキストに描画し,その内容がゆっくりとVRAMに転送される。
- 処理せずに,DirectX経由でGPUに渡してGPUに描画処理させ,その内容が高速にVRAMに転送される。
- RAMDAC等がVRAMの内容をディスプレーに信号として転送。
- ディスプレーは信号を受け取り画面上に描画。
参考URL:GDIを使う場合
GDI利用時はCPUに負荷がかかり,VRAM転送も遅い
http://d.hatena.ne.jp/NyaRuRu/20080701/p1
Web ブラウザや Flash は,Windows Vista 登場以前から,クライアント領域全体のイメージをソフトウェアで描画していました.
描画処理は CPU - メインメモリ間で完結しており,当然ながらグラフィックス性能は CPU クロックに比例します.
VRAM 転送が発生するのは,クライアント領域の内容が完成し,実際に画面に表示する最後の部分だけです.
GDIはuser32.dllよりも深い階層にある
http://ja.wikipedia.org/wiki/Graphics_Device_Interface
GDIは直線や曲線の描画、フォントのレンダリング、パレットの制御といったタスクを担当する。
ウィンドウやメニューなどのような描画については直接関わらず、GDIの上位に構築される「user32.dll内のユーザーサブシステム」に任される。
デバイスコンテキストはGDIの作業空間である
http://hiroba.chintai.net/qa2932175.html
複雑な描画を行うときに
実際に描画を行いたいデバイスコンテキストにちまちま描画していくと
ちらつきが出るなどの問題が発生することがあります。そこで、描画データを裏で組み立てておいて、完成したら一気に転送するということをしたりします。
そのための作業空間がメモリデバイスコンテキストです。
http://m--takahashi.com/bbs/pastlog/07100/07043.html
最終的には、すべての描画指示はドット単位のデータとしてグラフィックチップ内部に存在する
VRAMへと送り込まれます。VRAMへの到達についての違いが、DirectX, GDI, HAL, HEL
では違うわけです。
参考URL:GPUを使う場合
GPUがあるおかげでCPUの負担は減り,CPUは描画以外のことに専念できる
http://ziddy.japan.zdnet.com/qa2360231.htmlCPUは、GPUに対して、「何処から何処までの範囲にどの様な画を描け」と命令するだけです。…
CUIでは、ASCIIコードを送れば表示してくれます。これも厳密にはGPUです。
GPUがなければ、文字フォントを構成するドットを1つずつ転送しなければなりません。…CPUは、描画に労力を裂くこと無く、演算に力を発揮できるのです。
アセンブラで直接VRAMに書き込んで画面制御をしたい
http://questionbox.jp.msn.com/qa3470560.html現在のWindowsアプリであれば、直接VRAMに書き込むということは出来ません。
(ドライバ以外はハードウェアに対する直接アクセスは許されていないのです)
なお,Windows VistaのAeroモードの場合,GPU+VRAMの役割はさらに増える。
従来はゲーム時などにしか使われていなかったDirectXの手法が,全ウィンドウの描画に利用されるようになったのだ。
(ただし,Aeroを使っている人がどれだけいるか,甚だ不明だが・・・)
Vistaの地平 第5回 Vistaのハードウェア要件:改良されたVistaの描画アーキテクチャ
http://www.atmarkit.co.jp/fwin2k/vista_feature/05hardware/05hardware_03.htmlVRAMの中には、実際の表示スクリーンに相当するメモリ領域があり、そこにデータを描くと、その内容がそのままディスプレイに表示される。
従来のウィンドウ・システムでは,ウィンドウが動いたり、上下関係が変わるたびにリペイント・イベントが発生する。…
VistaのAEROでは,すべてのアプリケーションがまずVRAM上にいったんウィンドウ・イメージを構築し、それを最終的な表示画面領域へ転送する。
アプリケーションはVRAM上の決まったバッファ領域に描くだけなので、常にウィンドウ全体の内容を描くことができる。
隠されることがないので、部分的なリペイント領域を計算する必要もない。これがVistaで大量のグラフィックスVRAM(とGPU機能)を要求する理由である。アプリケーションを同時にたくさん開くためには、大量のVRAM が必要となる。
MicrosoftがLonghornの3Dユーザーインターフェイスを明らかに〜DirectX9 GPUが必須(2003年)
http://pc.watch.impress.co.jp/docs/2003/0509/kaigai01.htmWindowsのGUIはGPUの進化を全く活かしてこなかった。通常のアプリケーションが使うのは、旧態依然とした2DグラフィックスAPI「GDI」で、Windows XPになり見かけは変わっても基本的なシステムは変わっていなかった。
だが、Longhornでは最新GPUの機能を活かしたGUIへと変わる。そのため、Longhornの新UIのフル機能を使うためには、DirectX9クラスGPUが必要となる。
Vista買うのはまだ早い(2007年)
http://www.4gamer.net/specials/tooearlytogetvista/001/tooearlytogetvista_003.shtmlGDIがサポートするのは,描画領域の転送(=領域のコピー)や直線,円弧,文字の描画など,非常に基礎的な2Dグラフィックスの描画機能に限られている。
一方,Direct3Dでは,それこそ先ほど説明したアルファブレンディングを行ったり,あるいはハードウェアの性能をフルに生かした3Dグラフィックスの描画を行ったりできる。
そして,これらの描画にはグラフィックスカードの高速なハードウェアアクセラレーションを利用できるので,処理も速い。
ここまででわかるのは,要するにCPUはディスプレーに直に命令を出すのではないということ。
CPUが描画作業を委託するのは,GPUないしGDIである。
GPUを考えようとすると,OS内部ではなく,ビデオチップ(VGA)のほうに話がそれてしまう。
そこで次は,GPUではなく,よりCPUに近いGDIの場合を例にとって,
- CPUがどういう環境下でGDIに命令を出しているのか?
を見てみよう。
(3)GDI
DLLやAPIよりも深い部分で,どうやって描画処理がなされているのかを知るためには,OSの2つのモードを知る必要がある。
前項に出てきた一つの重要な図を見返してみる。
描画アーキテクチャを表した図
http://www.atmarkit.co.jp/fwin2k/vista_feature/05hardware/arch01.gif※元ページ:
http://www.atmarkit.co.jp/fwin2k/vista_feature/05hardware/05hardware_03.html)
元ページには,図の解説として,
「GDIとDirectXという2種類のAPIに対して、それぞれデバイス・ドライバが別々に用意されている。」
とある。
そしてそのドライバが動作している領域の端には「ユーザモード・カーネルモード 境界」と書いてある。
カーネルモードとは,すべてのハードウエアとメモリーに直接アクセスできる高い特権を持つ実行モードのこと。
描画命令を受け取って,VRAMにアクセスするGDI本体のプログラム(ドライバ)も,このカーネルモードで動作している。
カーネルモードのプログラムがPC上でどれくらい動作しているのかについては,下記サイトを参考にしてタスクマネージャから確認できる。
カーネル・モードとユーザー・モードの負荷状況を簡単に見分ける方法
http://www.atmarkit.co.jp/fwin2k/win2ktips/029overkernel/overkernel.htmlユーザー・モードで実行されるアプリケーションは、カーネル・モードが管理するメモリ資源などには直接にはアクセスできない。
このような制限を加えることで、万一ユーザー・モードで実行中のアプリケーションが暴走したとしても、システムには影響が及ばないようにしているわけだ。
なぜNT系OSは堅牢といわれるのか?:システム・コードをすべて保護するNT系
http://itpro.nikkeibp.co.jp/article/COLUMN/20080929/315586/?ST=win&P=2
Windows NT系では,「User」「GDI」「Kernel」などのシステム・コードが,カーネル・モードで動作するようになっている。Userモジュールは,ウインドウやダイアログの表示など,主にユーザー・インターフェースをアプリケーションに提供するものだ。GDIモジュールはグラフィックス描画を提供する。Kernelモジュールは,メモリーやプロセス管理などOSの基本機能を提供する。
Insider's Eye: Windows 2000のデバイス ドライバを探る
第2回 WDMは何が変わったのか(1/2)
http://www.atmarkit.co.jp/fwin2k/insiderseye/watchdd002/whatwdm1-1.htmlWindows NT 4.0は、図3のようにディスプレイドライバやGDIといったコンポーネントをカーネル モードへと移した。
これにより、Windows NT 4.0は限定的ながらもDirectXをサポートすることが可能になった。デバイスドライバがカーネル モードへ移行したことは、ドライバのエラーがOSにとって致命傷となるケースが増えるということでもある。
これを防止するためWindows 2000では、「ドライバの署名(Driver Signing)」という機能が提供される。
一般のプログラミングにおいては,そこで生成されるプログラムは,ユーザモードで動作するアプリケーションである。
しかし,VRAMやディスプレーなどのハードウェアにアクセスするようなプログラムは,カーネルモードで動作する特別なアプリケーションでなければならない。
そしてそういった特別な動作モードのアプリケーションとして筆頭に挙げられるのが,ハードウェアのデバイスドライバというわけだ。
2つのモードの間には明確に線が引かれており,ユーザモードからカーネルモードへは干渉できないように,OSによりしっかりガードされている。
そこで次は,いきなり話をカーネルモード自体に移すのではなく,先に
- 「ユーザモードとカーネルモードはどのように連携しあっているのか?」
を考える。
(4)Windowsサブシステム
ユーザモードとカーネルモードの連携については下記ページが詳しい。
Windowsのユーザー・モードとカーネル・モードの図
http://itpro.nikkeibp.co.jp/article/Windows/20051111/224393/zu02.html?ST=win
分離されているはずの2つのモードはどこでつながるのか?
http://itpro.nikkeibp.co.jp/article/Windows/20051111/224393/ユーザー・モード側はアプリケーションが動作するモードである。
ここで注目するのは,Windowsサブシステムと呼ばれるブロックだ。実はこのブロックには,Windows API(アプリケーション・プログラミング・インターフェース)が実装されている。
Windowsサブシステムこそが,ユーザー・モードとカーネル・モードを結びつける鍵なのである。
Windowsサブシステムの主要な実体は,CSRSS.EXEと言うプログラムである。重要なのはこれが,Kernel32.dll,User32.dll,Gdi32.dllなどのDLLを管理していることだ。
…このDLLはいわゆる「Windows API」である。
タスクマネージャを開き,CSRSS.EXE (Client Server Runtime Subsystem)というプロセスが確かに動作している事を確かめてみよう。
これがWindowsサブシステムであり,プロセスの生成消滅の管理を行なう傍ら,「ユーザモードのアプリケーション」と「カーネルモードのデバイスドライバ」とを橋渡ししている。
(Windowsサブシステムは,環境サブシステムプロセスとも呼ばれる。)
実際の橋渡しが発生するタイミングは下記のようになる。
- (1)ユーザモードアプリケーションがWin32APIを呼び出す。
- ここで呼び出すAPIは実はすべてダミーであり,スタブDLLと呼ばれる。
- (2)スタブDLLへの呼び出しは,Windowsサブシステム(csrss.exe)への呼び出しに変換される。
- ここでWindowsサブシステムへの呼び出しを担当するのは,ローカル・プロシージャ・コール(LPC)というカーネルモードの通信サービス。
- ユーザモードアプリケーションと,Windowsサブシステムをそれぞれ仮想マシンサーバのような物に見立てて,間の通信を取り持っている。
- (3)Windowsサブシステムは,Kernel32.dll,User32.dll,Gdi32.dllといったDLLを管理しており,これらのDLLを呼び出す。
- これらのDLLはサブシステムDLLと呼ばれ,Windows APIを実装している。
- (4)サブシステムDLLは,Win32API呼び出しの内容に応じて,Ntoskrnl.exe や Win32k.sys などのカーネルモードシステムサービスを呼び出す。
- 特に Win32k.sys はカーネルモードデバイスドライバであり,ウィンドウ管理やグラフィック操作を行なう(GDIとしての機能が実装されている)。
- (5)呼び出されたカーネルモードデバイスドライバは,ウィンドウやグラフィックを操作する。
下記は参考サイト。
特に,書籍「インサイドWindows」をオンラインで読めるページが役に立つ。
Windows の主なプロセスの概説:Client Server Runtime Process
http://ittechinf.wiki.zoho.com/Windows%E3%81%AE%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%83%84%E3%83%AA%E3%83%BC.html#Windows%20SubSystemBOOK Preview:インサイドMicrosoft Windows 第4版 上:第2章 システムアーキテクチャ
http://www.atmarkit.co.jp/fwin2k/bookpreview/insidewin4/insidewin4_04.html
これで,グラフィック描画やウィンドウ生成などのコアな機能について下記の事がわかる。
- それらの機能の直接の呼び出しは,OS中の特別な動作モードによってガードされている。
- 通常の動作モードからの間接的な呼び出しは,Windowsサブシステムという窓口を介して[]行われる。
一般ユーザがユーザモード側にいる以上,純粋に直接的な仕方で機械語からウィンドウ生成を行なうような事はできない,という事だ。
前回までに見てきたように,ユーザモード側に公開されている user32.dll から CreateWindow 関数を呼び出す,という方法を取るのが限界。
しかもそのDLLは直接呼び出せているわけではなく,実際には スタブDLL→LPC→Windowsサブシステム という経路を経た上で呼び出される。
OSを安全に使っている以上,この辺で深追いをあきらめざるを得ない。
(5)デバイスドライバ
しかしどうしてもカーネルモードの世界に行きたい,という場合,デバイスドライバのプログラミングに足を突っ込むという選択肢がある。
そこは.exeを作って満足できる世界ではなく,.sys を作る世界であり,ブルースクリーンやマシン再起不能が日常茶飯事となる。
デバイスドライバを作るために必要なフレームワークは,マイクロソフトから提供されている。
Windows Driver Kit (WDK) と Windows Logo Kit (WLK) のダウンロード
http://www.microsoft.com/japan/whdc/devtools/wdk/WDKpkg.mspx
コーディングのために必要な作り方の情報源もわずかながらある。
Windows Device Driver Programming Part 1
http://ruffnex.oc.to/kenji/windriver/デバイスドライバはカーネルモードで動作するためWindowsに致命的なダメージを与える可能性があります。
今回、私はWindowsXP + WinXPDDKで解説していますが、ブルースクリーンなんて当たり前です。日常茶飯事です。なので、場合によっては「Windowsが起動しなくなりました」ということが有り得るかもしれません。…さて、とりあえず実行したいわけですが、ドライバは通常Windows起動時にWindows本体に組み込まれます。
ドライバの中にはOSの boot時に実行されるものやシステム初期化時に実行されるものなど、もはやOSの一部となるような動作をするのが当たり前というか、そういう処理が必要なソフトウェアがそもそもドライバを利用するわけです。なので、再起動が日常茶飯事になります。
KENJI'S HOMEPAGE
http://ruffnex.oc.to/kenji/
情報量は少ないし危険も大きい。
よほどのチャレンジの余地があるか,ハードウェアのメーカに勤めているのでもない限り,手を出すのははばかられる。
まとめ
しかしとりあえず,元の「ウィンドウとは何か?」というテーマについて,下記の事が分った。
- ウィンドウ生成処理はサブシステムDLLに実装されており,そのDLLへの直接のアクセスはカーネルモードでないとできない。
- 実際のグラフィック操作はカーネルモードデバイスドライバが実行する。
- カーネルモードで動作するアプリケーション(もしくはサービス)を作るには,デバイスドライバの形でプログラムを組むという手がある。
今後はウィンドウを目にする度に,それを当たり前と思わずに,
「このウィンドウは
- .NETないしMFCなどのラッパクラスが
- ユーザモードの仮想マシン上でWindows API のスタブDLLを呼び出して
- その呼び出しがローカルプロシージャコールで伝送され
- Windows サブシステムがそれをキャッチしてプロセスを生み
- サブシステムDLLが呼び出され
- カーネルモードデバイスドライバのGDI機能が呼び出され
- デバイスコンテキストに書き込みが行われ
- VRAMに書き込みが行われ
- RAMDACなどがその内容を信号として転送し
- ディスプレーが信号を受け取って描画されているのだなあ
」
としみじみ感じる事ができるだろう。
関連する記事:
自作のC言語プログラムから,BIOS設定(CMOS)を読み書きする方法 (の調査ログ) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20120104/p1
コマンドプロンプトから,Win32 APIや任意のDLLを呼び出して実行しよう (コマンドプロンプトから画面キャプチャする方法の仕組みを理解) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20100804/p1
メモリの中身を読んでみよう (プロセスをダンプ+解析する方法) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20081019/1224341559
Windows XP等の,IE脆弱性の攻撃方法「ヒープ・スプレー」と「Use After Free」を,HTMLサンプルコードで理解しよう - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20140508/IEBrowserHeapSprayUseAfterFreeVulnerabilityTechnique
メモリ・CPUなどハードウェアの構成情報を,バッチで取得しよう (WSH/JScriptでWMIを使う方法) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20100906/p1
コマンドラインからマウスを操作する方法 (rundll32.exeで動くDLLの作成法) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20081117/1226943698