[Windows] SQL from PowerShell without Additional Cmdlets

ついでにもう一個。

SQL Server を操作するときには、SQL Server Management Studio や sqlcmd などの管理ツールを使うのが普通です。最近は SQL Server PowerShell もあるみたいですね。

SQL Server PowerShell Overview
http://msdn.microsoft.com/en-us/library/cc281954(v=sql.105).aspx

しかし、SQL Server 向けのモジュールを入れなくても、.NET があれば SQL クエリを発行できるので、標準の PowerShell の機能だけで SQL Server の簡単な操作ならできるはず、ということでやってみました。

ADFS や WSUS など、Windows Internal Database の環境には便利かもしれません。管理ツールを追加でインストールすることに制限がある環境には特に。

#
# PowerSQL.ps1
#

if ( $args.Count -eq 0 ) {
@"

SQL Server Command-Line Tool with PowerShell

USAGE:
  .\POWERSQL.PS1 -Tab
  .\POWERSQL.PS1 -Col <Table>
  .\POWERSQL.PS1 <Query String>

"@;
    exit 0;
}
elseif ( $args[0].ToString().ToUpper() -eq "-TAB" ) {
    $QueryStr= @"
SELECT CONCAT(s.name, ‘.’, t.name)
FROM sys.tables AS t INNER JOIN sys.schemas AS s
  ON t.schema_id = s.schema_id
"@;
}
elseif ( ($args[0].ToString().ToUpper() -eq "-COL")
         -and ($args.Count -gt 1) ) {
    $ColName= $args[1];
    $QueryStr= @"
SELECT o.name, c.name
FROM sys.columns AS c INNER JOIN sys.tables AS o
  ON o.object_id = c.object_id WHERE o.name = ‘$ColName’
"@;
}
else {
    $QueryStr= $args[0];
}

[void][Reflection.Assembly]::LoadWithPartialName("System.Data");

$ConnStr= (gwmi -Namespace root\adfs SecurityTokenService).ConfigurationDatabaseConnectionString;

$SQLConn= New-Object System.Data.SqlClient.SqlConnection($ConnStr);
$SQLConn.Open();

$SQLComm= $SQLConn.CreateCommand();
$SQLComm.CommandText= $QueryStr;

$Table= @();

@"
Connection String
—————–
$ConnStr

Query String
————
$QueryStr
"@;

$Reader= $SQLComm.ExecuteReader();
while ( $Reader.Read() ) {
    $Line= New-Object PSObject;
    for ( $i=0 ; $i -lt $Reader.FieldCount ; ++$i ) {
        Add-Member -InputObject $Line `
                    -MemberType NoteProperty `
                    -Name ("Col"+$i) `
                    -Value ($Reader.GetValue($i).ToString());
    }
    $Table+= $Line;
}

$SQLConn.Close();

$Table | ft -AutoSize;

 

もともと ADFS 環境でテストをするために作ったので、接続文字列 $ConnStr は Windows Server 2012 ADFS 専用に WMI から取ってきています。実際には以下の文字列になります。Windows Server 2008 R2 までとは、既定の名前付きパイプ名が異なることに注意。

Data Source=\\.\pipe\Microsoft##WID\tsql\query;Initial Catalog=AdfsConfiguration;Integrated Security=True

ADFS 以外のインスタンスに対する接続文字列は、以下のページをご覧下さい。

SqlConnection.ConnectionString プロパティ (System.Data.SqlClient)
http://msdn.microsoft.com/ja-jp/library/system.data.sqlclient.sqlconnection.connectionstring(v=VS.80).aspx

ExecuteReader メソッドしか実装していないので、対応している SQL 文は SELECT のみです。接続文字列を引数として取るようにして、ExecuteNonQuery メソッドも実装すればより便利になります。テーブルと列の一覧ぐらいは簡単に見れた方がいいので、カタログ ビューを使うクエリは予め実装しておきました。

出力例を示します。Windows Server 2012 ADFS での出力例です。

PS C:\mswork> .\PowerSQL.ps1

SQL Server Command-Line Tool with PowerShell

USAGE:
  .\POWERSQL.PS1 -Tab
  .\POWERSQL.PS1 -Col <Table>
  .\POWERSQL.PS1 <Query String>

PS C:\mswork> .\PowerSQL.ps1 -tab
Connection String
—————–
Data Source=\\.\pipe\Microsoft##WID\tsql\query;Initial Catalog=AdfsConfiguration;Integrated Securit
y=True

Query String
————
SELECT CONCAT(s.name, ‘.’, t.name)
FROM sys.tables AS t INNER JOIN sys.schemas AS s
  ON t.schema_id = s.schema_id

Col0
—-
IdentityServerPolicy.Policies
IdentityServerPolicy.ClaimTypes
IdentityServerPolicy.ClaimDescriptors
IdentityServerPolicy.ClaimDescriptorExtensibleProperties
IdentityServerPolicy.Scopes
IdentityServerPolicy.ScopeIdentities
IdentityServerPolicy.ScopeSigningCertificates
IdentityServerPolicy.ScopeClaimTypes
IdentityServerPolicy.ScopeContactInfoAddresses
IdentityServerPolicy.ScopePolicies
IdentityServerPolicy.ScopeExtensibleProperties
IdentityServerPolicy.Authorities
IdentityServerPolicy.AuthorityIdentities
IdentityServerPolicy.AuthorityContactInfoAddresses
IdentityServerPolicy.AuthorityExtensibleProperties
IdentityServerPolicy.AuthorityPolicies
IdentityServerPolicy.MetadataSources
IdentityServerPolicy.AuthorityArtifactResolutionServices
IdentityServerPolicy.AuthoritySamlEndpoints
IdentityServerPolicy.ScopeSamlEndpoints
IdentityServerPolicy.ScopeAssertionConsumerServices
IdentityServerPolicy.AuthorityClaimTypes
IdentityServerPolicy.ServiceSettings
IdentityServerPolicy.LeasedTasks
IdentityServerPolicy.ServiceStateSummary
IdentityServerPolicy.ServiceObjectTypeRelationships
IdentityServerPolicy.SyncProperties

PS C:\mswork> .\PowerSQL.ps1 -col SyncProperties
Connection String
—————–
Data Source=\\.\pipe\Microsoft##WID\tsql\query;Initial Catalog=AdfsConfiguration;Integrated Security=True

Query String
————
SELECT o.name, c.name
FROM sys.columns AS c INNER JOIN sys.tables AS o
  ON o.object_id = c.object_id WHERE o.name = ‘SyncProperties’

Col0           Col1
—-           —-
SyncProperties PropertyName
SyncProperties PropertyValue

PS C:\mswork> .\PowerSQL.ps1 "select * from IdentityServerPolicy.SyncProperties"
Connection String
—————–
Data Source=\\.\pipe\Microsoft##WID\tsql\query;Initial Catalog=AdfsConfiguration;Integrated Security=True

Query String
————
select * from IdentityServerPolicy.SyncProperties

Col0                            Col1
—-                            —-
LastSyncFromPrimaryComputerName
LastSyncStatus                  0
LastSyncTime                    2012/12/24 11:49:21
PollDurationInSeconds           300
PrimaryComputerName
PrimaryComputerPort             80
Role                            PrimaryComputer

PS C:\mswork>

 

sqlcmd の方がいいな・・・。(ぼそ

[Windows] OpenSSL on Windows and PowerShell Script Signing

年明けなので、軽い話題を。

EFS や IIS などの PKI 関連の検証を行なうときは、自己署名証明書を使うのが簡単です。しかし、自己署名を使う方法と、認証局に署名してもらう方法とでは微妙に異なっていることが多いので、OpenSSL で認証局を作って、そこで署名するようにしています。Windows 的には、Active Directory 証明書サービス (ADCS) を使うのが王道かもしれませんが、ADCS だと自動化され過ぎて面白くないのと、ADCS は規模が大きいので 「鶏を割くに牛刀を用い」 ているような気がして結局 OpenSSL です。Windows SDK に入っている makecert.exe を使った方が実は楽なのですが、Windows 以外でも使えるオープンな OpenSSL を覚えておいた方が後で幸せになれそうです。

MakeCert (Windows)
http://msdn.microsoft.com/en-us/library/aa386968(v=VS.85).aspx

そんなわけで、OpenSSL で認証局を作って証明書要求に署名するところまでをご紹介。環境はこれです。

  • OS: Windows 8 (x64)
  • OpenSSL 1.0.1c 10 May 2012 (64bit)
  • ActivePerl v5.14.2 built for MSWin32-x64-multi-thread

ダウンロード ページは↓です。

ActivePerl Downloads – Perl Binaries for Windows, Linux and Mac
http://www.activestate.com/activeperl/downloads

Shining Light Productions – Win32 OpenSSL
http://slproweb.com/products/Win32OpenSSL.html
→ "Win64 OpenSSL v1.0.1c" あたりをダウンロード。

ダウンロードが終わったら、両方の bin フォルダーにパスを通しておきましょう。私の環境では、バイナリ―が入っているフォルダーは以下の 2 つです。

  • C:\Perl64\bin
  • C:\OpenSSL-Win64\bin

とりあえずバージョンを表示させてみて、ごちゃごちゃ表示されれば OK です。

E:\MSWORK> perl -v

This is perl 5, version 14, subversion 2 (v5.14.2) built for MSWin32-x64-multi-thread
(with 1 registered patch, see perl -V for more detail)

Copyright 1987-2011, Larry Wall

Binary build 1402 [295342] provided by ActiveState http://www.ActiveState.com
Built Oct  7 2011 15:19:36

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using "man perl" or "perldoc perl".  If you have access to the
Internet, point your browser at
http://www.perl.org/, the Perl Home Page.

E:\MSWORK> openssl version
OpenSSL 1.0.1c 10 May 2012

 

1. 認証局を立てる

認証局関連の作業用フォルダーを作ります。どこでもいいです。ここでは C:\Openssl-Win64\work を使います。

C:\OpenSSL-Win64> md work

C:\OpenSSL-Win64> cd work

C:\OpenSSL-Win64\work>

CA.pl と openssl.cfg ファイルを bin フォルダーからコピーします。

C:\OpenSSL-Win64\work> copy ..\bin\CA.pl .\
        1 file(s) copied.

C:\OpenSSL-Win64\work> copy ..\bin\openssl.cfg .\
        1 file(s) copied.

CA.pl を開いて、以下の一行を変更します。先ほどコピーした openssl.cfg を読むように指定します。CA.pl をそのままにして環境変数 SSLEAY_CONFIG を使ってもいいのですが、環境変数を増やしたくないので・・・。

#$SSLEAY_CONFIG=$ENV{"SSLEAY_CONFIG"};
$SSLEAY_CONFIG="-config C:\\OpenSSL-Win64\\work\\openssl.cfg";

openssl.cfg も変更します。
まず policy_match セクション。"match" の部分を全て optional にしておきます。

[ policy_match ]
#countryName         = match
#stateOrProvinceName = match
#organizationName    = match

countryName          = optional
stateOrProvinceName  = optional
organizationName     = optional

 

次に req_distinguished_name セクション。Organization Name と Common Name 以外は全てコメント アウトします。

[ req_distinguished_name ]
#countryName         = Country Name (2 letter code)
#countryName_default = AU
#countryName_min     = 2
#countryName_max     = 2

#stateOrProvinceName         = State or Province Name (full name)
#stateOrProvinceName_default = Some-State

#localityName = Locality Name (eg, city)

0.organizationName         = Organization Name (eg, company)
0.organizationName_default = Internet Widgits Pty Ltd

# we can do this but it is not needed normally :-)
#1.organizationName         = Second Organization Name (eg, company)
#1.organizationName_default = World Wide Web Pty Ltd

#organizationalUnitName         = Organizational Unit Name (eg, section)
#organizationalUnitName_default =

commonName     = Common Name (e.g. server FQDN or YOUR name)
commonName_max = 64

#emailAddress     = Email Address
#emailAddress_max = 64

# SET-ex3 = SET extension number 3

準備が整いました。書き上げてから気づきましたが、CA で使う鍵の長さも 2048bit にしたほうがよいかもしれません。

ええい面倒だ 時すでに遅し、ということで CA.pl を使って証明機関を作成します。簡単です。
CA の名前や組織名には好きな名前を付けて下さい。以下の例では Sunakata と Sunakata Root CA です。

C:\OpenSSL-Win64\work> perl CA.pl -newca
CA certificate filename (or enter to create)
(Enter を押す)

Making CA certificate …
Loading ‘screen’ into random state – done
Generating a 1024 bit RSA private key
.++++++
…………………++++++
writing new private key to ‘./demoCA/private/cakey.pem’
Enter PEM pass phrase: (CA の秘密鍵へのアクセス パスワードを設定)
Verifying – Enter PEM pass phrase: (パスワード確認)
—–
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter ‘.’, the field will be left blank.
—–
Organization Name (eg, company) [Internet Widgits Pty Ltd]: Sunakata
Common Name (e.g. server FQDN or YOUR name) []: Sunakata Root CA

Please enter the following ‘extra’ attributes
to be sent with your certificate request

A challenge password []: (Enter を押す)
An optional company name []: (Enter を押す)

Using configuration from C:\OpenSSL-Win64\work\openssl.cfg
Loading ‘screen’ into random state – done
Enter pass phrase for ./demoCA/private/cakey.pem: (CA 秘密鍵のパスワード)

Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number:
            8c:fb:c2:57:59:f8:90:ec
        Validity
            Not Before: Jan  2 07:00:35 2013 GMT
            Not After : Jan  2 07:00:35 2016 GMT
        Subject:
            organizationName          = Sunakata
            commonName                = Sunakata Root CA
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                20:2E:79:06:73:C2:8A:BB:52:32:90:1F:E8:66:87:5B:18:E0:8F:97
            X509v3 Authority Key Identifier:
                keyid:20:2E:79:06:73:C2:8A:BB:52:32:90:1F:E8:66:87:5B:18:E0:8F:97

            X509v3 Basic Constraints:
                CA:TRUE
Certificate is to be certified until Jan  2 07:00:35 2016 GMT (1095 days)

Write out database with 1 new entries
Data Base Updated

C:\OpenSSL-Win64\work>

CA ができました。
CA.pl に定義されている名前で、勝手にフォルダー階層やファイルができます。

C:\OpenSSL-Win64\work> tree .
C:\OPENSSL-WIN64\WORK
└─demoCA
    ├─certs
    ├─crl
    ├─newcerts
    └─private

主要なファイルは以下の 2 つです。

  • ルート証明書: demoCA\cacert.pem
  • 秘密鍵: demoCA\private\cakey.pem

その他のファイルは、CA が発行した証明書を管理するデータベースなどのファイルです。証明書の失効や CRL の生成で使います。

拡張子が pem になっていますが、ルート証明書の cacert.pem は Windows でいうところの拡張子 cer と同じ X.509 証明書なので、以下のコマンドで開くことができます。もちろん、拡張子を cer に変えたり、関連付けを作成しても構いません。

> rundll32 cryptext.dll,CryptExtOpenCER demoCA\cacert.pem

お馴染みの、以下のダイアログが開きます。

image image

ルート証明書は、検証環境の [信頼されたルート証明機関] の証明書ストアに入れておきましょう。

2. 証明書要求を作って署名する

証明書要求は何でもいいのですが、多少は現代風の技術を取り入れるという趣旨のもと、PowerShell スクリプト (拡張子 ps1) にデジタル署名を追加するための証明書で試してみます。PowerShell なんて知らん、では済まない時代になってきました。嫌がらずに使って慣れていきましょう。

適当な環境で、スクリプトの実行ポリシーを AllSigned にします。これも Windows 8 の環境ですが Windows 7 でも変わらないはずです。

PS E:\mswork> Set-ExecutionPolicy AllSigned

Execution Policy Change
The execution policy helps protect you from scripts that you do not trust. Changing the execution
policy might expose you to the security risks described in the about_Execution_Policies help topic
at
http://go.microsoft.com/fwlink/?LinkID=135170. Do you want to change the execution policy?
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"): y

PS E:\mswork> Get-ExecutionPolicy
AllSigned
PS E:\mswork>

適当なスクリプト ファイルを作って、実行してみます。

PS E:\mswork> "Get-Host" | Out-File Test.ps1

PS E:\mswork> .\Test.ps1
.\Test.ps1 : File E:\mswork\Test.ps1 cannot be loaded. The file E:\mswork\Test.ps1 is not digitally
signed. The script will not execute on the system. For more information, see about_Execution_Polic
ies at
http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ .\Test.ps1
+ ~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

当然ですが、Test.ps1 がデジタル署名されていないので怒られます。

PowerShell スクリプトの署名には、拡張属性の extendedKeyUsage (OID=2.5.29.37) に codeSigning (OID=1.3.6.1.5.5.7.3.3) が設定されている証明書が必要です。

http://www.oid-info.com/get/2.5.29.37
http://www.oid-info.com/get/1.3.6.1.5.5.7.3.3

どんな方法でもいいのですが、今回は certreq で証明書要求を作って Sunakata Root CA で署名します。

まず、certreq で要求を作るための設定ファイルを用意します。設定項目は以下のページを見て下さい。

Appendix 3: Certreq.exe Syntax
http://technet.microsoft.com/en-us/library/cc736326.aspx

;
; policy.inf
;
;
http://technet.microsoft.com/en-us/library/cc736326.aspx
;

[NewRequest]
Subject="CN=PowerShell Script Signing"
Exportable=TRUE
KeyLength=2048

[Extensions]
;
http://msdn.microsoft.com/en-us/library/windows/desktop/aa379367(v=vs.85).aspx
2.5.29.37 = "{text}1.3.6.1.5.5.7.3.3"

certreq で証明書要求を作ります。

> certreq -new policy.inf ps1sign.req

> openssl req -in ps1sign.req -text
Certificate Request:
    Data:
        Version: 0 (0×0)
        Subject: CN=PowerShell Script Signing
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:dd:13:1c:85:ba:47:6c:ab:7b:8c:01:4f:78:bd:
                    82:72:30:32:13:c7:9c:0f:69:e7:fc:d9:e4:d8:21:
                    be:ef:1b:a2:50:0e:8d:14:56:11:f0:f4:d6:1f:3f:
                    bf:48:ef:c4:01:58:71:4e:e4:16:dd:b5:4e:eb:3b:
                    e9:6e:82:99:09:29:bf:1e:91:d7:b2:e9:f6:0a:15:
                    48:7a:a1:b6:44:04:64:11:ac:cf:ca:2e:6b:d9:b5:
                    a9:1b:91:22:07:67:fc:8c:67:51:fd:dd:af:63:96:
                    bf:36:62:6a:69:27:f3:02:8e:66:cd:8f:30:ca:98:
                    ee:e7:72:7d:a0:1e:02:0b:a7:52:3f:5d:9e:a5:e4:
                    a7:ec:87:67:fb:51:b5:05:31:97:fc:71:cf:11:f5:
                    b2:5a:77:d9:e4:e7:0e:43:bc:b9:b6:17:f8:40:2e:
                    d6:fb:2a:2b:f1:32:c9:db:60:07:b1:c5:89:93:36:
                    23:f9:25:5c:f8:14:60:47:cc:bc:56:42:63:1f:e6:
                    84:9e:43:34:4c:fd:36:df:66:eb:17:6b:50:e9:79:
                    c0:80:95:05:f0:8e:ca:d3:9b:56:35:12:97:8d:65:
                    5d:d0:4a:2f:e4:af:25:1b:b7:b5:0d:e4:af:5c:30:
                    ae:af:63:0d:74:83:d6:ce:21:0b:47:58:91:88:9e:
                    3a:01
                Exponent: 65537 (0×10001)
        Attributes:
            1.3.6.1.4.1.311.13.2.3   :6.2.9200.2
            1.3.6.1.4.1.311.21.20    :unable to print attribute
            1.3.6.1.4.1.311.13.2.2   :unable to print attribute
        Requested Extensions:
            X509v3 Extended Key Usage:
                Code Signing
            X509v3 Subject Key Identifier:
                74:42:81:6D:9D:64:7A:10:4D:B3:1F:EA:05:85:5B:02:5B:43:62:AA
            X509v3 Key Usage: critical
                Digital Signature

証明書要求ファイル ps1sign.req とともに、証明書ストアの [証明書の登録要求] に秘密鍵が保存されます。

image

次に、作成した証明書要求に署名を行ないます。

今回は、拡張属性に codeSigning を付けるため、設定ファイル openssl.cfg を編集しないといけません。このへんがちょっと面倒です。openssl.cfg の末尾に、以下の 2 行を追加します。これを忘れると、証明書要求に codeSigning が設定されていても、署名された証明書には拡張属性が設定されません。もっとうまいやり方がありそうなんですがね・・・。

[ v3_extensions ]
extendedKeyUsage=codeSigning

あとは簡単です。

C:\OpenSSL-Win64\work> openssl ca -in ps1sign.req -outdir . -out ps1sign.cer -config openssl.cfg -extensions v3_extensions
Using configuration from openssl.cfg
Loading ‘screen’ into random state – done
Enter pass phrase for ./demoCA/private/cakey.pem: (CA 秘密鍵のパスワード)
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number:
            8c:fb:c2:57:59:f8:90:f0
        Validity
            Not Before: Jan  2 09:02:29 2013 GMT
            Not After : Jan  2 09:02:29 2014 GMT
        Subject:
            commonName                = PowerShell Script Signing
        X509v3 extensions:
            X509v3 Extended Key Usage:
                Code Signing
Certificate is to be certified until Jan  2 09:02:29 2014 GMT (365 days)
Sign the certificate? [y/n]: y

1 out of 1 certificate requests certified, commit? [y/n] y
Write out database with 1 new entries
Data Base Updated

署名した証明書を、再び certreq を使ってインストールします。

C:\OpenSSL-Win64\work> certreq -accept ps1sign.cer

これで、個人証明書ストアに秘密鍵つきの証明書が保存されました。

image image image

あとは簡単で、Set-AuthenticodeSignature を使って署名するだけです。

PowerShell ドライブの Cert: を使うと証明書ストアをブラウズできるので、作った証明書を探します。

PS E:\mswork> $Certs= gci Cert:\CurrentUser\My
PS E:\mswork> $Certs

    Directory: Microsoft.PowerShell.Security\Certificate::CurrentUser\My

Thumbprint                                Subject
———-                                ——-
5ACA55270D5F82044FDCA822AFE4BACEA33515EA  CN=zune-tuner://windowsphone/39d1483b%20-%20e7c93dac%2…
4EEF76D8ADEE8A60D166A4B57E84D9418D3C0BD1  CN=PowerShell Script Signing
3F3675A193F4750ADF7DFC856EAB17F56AE3B35C  CN=Windows Azure Tools

2 番目にあるので $Certs[1] で署名。

PS > Set-AuthenticodeSignature -Certificate $Certs[1] -FilePath .\Test.ps1

    Directory: E:\mswork

SignerCertificate                         Status                       Path
—————–                         ——                       —-
4EEF76D8ADEE8A60D166A4B57E84D9418D3C0BD1  Valid                        Test.ps1

内容を確認し、実行してみます。

PS E:\mswork> Get-Content .\Test.ps1
Get-Host

# SIG # Begin signature block
# MIIE4QYJKoZIhvcNAQcCoIIE0jCCBM4CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
(中略)
# HxsuGy1BhAOXwggkVgrQYUPOclzgxwGPMxicMp+6303y6Hslp1vDZkhgn4x/ppys
# F6B9vww=
# SIG # End signature block

PS E:\mswork> Get-ExecutionPolicy
AllSigned

PS E:\mswork> .\Test.ps1

Do you want to run software from this untrusted publisher?
File E:\mswork\Test.ps1 is published by CN=PowerShell Script Signing and is not trusted on your
system. Only run scripts from trusted publishers.
[V] Never run  [D] Do not run  [R] Run once  [A] Always run  [?] Help (default is "D"): r

Name             : ConsoleHost
Version          : 3.0
InstanceId       : add59fda-6386-4ea6-850d-b7f3943673ea
UI               : System.Management.Automation.Internal.Host.InternalHostUserInterface
CurrentCulture   : ja-JP
CurrentUICulture : en-US
PrivateData      : Microsoft.PowerShell.ConsoleHost+ConsoleColorProxy
IsRunspacePushed : False
Runspace         : System.Management.Automation.Runspaces.LocalRunspace

実行時の "信頼されていない発行元からのソフトウェアを実行しますか?" という警告が気になる場合は、署名に利用した証明書を個人証明書ストアから [信頼された発行元] の証明書ストアに入れておけば、警告が出なくなります。

[.NET] [Asm] Managed Code Debugging with SOS extension

久々の更新です。ようやく .NET デバッグが実戦でも通用するレベルになってきたので、初歩を紹介します。もちろん .NET デバッグといっても Visual Studio を使うのではなく、ntsd やら windbg といった Windows デバッガーを使います。Windows デバッガーの利点としては、稼働環境へファイルをコピーするだけでいい、リモート デバッグが可能、Visual Studio より細かいことができる、などが挙げられます。コンピューターの動作を理解するのにも役立ちますし、慣れてくると Visual Studio より速くデバッグできるようになります。それと、デバッガーの黒い画面を開いて仕事をしていると、周りから見ても 「仕事をしている感」 が醸し出されて便利です。(なんだそれは

ただし、まだ .NET Framework の動きはほとんど理解しきれていないので、細かい説明は端折ります。そのうち覚えます。

.NET のデバッグを行うためには、SOS と呼ばれるデバッガー エクステンション DLL が必要になります。ただし、これは .NET Framework に含まれているので、別途ダウンロードする必要はありません。

%windir%\Microsoft.NET\Framework\<.NET バージョン>\SOS.dll
%windir%\Microsoft.NET\Framework64\<.NET バージョン>\SOS.dll

SOS とは、"Son Of Strike" の略です。じゃあ Strike って何ぞ、という話ですが、これは以下のブログにそのエピソードが詳細に書かれています。もともと CLR 開発チームが "Lightning" という名前で作っていたものを、デバッガー エクステンションにしたときに "Strike" という名前に変えて、そこから一部のコードを取り除いたものだから "Son Of Strike" だそうです。

この記事には、そんな小話だけでなく SOS に関する非常に詳細な説明が書かれています。

SOS Debugging of the CLR, Part 1 – Jason Zander’s blog – Site Home – MSDN Blogs
http://blogs.msdn.com/b/jasonz/archive/2003/10/21/53581.aspx

MSDN だとこんなページもあります。

SOS.dll (SOS Debugging Extension)
http://msdn.microsoft.com/en-us/library/bb190764.aspx

このブログでは、細かいことは抜きにして早速デバッグしてみましょう。シンボルの設定は必ず行って下さい。順番が逆ですが、デバッグ環境の作り方をそのうち記事にするかもしれません。

Use the Microsoft Symbol Server to obtain debug symbol files
http://support.microsoft.com/kb/311503/en

今回の検証環境はこんな感じです。
現時点で最新の環境を使っていますが、Windows 7 でも XP でも同じことができるはずです。

  • OS: Windows Server 2012
  • CLR: 4.0.30319.18010 (.NET Framework 4.5)
  • IDE: Visual Studio 2012
  • Debugger: 6.2.9200.16384 (Windows Kit 8.0)

まずは、適当なプログラムを書きます。C# です、お決まりですね。もちろん F# でもいいです。

using System;
using System.IO;

namespace cssandbox {
    class Program {
        static void Main(string[] args) {
            var Prog = new Program();
            Prog.Print(Console.Out);
        }

        string mMessage1;
        string mMessage2;

        Program() {
            mMessage1 = "Hello!";
            Sub();
        }

        Program(string s) {
            mMessage1 = s;
            Sub();
        }

        void Sub() {
            mMessage2 = DateTime.Now.ToString();
        }

        void Print(TextWriter Writer) {
            Writer.WriteLine(mMessage1);
            Writer.WriteLine(mMessage2);
        }

    }
}

これを Debug 構成でビルドして、デバッガーから起動します。私は ntsd 派なのでこんな感じです。ntdll!LdrpDoDebuggerBreak で止まるはずで、これはネイティブ コードのデバッグと同じです。というか、まだこのタイミングでは CLR がロードされていないので、マネージド コードは存在しません。

image

Microsoft (R) Windows Debugger Version 6.2.9200.16384 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: cssandbox.exe
Symbol search path is: srv*d:\websymbols*
http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 005e0000 005e8000   cssandbox.exe
ModLoad: 77820000 77977000   ntdll.dll
ModLoad: 6f2f0000 6f33a000   C:\Windows\SysWOW64\MSCOREE.DLL
ModLoad: 77630000 77760000   C:\Windows\SysWOW64\KERNEL32.dll
ModLoad: 76f20000 76fc6000   C:\Windows\SysWOW64\KERNELBASE.dll
(9a4.b10): Break instruction exception – code 80000003 (first chance)
eax=00000000 ebx=00000003 ecx=be050000 edx=00000000 esi=00000000 edi=00000000
eip=778c054d esp=0076f864 ebp=0076f890 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
778c054d cc              int     3
0:000>

 

ここで重要なのが、CLR が動作するプラットフォームと、利用するデバッガーのプラットフォームを一致させておくことです。OS が 64 bit だったとしても、デバッグするアプリが 32bit で動作する場合は、32bit のデバッガーを使わないといけません。SOS.dll はデバッグ対象の CLR とバージョンとプラットフォームが一致していないと動かないのですが、当然 64bit のデバッガー プロセスから 32bit の SOS.dll をロードできないため、このような制限が生まれます。デバッガーを起動する前にデバッグ対象の動作プラットフォームを調べておきましょう。

SOS のロード

SOS のロードで一番単純な方法は、以下のように SOS.dll のパスを直接指定する方法です。

0:000> .load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll

32bit デバッガーから 64bit の SOS を読もうとすると、以下のように怒られます。

0:000> .load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.dll
The call to LoadLibrary(C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.dll) failed, Win32 error 0n193
    "%1 is not a valid Win32 application."
Please check your debugger configuration and/or network access.

以上のようにプラットフォームの違いは判別されますが、.NET Framework バージョンの違いは判別されないので、以下のように .NET 2.0 の SOS はロードできてしまいます。これだと後々の SOS のコマンドが正しく動きません。

0:000> .load C:\Windows\Microsoft.NET\Framework\v2.0.50727\SOS.dll

いちいち .NET Framework のバージョンを調べるのが面倒くさい、という人のために .loadby というコマンドが存在します。こんな感じに使います。

0:000> sxe ld:clr
0:000> g
ModLoad: 6d500000 6db92000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
eax=00000000 ebx=00800000 ecx=00000000 edx=00000000 esi=00000000 edi=7e60d000
eip=77860fe8 esp=0076f4e4 ebp=0076f53c iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
ntdll!NtMapViewOfSection+0xc:
77860fe8 c22800          ret     28h
0:000> .loadby sos clr

.loadby を使うと、指定したモジュールと同じところにある拡張 DLL をロードさせることができます。したがって、CLR.dll がロードされた後のタイミングで、clr.dll と同じところの SOS をロードすることで、適切なバージョンの SOS をロードすることができます。

デバッグするプログラムによっては clr で .loadby できず、代わりに mscoreei や mscorwks を使うことがあります。実現したいことは、CLR と同じ SOS をロードするだけなので、困ったら CLR のバージョンを調べて絶対パス指定で .load すれば OK です。

ブレーク ポイント

デバッグは、ブレークポイントを設定するところから始まります。そんなわけで、前述のサンプル プログラムの Main 関数で止めてみましょう。ネイティブ コードと違って、x コマンドは使えません。

単純な方法は、SOS の !name2ee コマンドを使う方法です。一気にやるとこんな感じです。

0:000> sxe ld:clrjit
0:000> g
(9a4.b10): Unknown exception – code 04242420 (first chance)
ModLoad: 6ee60000 6eece000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll
eax=00000000 ebx=00800000 ecx=00000000 edx=00000000 esi=00000000 edi=7e60d000
eip=77860fe8 esp=0076e5dc ebp=0076e634 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!NtMapViewOfSection+0xc:
77860fe8 c22800          ret     28h
0:000> !name2ee cssandbox!cssandbox.Program.Main
Module:      008e2e94
Assembly:    cssandbox.exe
Token:       06000001
MethodDesc:  008e37ac
Name:        cssandbox.Program.Main(System.String[])
Not JITTED yet. Use !bpmd -md 008e37ac to break on run.
0:000> !bpmd -md 008e37ac
MethodDesc = 008e37ac
Adding pending breakpoints…
0:000> g
(9a4.b10): CLR notification exception – code e0444143 (first chance)
JITTED cssandbox!cssandbox.Program.Main(System.String[])
Setting breakpoint: bp 00AA0077 [cssandbox.Program.Main(System.String[])]
Breakpoint 0 hit
eax=00000000 ebx=0076f31c ecx=024a22cc edx=00000000 esi=00000000 edi=0076f290
eip=00aa0077 esp=0076f264 ebp=0076f278 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
00aa0077 90              nop
0:000>

ブレークポイントを設定するのに使いたいコマンドは !bpmd です。bp コマンドは、メモリにロードされているコードのアドレスを直接指定しますが、上記の例では、!name2ee コマンドを使って取得した Method Descriptor という値を使ってブレーク ポイントを設定しています。

コマンドの出力にもありますが、.NET の大きな特徴として、実行時 (JIT) コンパイルが行われることが挙げられます。当然、まだコンパイルされていないメソッドに対して bp コマンドは使えません。そこで、Method Descriptor を間接的に使ってブレークポイントを設定するわけです。Method Descriptor や EEClass といった CLR の内部構造については、以下の記事などを参考にして下さい。まだよく知らないのですわ・・・スミマセン。

JIT and Run: .NET Framework の内部: CLR がランタイム オブジェクトを作成するしくみ — MSDN Magazine, 2005 年 5 月
 http://msdn.microsoft.com/ja-jp/magazine/ee216336.aspx

アプリケーションを起動して clr.dll がロード直後のタイミングでは、CLR の内部構造がほとんど何もできていないので、!name2ee すら実行することができず、以下のようなエラーが出ます。

0:000> !name2ee cssandbox!cssandbox.Program.Main
Failed to obtain AppDomain data.
Failed to request module list.

そこで今回は、clrjit.dll がロードされるタイミングを sxe で止めて、そのときに !name2ee を実行しました。

!bpmd した後の出力結果を見ると、JIT されたタイミングで bp コマンドが実行されているのが分かります(紫字部分)。JIT されてしまえば、ネイティブ コードと同じように扱うことができます。ただし、コンパイルされたコードはネイティブのものとは微妙に印象が異なるのが面白いところです。

ブレークポイントで止まっている状態で、以下のコマンドを実行してみます。

0:000> bl
0 e 00aa0077     0001 (0001)  0:****
0:000> !name2ee cssandbox!cssandbox.Program.Main
Module:      008e2e94
Assembly:    cssandbox.exe
Token:       06000001
MethodDesc:  008e37ac
Name:        cssandbox.Program.Main(System.String[])
JITTED Code Address: 00aa0050
0:000> r
eax=00000000 ebx=0076f31c ecx=024a22cc edx=00000000 esi=00000000 edi=0076f290
eip=00aa0077 esp=0076f264 ebp=0076f278 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
00aa0077 90              nop
0:000> k
ChildEBP RetAddr
WARNING: Frame IP not in any known module. Following frames may be wrong.
0076f278 6d502652 0xaa0077
0076f284 6d51264f clr!CallDescrWorkerInternal+0×34
0076f2d8 6d512e95 clr!CallDescrWorkerWithHandler+0x6b
0076f350 6d5c74ec clr!MethodDescCallSite::CallTargetWorker+0×152
0076f47c 6d5c7610 clr!RunMain+0x1aa
0076f6f0 6d651dc4 clr!Assembly::ExecuteMainMethod+0×124
0076fbf4 6d651e67 clr!SystemDomain::ExecuteMainMethod+0×614
0076fc50 6d651f7a clr!ExecuteEXE+0x4c
0076fc90 6d65416a clr!_CorExeMainInternal+0xdc
0076fccc 6f27f5a3 clr!_CorExeMain+0x4d
0076fd04 6f2f7efd mscoreei!_CorExeMain+0x10a
0076fd1c 6f2f4de3 MSCOREE!ShellShim__CorExeMain+0x7d
0076fd24 77658543 MSCOREE!_CorExeMain_Exported+0×8
0076fd30 7787ac69 KERNEL32!BaseThreadInitThunk+0xe
0076fd74 7787ac3c ntdll!__RtlUserThreadStart+0×72
0076fd8c 00000000 ntdll!_RtlUserThreadStart+0x1b

まず、!bpmd によって自動的に bp コマンドが実行されたので、bl で確認できます。!name2ee コマンドを実行すると、JIT されたコードが 00aa0050 にロードされていることが分かります。bp された場所と若干ずれていますね。本当は 00aa0077 ではなく 00aa0050 で止まってほしいところです。止めたいところを !name2ee で調べて、JIT されていれば、bp コマンドを手動で打つこともできます。

また、r や k といったいつものコマンドも使うことができます。ただし、0xaa0077 のアドレスなどはシンボル名で解決されていません。スタックを見ると、clr.dll から cssandbox.exe が呼ばれていることが分かります。ステップ実行もできます。

変数を見る

目的の所で止めたら、変数の値を見たくなります。頑張れば dd コマンドを使えないこともないですが、.NET オブジェクトについては、SOS のエクステンションに頼ることになります。

cssandbox.exe に適当なパラメーターを指定して起動し、args の値を見る場合の例を示します。

Microsoft (R) Windows Debugger Version 6.2.9200.16384 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: cssandbox.exe ABCD 漢字
Symbol search path is: srv*d:\websymbols*
http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00af0000 00af8000   cssandbox.exe
ModLoad: 77820000 77977000   ntdll.dll
ModLoad: 6f2f0000 6f33a000   C:\Windows\SysWOW64\MSCOREE.DLL
ModLoad: 77630000 77760000   C:\Windows\SysWOW64\KERNEL32.dll
ModLoad: 76f20000 76fc6000   C:\Windows\SysWOW64\KERNELBASE.dll
(11a4.cf0): Break instruction exception – code 80000003 (first chance)
eax=00000000 ebx=00000003 ecx=18f00000 edx=00000000 esi=00000000 edi=00000000
eip=778c054d esp=00c7f76c ebp=00c7f798 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
778c054d cc              int     3
0:000> sxe ld:clrjit
0:000> g
(11a4.cf0): Unknown exception – code 04242420 (first chance)
ModLoad: 6ee60000 6eece000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll
eax=00000000 ebx=00800000 ecx=00000000 edx=00000000 esi=00000000 edi=7ef7d000
eip=77860fe8 esp=00c7e4ec ebp=00c7e544 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!NtMapViewOfSection+0xc:
77860fe8 c22800          ret     28h
0:000> .loadby sos clr
0:000> !bpmd cssandbox.exe cssandbox.Program.Main
Found 1 methods in module 00d72e94…
MethodDesc = 00d737ac
Adding pending breakpoints…
0:000> g
(11a4.cf0): CLR notification exception – code e0444143 (first chance)
JITTED cssandbox!cssandbox.Program.Main(System.String[])
Setting breakpoint: bp 01220077 [cssandbox.Program.Main(System.String[])]
Breakpoint 0 hit
eax=00000000 ebx=00c7f22c ecx=02d122cc edx=00000000 esi=00000000 edi=00c7f1a0
eip=01220077 esp=00c7f174 ebp=00c7f188 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
01220077 90              nop
0:000> !dso
OS Thread Id: 0xcf0 (0)
ESP/REG  Object   Name
ecx      02d122cc System.Object[]    (System.String[])
00C7F184 02d122cc System.Object[]    (System.String[])
00C7F200 02d122cc System.Object[]    (System.String[])
00C7F35C 02d122cc System.Object[]    (System.String[])
00C7F394 02d122cc System.Object[]    (System.String[])
0:000> !da 02d122cc
Name:        System.String[]
MethodTable: 6c8eae88
EEClass:     6c5abb70
Size:        24(0×18) bytes
Array:       Rank 1, Number of elements 2, Type CLASS
Element Methodtable: 6c93afb0
[0] 02d122e4
[1] 02d122fc
0:000> !do 02d122e4
Name:        System.String
MethodTable: 6c93afb0
EEClass:     6c54486c
Size:        22(0×16) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorl
ib.dll
String:      ABCD
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
6c93c770  40000aa        4         System.Int32  1 instance        4 m_stringLength
6c93b9a8  40000ab        8          System.Char  1 instance       41 m_firstChar
6c93afb0  40000ac        c        System.String  0   shared   static Empty
    >> Domain:Value  010234f0:NotInit  <<
0:000> !do -nofields 02d122fc
Name:        System.String
MethodTable: 6c93afb0
EEClass:     6c54486c
Size:        18(0×12) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorl
ib.dll
String:      漢字
0:000>

Main 関数で止めた後、!dso と !do コマンドを呼び出しています。!dso は、スタックに積まれているオブジェクトの一覧を表示するもので、!do はオブジェクトを表示するコマンドです。.オブジェクトが型情報を持っているので、dt と違って型を明示する必要がなくて便利です。dynamic 型でやるとどうなるんですかね。配列の場合は !do ではなく !da を使います。

!dso の結果を見ると args の値が ecx レジスターに入っています。実際、.NET のメソッドは fastcall で呼ばれるようです。

アセンブラを見る

!bpmd を使えばメソッドの先頭で止めることができますが、メソッドの途中で止める場合には、JIT されたコードのアセンブラを見ないといけません。ネイティブと同じように u や uf コマンドを使うことができます。例えば Main メソッドのアセンブラを uf で見るとこんな感じです。

0:000> !name2ee cssandbox cssandbox.Program.Main
Module:      00d72e94
Assembly:    cssandbox.exe
Token:       06000001
MethodDesc:  00d737ac
Name:        cssandbox.Program.Main(System.String[])
JITTED Code Address: 01220050
0:000> uf 01220050
01220050 55              push    ebp
01220051 8bec            mov     ebp,esp
01220053 83ec14          sub     esp,14h
01220056 33c0            xor     eax,eax
01220058 8945f4          mov     dword ptr [ebp-0Ch],eax
0122005b 8945f0          mov     dword ptr [ebp-10h],eax
0122005e 8945ec          mov     dword ptr [ebp-14h],eax
01220061 894dfc          mov     dword ptr [ebp-4],ecx
01220064 833d6031d70000  cmp     dword ptr ds:[0D73160h],0
0122006b 7405            je      01220072

0122006d e84270576c      call    clr!JIT_DbgIsJustMyCode (6d7970b4)

01220072 33d2            xor     edx,edx
01220074 8955f8          mov     dword ptr [ebp-8],edx
01220077 90              nop
01220078 b9f037d700      mov     ecx,0D737F0h
0122007d e87e20b4ff      call    00d62100
01220082 8945f4          mov     dword ptr [ebp-0Ch],eax
01220085 8b4df4          mov     ecx,dword ptr [ebp-0Ch]
01220088 ff151038d700    call    dword ptr ds:[0D73810h]
0122008e 8b45f4          mov     eax,dword ptr [ebp-0Ch]
01220091 8945f8          mov     dword ptr [ebp-8],eax
01220094 8b45f8          mov     eax,dword ptr [ebp-8]
01220097 8945f0          mov     dword ptr [ebp-10h],eax
0122009a e875d3686b      call    mscorlib_ni+0x36d414 (6c8ad414)
0122009f 8945ec          mov     dword ptr [ebp-14h],eax
012200a2 8b4df0          mov     ecx,dword ptr [ebp-10h]
012200a5 8b55ec          mov     edx,dword ptr [ebp-14h]
012200a8 3909            cmp     dword ptr [ecx],ecx
012200aa ff15e037d700    call    dword ptr ds:[0D737E0h]
012200b0 90              nop
012200b1 90              nop
012200b2 8be5            mov     esp,ebp
012200b4 5d              pop     ebp
012200b5 c3              ret
0:000>

いやー、硬派ですね。というのも、ほとんどの call 命令のオペランドが生アドレスだからでしょうか。

実は、アセンブラを見る時も SOS に頼ることができます。それが !U です。また、プライベート シンボルがある場合には、.lines を使うことでネイティブ コードと同じように行番号を表示させることができます。こんな感じです。

0:000> .lines
Line number information will be loaded
0:000> !U .
Normal JIT generated code
cssandbox.Program.Main(System.String[])
Begin 01220050, size 66

d:\VSDev\Projects\cssandbox\Program.cs @ 11:
01220050 55              push    ebp
01220051 8bec            mov     ebp,esp
01220053 83ec14          sub     esp,14h
01220056 33c0            xor     eax,eax
01220058 8945f4          mov     dword ptr [ebp-0Ch],eax
0122005b 8945f0          mov     dword ptr [ebp-10h],eax
0122005e 8945ec          mov     dword ptr [ebp-14h],eax
01220061 894dfc          mov     dword ptr [ebp-4],ecx
01220064 833d6031d70000  cmp     dword ptr ds:[0D73160h],0
0122006b 7405            je      01220072
0122006d e84270576c      call    clr!JIT_DbgIsJustMyCode (6d7970b4)
01220072 33d2            xor     edx,edx
01220074 8955f8          mov     dword ptr [ebp-8],edx
>>> 01220077 90              nop

d:\VSDev\Projects\cssandbox\Program.cs @ 12:
01220078 b9f037d700      mov     ecx,0D737F0h (MT: cssandbox.Program)
0122007d e87e20b4ff      call    00d62100 (JitHelp: CORINFO_HELP_NEWSFAST)
01220082 8945f4          mov     dword ptr [ebp-0Ch],eax
01220085 8b4df4          mov     ecx,dword ptr [ebp-0Ch]
01220088 ff151038d700    call    dword ptr ds:[0D73810h] (cssandbox.Program..ctor(), mdToken: 06000002)
0122008e 8b45f4          mov     eax,dword ptr [ebp-0Ch]
01220091 8945f8          mov     dword ptr [ebp-8],eax

d:\VSDev\Projects\cssandbox\Program.cs @ 13:
01220094 8b45f8          mov     eax,dword ptr [ebp-8]
01220097 8945f0          mov     dword ptr [ebp-10h],eax
0122009a e875d3686b      call    mscorlib_ni+0x36d414 (6c8ad414) (System.Console.get_Out(), mdToken: 06000945)
0122009f 8945ec          mov     dword ptr [ebp-14h],eax
012200a2 8b4df0          mov     ecx,dword ptr [ebp-10h]
012200a5 8b55ec          mov     edx,dword ptr [ebp-14h]
012200a8 3909            cmp     dword ptr [ecx],ecx
012200aa ff15e037d700    call    dword ptr ds:[0D737E0h] (cssandbox.Program.Print(System.IO.TextWriter), mdToken: 06000005)
012200b0 90              nop

d:\VSDev\Projects\cssandbox\Program.cs @ 14:
012200b1 90              nop
012200b2 8be5            mov     esp,ebp
012200b4 5d              pop     ebp
012200b5 c3              ret
0:000>

かなり読みやすくなりました。

コンストラクターについて

上のアセンブラで call 命令の部分を見ると、cssandbox.Program..ctor() というメソッドを呼び出す箇所があることに気づきます。コードを見ればすぐに分かりますが、これはコンストラクターです。コンストラクターは、内部的に .ctor というメソッドとして扱われるようです。ctor の先頭のドットも含めてメソッド名なので、完全修飾名にするとドットが連続する不思議な名前になります。

当然、!name2ee で Method Descriptor を見つけることもできます。Program.Program や Program.new のような名前では検索できないことを確認して下さい。今回は 2 つのコンストラクターをオーバーロードしていますので、両方とも検出され、パラメーターの種類も出してくれます。

0:000> !name2ee cssandbox cssandbox.Program.Program
Module:      00d72e94
Assembly:    cssandbox.exe
0:000> !name2ee cssandbox cssandbox.Program.new
Module:      00d72e94
Assembly:    cssandbox.exe
0:000> !name2ee cssandbox cssandbox.Program..ctor
Module:      00d72e94
Assembly:    cssandbox.exe
Token:       06000002
MethodDesc:  00d737b8
Name:        cssandbox.Program..ctor()
Not JITTED yet. Use !bpmd -md 00d737b8 to break on run.
———————–
Token:       06000003
MethodDesc:  00d737c0
Name:        cssandbox.Program..ctor(System.String)
Not JITTED yet. Use !bpmd -md 00d737c0 to break on run.
0:000>

[linux] serial console login to Ubuntu 12.04 LTS from Windows 8 RP

Linux のマシンを立てたら、やはりシリアル コンソールを使ってみないといけません。そんなわけで、Windows 8 から Ubuntu 12.04 LTS の物理マシンに RS-232C 経由で接続してみます。

まず、シリアル ケーブルを用意します。時代錯誤な、と言われそうですが、きっとハードウェア屋の業界ではまだまだ RS-232C は現役です。でもセントロニクスはちょっと・・・いやなんでもないです。

ネットで買えばいいんですが、あえて秋葉原で買ってきました。ヒロセテクニカルの地下でゲット。

CIMG2780

計 5,000 円弱。今見たら Amazon の方が安い。ちっ。
Windows クライアント側のノート PC には外出しの COM ポートがないので、USB 変換ケーブルを使います。高い。

Windows 側の設定

せっかくなので、Windows 8 Release Preview で試します。

買ってきた ELECOM の USB to シリアル ケーブルに付属のドライバー インストール CD には Windows 98/Me/2000/XP/Vista と書いてありますが、Windows 7 と Windows 8 も 32bit 版であれば動作します。64bit のドライバーは付属されていないので動作しません。

付属 CD を使ってドライバーをインストールし、ケーブルを接続すると、ドライバーが正常にロードされて COM ポートが追加されました。

image

ドライバーは、ser2el.sys というファイルで、チップメーカーの Prolific 製です。

image image

デバイス マネージャーから、ボーレートなどの設定が可能です。[詳細設定] 画面では、COM ポート番号の指定も可能です。

image

64bit でも動作するドライバーないんかい、と探してみると、なんか見つけましたよ。あれ?

http://www.prolific.com.tw/US/ShowProduct.aspx?p_id=225&pcid=41

そんなわけで、これを 64bit の Windows 7 SP1 にて入れてみましたが、動作せず・・・。

01

04 

02 03

チップの種類が違うんですかねー、とヘルプを見ていると!

05

Please be warned that counterfeit/fake PL-2303HX Rev A (or PL-2303HXA) USB-to-Serial Controller ICs using Prolific’s trademark logo, brandname, and drivers, are being sold in the China market. Counterfeit IC products show exactly the same outside chip markings but generally are of poor quality and causes Windows driver compatibility issues (Yellow Mark Error Code 10 in Device Manager under WinXP, Vista, and 7). This warning is issued to all customers and consumers to avoid confusion and false purchase. Please purchase only from stores or vendors providing technical and RMA support.

なんと偽物!!エレコム!!!

どうせなら 64bit ドライバーもビルドしておいて欲しかった・・・。そんなわけで皆様、デバイス代はケチらないようにしましょう。

Ubuntu 側の設定

気を取り直して、次に Ubuntu 側の設定に移ります。

本家のここが参考になります。あとは Google 先生なり Bing 先生に頼る感じで。

SerialConsoleHowto – Community Ubuntu Documentation
https://help.ubuntu.com/community/SerialConsoleHowto

まずはブートローダーである GRUB の設定です。/etc/default/grub を以下のように編集します。青字が修正箇所です。
これで、VGA と COM の両方のコンソールにブート時から文字が出力されます。

# If you change this file, run ‘update-grub’ afterwards to update
# /boot/grub/grub.cfg.
# For full documentation of the options in this file, see:
#   info -f grub -n ‘Simple configuration’

GRUB_DEFAULT=0
GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash console=tty0 console=ttyS0,115200"
GRUB_CMDLINE_LINUX=""

# Uncomment to enable BadRAM filtering, modify to suit your needs
# This works with Linux (no patch required) and with any kernel that obtains
# the memory map information from GRUB (GNU Mach, kernel of FreeBSD …)
#GRUB_BADRAM="0×01234567,0xfefefefe,0x89abcdef,0xefefefef"

# Uncomment to disable graphical terminal (grub-pc only)
GRUB_TERMINAL=console
GRUB_TERMINAL=serial
GRUB_SERIAL_COMMAND="serial –speed=115200 –unit=0 –word=8 –parity=no –stop=1"

# The resolution used on graphical terminal
# note that you can use only modes which your graphic card supports via VBE
# you can see them in real GRUB with the command `vbeinfo’
#GRUB_GFXMODE=640×480

# Uncomment if you don’t want GRUB to pass "root=UUID=xxx" parameter to Linux
#GRUB_DISABLE_LINUX_UUID=true

# Uncomment to disable generation of recovery mode menu entries
#GRUB_DISABLE_RECOVERY="true"

# Uncomment to get a beep at grub start
#GRUB_INIT_TUNE="480 440 1"

変更内容を反映させます。update-grub2 というコマンドもあるようだが・・・?

$ sudo update-grub

次に、init デーモンである upstart の設定を行ないます。以下の内容で /etc/init/ttyS0.conf を新規作成します。

#
# ttyS0 – getty
#
# This service maintains a getty on ttyS0 from the point the system is
# started until it is shut down again.

start on stopped rc or RUNLEVEL=[2345]
stop on runlevel [!2345]

respawn
exec /sbin/getty -L 115200 ttyS0 vt102

ubuntu 側はこれで OK。

接続してみる

いよいよ接続です。接続ソフトは teraterm を使います。定番ですね。

以下のページからダウンロードできます。
http://sourceforge.jp/projects/ttssh2/

起動して、新しいセッションのダイアログで Serial を選択。

image

teraterm 側にもシリアル ポートの設定があるので、必ず ubuntu 側と揃えておきましょう。
今後のためにも、デバイス マネージャーのポートの設定も揃えた方がよいです。(たぶん)

image

あとは待機。

image

上の状態で、Ubuntu 側を再起動します。

お、なんか出てきた。

image

アカウントを入力して、無事 ttyS0 でログオンに成功。

image

[C++] [WMI] Windows Storage Management

CreateFIle API を使うとき、多くの場合では第一引数にファイル パスを渡してファイルを開きます。CreateFile の処理は内部的に Windows カーネルのオブジェクト マネージャーが第一引数の文字列をデバイス オブジェクトとして解釈し、I/O マネージャーが適当なドライバーにリダイレクトするという流れになっています。例えば、指定したファイルのあるボリュームが NTFS フォーマットだったら ntfs.sys が処理し、UNC パスだったら mrxsmb.sys が処理したりという感じです。厳密には誤りがあるかもしれませんが、大枠はこんな感じです。

CreateFile は、ファイル システムだけでなくデバイスへ直接アクセスすることもできます。ボリュームやディスクへのダイレクト I/O を行なえるわけです。シリアル ケーブルの通信なんかもそうですね。詳しくは MSDN で CreateFile を見て下さい。

CreateFile function
http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx

ダイレクト I/O を使うと、フォーマットされていないボリュームや、パーティショニングされていないディスクに対して読み書きが可能になります。いわゆる RAW ディスクというやつで、ファイル システム ドライバーを必要としないわけです。この場合、上記 MSDN ページに書いてありますが CreateFile の第一引数に \\.\C: とか \\.\PhysicalDrive0 というような文字列を渡します。前者がボリューム デバイスを開く場合で、後者がディスク デバイスを開く場合です。

この文字列は、デバイス オブジェクトを直接示しているのではなく、デバイス オブジェクトへのシンボリック リンクの名前を示しています。デバイス オブジェクトについては、お馴染み Winobj で見ることができます。

image

Winobj
http://technet.microsoft.com/en-us/sysinternals/bb896657

上の画面だと、例えば以下のようなシンボリック リンクを確認できます。

\GLOBAL??\C: ⇒ \Device\HarddiskVolume1
\GLOBAL??\PhysicalDrive0 ⇒ \Device\Harddisk0\DR0

\GLOBAL?? というのは、オブジェクト マネージャーの解釈する名前空間です。が、Windows API はこの名前空間を解釈することができません。このため、\\.\ という接頭辞をつけるルールになっていて、CreateFile はこれを \GLOBAL?? という名前空間に変換してオブジェクト マネージャーに渡します。上の例で行けば、以下のような流れがあるわけです。

\\.\PhysicalDrive0 ⇒ \GLOBAL??\PhysicalDrive0 ⇒ \Device\Harddisk0\DR0

オブジェクト マネージャーが管理するオブジェクトについては、カーネル デバッガーの !object コマンドでも確認可能です。上記画面キャプチャと同じ環境でのカーネル デバッガーの出力を抜粋します。OS は Windows 7 SP1 64bit です。

まずは C: について。シンボリック リンクが HarddiskVolume0 を指していて、HarddiskVolume0 は volmgr.sys のデバイス オブジェクトであることが分かります。

0: kd> !driveinfo c:
Drive c:, DriveObject fffff8a00029c420
Directory Object: fffff8a000008060 Name: C:
Target String is ‘\Device\HarddiskVolume1′
Drive Letter Index is 3 (C:)
Volume DevObj: fffffa8007cc39a0
Vpb: fffffa8007cc0820 DeviceObject: fffffa8007fd0030
FileSystem: \FileSystem\Ntfs
*************************************************************************
*** ***
*** ***
*** Your debugger is not using the correct symbols ***
*** ***
*** In order for this command to work properly, your symbol path ***
*** must point to .pdb files that have full type information. ***
*** ***
*** Certain .pdb files (such as the public OS symbols) do not ***
*** contain the required information. Contact the group that ***
*** provided you with these symbols if you need this command to ***
*** work. ***
*** ***
*** Type referenced: ntfs!VOLUME_DEVICE_OBJECT ***
*** ***
*************************************************************************
Cannot get ntfs!VOLUME_DEVICE_OBJECT.Vcb @ fffffa8007fd0030

0: kd> !object \GLOBAL??\C:
Object: fffff8a00029c420  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a00029c3f0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a000008060  Name: C:
    Target String is ‘\Device\HarddiskVolume1′
    Drive Letter Index is 3 (C:)

0: kd> !object \device\harddisk1\partition1
Object: fffff8a00029cd10  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a00029cce0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a0001f8060  Name: Partition1
    Target String is ‘\Device\HarddiskVolume1′

0: kd> !object \Device\HarddiskVolume1
Object: fffffa8007cc39a0  Type: (fffffa8006d32c90) Device
    ObjectHeader: fffffa8007cc3970 (new version)
    HandleCount: 0  PointerCount: 9
    Directory Object: fffff8a000010920  Name: HarddiskVolume1

0: kd> !devobj fffffa8007cc39a0
Device object (fffffa8007cc39a0) is for:
HarddiskVolume1*** ERROR: Module load completed but symbols could not be loaded for spgu.sys
\Driver\volmgr DriverObject fffffa8007b617c0
Current Irp 00000000 RefCount 30480 Type 00000007 Flags 00201150
Vpb fffffa8007cc0820 Dacl fffff9a10033f0d0 DevExt fffffa8007cc3af0 DevObjExt fffffa8007cc3c58 Dope fffffa8007cc4820 DevNode fffffa8007ccda90
ExtensionFlags (0×00000800)
                             Unknown flags 0×00000800
AttachedDevice (Upper) fffffa8007ccea40 \Driver\fvevol
Device queue is not busy.

次に PhysicalDrive0 について。
PhysicalDrive0 が \Device\Harddisk0\DR0 という disk.sys のデバイス オブジェクトにリンクしていることが分かります。この DR0 というのがディスクのデバイス オブジェクトになるわけですが、NT4 の命名規約との互換性のためか、\Device\Harddisk0\Partition0 というシンボリック リンクも DR0 にリンクしています。このため、パーティション番号は 1 から始まることになります。DR が何の略なのかちょっと調べましたが出てきませんでした。知っている人教えて下さいー。

0: kd> !object \GLOBAL??\PhysicalDrive0
Object: fffff8a000154fe0  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a000154fb0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a000008060  Name: PhysicalDrive0
    Target String is ‘\Device\Harddisk0\DR0′

0: kd> !object \Device\Harddisk0\DR0
Object: fffffa8007e7c060  Type: (fffffa8006d32c90) Device
    ObjectHeader: fffffa8007e7c030 (new version)
    HandleCount: 0  PointerCount: 4
    Directory Object: fffff8a0001f8eb0  Name: DR0

0: kd> !devobj fffffa8007e7c060
Device object (fffffa8007e7c060) is for:
DR0 \Driver\Disk DriverObject fffffa8007cbce70
Current Irp 00000000 RefCount 0 Type 00000007 Flags 01002050
Vpb fffffa8007cbdb80 Dacl fffff9a100303ae0 DevExt fffffa8007e7c1b0 DevObjExt fffffa8007e7c858 Dope fffffa8007cbdb10
ExtensionFlags (0×00000800)
                             Unknown flags 0×00000800
AttachedDevice (Upper) fffffa8007e7cb90 \Driver\partmgr
AttachedTo (Lower) fffffa8007abd420 \Driver\ACPI
Device queue is not busy.

0: kd> !object \Device\Harddisk0\Partition0
Object: fffff8a000154200  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a0001541d0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a0001f8eb0  Name: Partition0
    Target String is ‘\Device\Harddisk0\DR0′

なお、この辺の動作は「インサイド Windows 第四版 第 10 章 ストレージ管理」 に書いてあります。

ダイレクト I/O 周りの動作をデバッグしてもいいのですが、それは今後検討するとして、今回はシンボリック リンク名とドライブ文字のマッピングについてのプログラムを書きました。例えば、C: ドライブは PhysicalDrive 何番なのか、逆に PhysicalDrive1 にはどのドライブ文字が割り当てられているのか、という点について調べようとすると、ビルトインでいいツールがないのです。いちいち Winobj やらデバッガーを使うのも鶏を割くのに牛刀を用いる感じがします。

プログラムはこんな感じです。
ご覧のとおり WMI を使いました。WMI だと間接的に情報を取ってくることになり、一次情報を取っていない気がして本当は嫌なのですが、他にシンプルな方法が思い浮かばなくて仕方なく、です。VDS である程度の情報は取ってこれるんですけどね。
なお、このプログラムでは CD-ROM ドライブや、ドライブ文字が割り当てられていないボリュームが出力されません。ダメダメです。

//
// wmidata.cpp
//

#include <Windows.h>
#include <stdio.h>
#include <strsafe.h>

#include <comdef.h>
#include <Wbemidl.h>

#pragma comment(lib, "wbemuuid.lib")

typedef struct _DRIVEINFO {
    WCHAR DriveLetter;
    UINT DiskIndex;
    UINT PartitionIndex;
} DRIVEINFO, *PDRIVEINFO;

class CWmiService {
private:
    IWbemLocator *mLocator;
    IWbemServices *mService;

public:
    CWmiService();
    ~CWmiService();

    BOOL Initialize();

    inline operator IWbemServices*() const {
        return mService;
    }

    IWbemClassObject *GetObject(IWbemClassObject*, LPCWSTR);
    UINT GetUint32(IWbemClassObject*, LPCWSTR);
    VOID GetString(IWbemClassObject*, LPCWSTR, PWSTR, ULONG);

    ULONG GetCount(LPCWSTR);

};

CWmiService::CWmiService()
    : mLocator(NULL), mService(NULL)
{}

CWmiService::~CWmiService() {
    if ( mLocator ) mLocator->Release();
    if ( mService ) mService->Release();
    CoUninitialize();
}

// http://technet.microsoft.com/ja-jp/library/aa390423(v=vs.85).aspx
BOOL CWmiService::Initialize() {
    HRESULT Result;
   
    Result= CoInitializeEx(0, COINIT_MULTITHREADED);
    if ( FAILED(Result) ) {
        wprintf(L"CoInitializeEx failed – 0x%08x\n", Result);
        return FALSE;
    }

    Result= CoInitializeSecurity(NULL, -1, NULL, NULL,
        RPC_C_AUTHN_LEVEL_DEFAULT,
        RPC_C_IMP_LEVEL_IMPERSONATE,
        NULL, EOAC_NONE, NULL);
    if ( FAILED(Result) ) {
        wprintf(L"CoInitializeSecurity failed – 0x%08x\n", Result);
        return FALSE;
    }
   
    Result = CoCreateInstance(CLSID_WbemLocator,
        0, CLSCTX_INPROC_SERVER,
        IID_IWbemLocator, (LPVOID *)&mLocator);
    if ( FAILED(Result) ) {
        wprintf(L"CoCreateInstance failed – 0x%08x\n", Result);
        return FALSE;
    }

    Result= mLocator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"),
         NULL, NULL, 0, NULL, 0, 0,&mService);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemLocator::ConnectServer failed – 0x%08x\n", Result);
        return FALSE;
    }

    Result= CoSetProxyBlanket(mService,
       RPC_C_AUTHN_WINNT,
       RPC_C_AUTHZ_NONE,
       NULL,
       RPC_C_AUTHN_LEVEL_CALL,
       RPC_C_IMP_LEVEL_IMPERSONATE,
       NULL, EOAC_NONE);
    if ( FAILED(Result) ) {
        wprintf(L"CoSetProxyBlanket failed – 0x%08x\n", Result);
        return FALSE;
    }

    return TRUE;
}

IWbemClassObject *CWmiService::GetObject(IWbemClassObject *Object, LPCWSTR Property) {
    HRESULT Result;
    IWbemClassObject *Ret= NULL;
    VARIANT Value;

    Result= Object->Get(Property, 0, &Value, NULL, NULL);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemClassObject::Get failed – 0x%08x\n", Result);
        return NULL;
    }

    Result= mService->GetObject(Value.bstrVal,
        WBEM_FLAG_RETURN_WBEM_COMPLETE,
        NULL, &Ret, NULL);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemServices::GetObject failed – 0x%08x\n", Result);
        Ret= NULL;
    }

    VariantClear(&Value);

    return Ret;
}

UINT CWmiService::GetUint32(IWbemClassObject *Object, LPCWSTR Property) {
    LRESULT Result;
    UINT Ret= 0;
    VARIANT Value;

    Result= Object->Get(Property, 0, &Value, NULL, NULL);
    if ( SUCCEEDED(Result) )
        Ret= Value.uintVal;
    else
        wprintf(L"IWbemClassObject::Get failed – 0x%08x\n", Result);

    VariantClear(&Value);

    return Ret;
}

VOID CWmiService::GetString(IWbemClassObject *Object, LPCWSTR Property,
                            PWSTR Buffer, ULONG BufferLength) {
    LRESULT Result;
    VARIANT Value;

    Result= Object->Get(Property, 0, &Value, NULL, NULL);
    if ( SUCCEEDED(Result) ) {
        StringCchCopy(Buffer, BufferLength, Value.bstrVal);
        VariantClear(&Value);
    }
    else {
        wprintf(L"IWbemClassObject::Get failed – 0x%08x\n", Result);
        return;
    }
}

ULONG CWmiService::GetCount(LPCWSTR Query) {
    LRESULT Result;
    ULONG Ret= 0;
    CONST INT BatchCount= 100;
    IWbemClassObject *Object[BatchCount];
    IEnumWbemClassObject *Enumerator= NULL;
   
    Result= mService->ExecQuery(bstr_t("WQL"), bstr_t(Query),
        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
        NULL, &Enumerator);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemServices::ExecQuery failed – 0x%08x\n", Result);
        return 0;
    }

    // http://msdn.microsoft.com/en-us/library/windows/desktop/aa394175(v=vs.85).aspx
    while ( Enumerator ) {
        ULONG Count= 0;
        Result= Enumerator->Next(WBEM_INFINITE, BatchCount, Object, &Count);
        if ( Count==0 ) break;
        if ( FAILED(Result) ) {
            wprintf(L"IEnumWbemClassObject::Next failed – 0x%08x\n", Result);
            break;
        }
        Ret+= Count;

        for ( ULONG i=0 ; i<Count ; ++i )
            Object[i]->Release();
    }

    Enumerator->Release();

    return Ret;
}

typedef int (__cdecl *PCOMPAREFUNC)(const void *, const void *);

int CompareDriveInfo_Index(CONST PDRIVEINFO p1, CONST PDRIVEINFO p2) {
    if ( p1->DiskIndex==p2->DiskIndex )
        return p1->PartitionIndex-p2->PartitionIndex;
    else
        return p1->DiskIndex-p2->DiskIndex;
}

int CompareDriveInfo_Drive(CONST PDRIVEINFO p1, CONST PDRIVEINFO p2) {
    return p1->DriveLetter-p2->DriveLetter;
}

// http://msdn.microsoft.com/en-us/library/windows/desktop/aa394175(v=vs.85).aspx
VOID DumpDiskDriveMapping() {
    CONST WCHAR QUERY_MAPPING[]= L"SELECT * FROM Win32_LogicalDiskToPartition";
    LRESULT Result;
    ULONG Count= 0;
    IEnumWbemClassObject *Enumerator= NULL;
    IWbemClassObject **Object= NULL;
    IWbemClassObject *Partition= NULL; // Antecedent
    IWbemClassObject *LogicalDisk= NULL; // Dependent
    PDRIVEINFO DriveInfo= NULL;
    ULONG i;

    CWmiService wmi;
    wmi.Initialize();
   
    ULONG MappedDrives= wmi.GetCount(QUERY_MAPPING);
    if ( MappedDrives==0 ) {
        wprintf(L"No drives?\n");
        goto cleanup;
    }

    DriveInfo= new DRIVEINFO[MappedDrives];
    Object= new IWbemClassObject*[MappedDrives];
    if ( !DriveInfo || !Object ) {
        wprintf(L"Memory allocation error – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    Result= ((IWbemServices*)wmi)->ExecQuery(
        bstr_t("WQL"), bstr_t(QUERY_MAPPING),
        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
        NULL, &Enumerator);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemServices::ExecQuery failed – 0x%08x\n", Result);
        goto cleanup;
    }

    Result= Enumerator->Next(WBEM_INFINITE, MappedDrives, Object, &Count);
    if ( FAILED(Result) ) {
        wprintf(L"IEnumWbemClassObject::Next failed – 0x%08x\n", Result);
        goto cleanup;
    }
   
    for ( i=0 ; i<Count ; ++i ) {
        Partition= wmi.GetObject(Object[i], L"Antecedent");
        if ( Partition ) {
            //
http://msdn.microsoft.com/en-us/library/windows/desktop/aa394135(v=vs.85).aspx
            DriveInfo[i].PartitionIndex= wmi.GetUint32(Partition, L"Index");
            DriveInfo[i].DiskIndex= wmi.GetUint32(Partition, L"DiskIndex");
            Partition->Release();
        }

        LogicalDisk= wmi.GetObject(Object[i], L"Dependent");
        if ( LogicalDisk ) {
            //
http://msdn.microsoft.com/en-us/library/windows/desktop/aa394173(v=vs.85).aspx
            WCHAR Drive[3];
            wmi.GetString(LogicalDisk, L"DeviceID", Drive, 2);
            DriveInfo[i].DriveLetter= Drive[0];
            LogicalDisk->Release();
        }

        Object[i]->Release();
    }

    qsort(DriveInfo, Count, sizeof(DRIVEINFO), (PCOMPAREFUNC)CompareDriveInfo_Index);

    // http://otndnld.oracle.co.jp/document/products/oracle10g/102/windows/B25695-02/ap_raw.htm

    int diskindex= -1;
    for ( i=0 ; i<Count ; ++i ) {
        if ( diskindex!=DriveInfo[i].DiskIndex ) {
            wprintf(L"\n\\\\.\\PhysicalDrive%d\n", DriveInfo[i].DiskIndex);
            diskindex= DriveInfo[i].DiskIndex;
        }

        wprintf(L"  \\\\.\\%c: => \\Device\\Harddisk%d\\Partition%d\n",
            DriveInfo[i].DriveLetter,
            DriveInfo[i].DiskIndex,
            DriveInfo[i].PartitionIndex+1,
            DriveInfo[i].DiskIndex);
    }

    //putwchar(L’\n’);
    //qsort(DriveInfo, Count, sizeof(DRIVEINFO), (PCOMPAREFUNC)CompareDriveInfo_Drive);
    //
    //for ( i=0 ; i<Count ; ++i ) {
    //    wprintf(L"  %c: => \\Device\\Harddisk%d\\Partition%d : \\PhysicalDrive%d\n",
    //        DriveInfo[i].DriveLetter,
    //        DriveInfo[i].DiskIndex,
    //        DriveInfo[i].PartitionIndex+1,
    //        DriveInfo[i].DiskIndex);
    //}

cleanup:
    if ( Object ) delete [] Object;
    if ( DriveInfo ) delete [] DriveInfo;
    if ( Enumerator ) Enumerator->Release();

}

出力結果はこんな感じになります。

出力例 1.

\\.\PhysicalDrive0
  \\.\V: => \Device\Harddisk0\Partition2

\\.\PhysicalDrive1
  \\.\C: => \Device\Harddisk1\Partition1
  \\.\H: => \Device\Harddisk1\Partition2
  \\.\T: => \Device\Harddisk1\Partition3

\\.\PhysicalDrive2
  \\.\E: => \Device\Harddisk2\Partition1
  \\.\W: => \Device\Harddisk2\Partition2

\\.\PhysicalDrive3
  \\.\F: => \Device\Harddisk3\Partition1

出力例 2.

\\.\PhysicalDrive0
  \\.\C: => \Device\Harddisk0\Partition2
  \\.\N: => \Device\Harddisk0\Partition4
  \\.\O: => \Device\Harddisk0\Partition4

\\.\PhysicalDrive1
  \\.\E: => \Device\Harddisk1\Partition1
  \\.\F: => \Device\Harddisk1\Partition2
  \\.\G: => \Device\Harddisk1\Partition3
  \\.\H: => \Device\Harddisk1\Partition4
  \\.\I: => \Device\Harddisk1\Partition5
  \\.\J: => \Device\Harddisk1\Partition6
  \\.\K: => \Device\Harddisk1\Partition7
  \\.\L: => \Device\Harddisk1\Partition8
  \\.\M: => \Device\Harddisk1\Partition9

パーティション番号が抜けているところは、ドライブ文字が割り当てられていないボリュームです。
出力例 2. で \Device\Harddisk0\Partition4 が 2 つあるのは、これが拡張パーティションで、論理ドライブが 2 つあるという意味です。MBR の制限ですね。一方 PhysicalDrive1 は GPT パーティショニングなので、9 つのパーティションでも問題なしです。

今回は C++ プログラムで処理しましたが、WMI については、やはり PowerShell でアクセスするのが一番楽です。他にも wmic やら wbemtest、VB スクリプトを使うなどの方法もあります。しかし、Windows 7 (2008 R2) なら PowerShell の Get-WMIObject と Set-WmiInstance コマンドレットを使わない手はありません。例え起動が遅いと言われようとも、これだけのために PowerShell を使う価値はあります。

ソース中のコメントに入れてありますが、今回のプログラムは Win32_DiskDriveToDiskPartition クラスのインスタンスを適当に成形しているだけです。

PS > Get-WmiObject Win32_DiskDriveToDiskPartition | fl Antecedent,Dependent

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE1"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #1, Partition #0"

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE1"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #1, Partition #1"

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE1"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #1, Partition #2"

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE0"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #0, Partition #0"

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #4

これまでの記事で、RPC サーバーと RPC クライアントができました。
サーバー間で RPC 通信ができるようになったので、とりあえず Network Monitor でパケット キャプチャーを取って見てみます。

まずは名前付きパイプによる通信から。

image

ごちゃごちゃしていますが、よく見れば内容は単純です。

サーバー間の名前付きパイプは、SMB による通信が行われます。上のパケットは Windows Server 2008 R2 の環境で取ったので、SMB 2.0 による通信が行われています。したがって、サーバー側のポート番号は 445/tcp です。

ファイル共有にアクセスするときと同様に、Negotiate → Session Setup → Tree Connect → Create というように SMB コマンドが実行されていきます。

一つ目のポイントは、Tree Connect コマンドでの接続先エンド ポイントが \\サーバー名\IPC$ となることです。ファイル共有の時は \\サーバー名\共有名 がエンドポイントになりますが、名前付きパイプの時は必ず IPC$ を見に行きます。ちなみに IPC は Interprocess Communications の略です。

Create するときのファイル名がパイプ名になります。\pipe\test というパイプを使った通信では、test というファイルを開きます。イメージとしては、\\サーバー名\IPC$ という共有フォルダーの中に test というファイルがあって、それを CreateFile するイメージです。

SMB Create の後に、RPC のバインド処理が行われます。プロトコルの階層を見ると、これは SMB Write コマンドによって行われていることがわかります。ようは WriteFile です。後の通信も、SMB の Write やら Read やらで行われます。これが名前付きパイプによる RPC 通信です。

image

それともう一つ、これは SMB 通信なので、Session Setup ではユーザー認証が行われます。今回の検証は Active Directory ドメイン環境で行っていて、AsyncClient の接続先には FQDN を入力したので Kerberos 認証が行われます。IP アドレスの場合は NTLM 認証が行われます。Session Setup の要求パケットを見ていくと分かりますが、通常の SMB 通信と同様に cifs のサービス チケットを提示しています。

では次に、TCP/IP による RPC を見てみます。
もちろん名前付きパイプの通信も TCP/IP なので、ネイティブ TCP/IP とでも言いましょうか。

image

表示の上では余計にごちゃごちゃしていますが、短くてシンプルなのが分かると思います。
このキャプチャーは、メソッドを続けて 2 回呼び出したときのものなので、要求1 → 要求2 → 応答1 → 応答2 という順番になっています。1 回のメソッド実行の流れは非常に単純です。TCP セッション確立 → RPC Bind → Request → Response という 4 段階だけです。

Network Monitor のサマリーにおいて、名前付きパイプのときもプロトコル名が MSRPC になっていましたが、今回の場合とはプロトコル階層が異なります。もちろん今回は SMB 通信は一切関係ありません。ポート番号はアプリケーションが指定しています。RPC の動的ポート割り当ても使うことができます。そのときは RPC サーバーでポートをバインドするときに RpcServerUseProtseqEx API を使います。

image

名前付きパイプでもネイティブ TCP/IP でもいいのですが、RPC Bind メッセージを見ると、3 種類の GUID がクライアントからの要求に含まれているのが分かります。下の抜粋がそうです。

- PContElem [0]
  – AbstractSyntax
    + IfUuid: {161B9AB8-1A96-40A6-BF8B-AA2D7EC94B6D}
      IfVersion: 1 (0×1)
  – TransferSyntaxes
    + IfUuid: {8A885D04-1CEB-11C9-9FE8-08002B104860}
      IfVersion: 2 (0×2)

- PContElem [1]
    PContId: 1 (0×1)
    NTransferSyn: 1 (0×1)
    Reserved: 0 (0×0)
  – AbstractSyntax
    + IfUuid: {161B9AB8-1A96-40A6-BF8B-AA2D7EC94B6D}
      IfVersion: 1 (0×1)
  – TransferSyntaxes
    + BTFNUuid: {6CB71C2C-9812-4540-0300000000000000}
      IfVersion: 1 (0×1)

image

既にお気づきと思いますが、この中の 2 つは、インターフェース定義に登場しています。プログラムを書く最初に uuidgen で idl ファイルのひな型を作成しましたが、AbstractSyntax の IfUuid は、この時の IDL に埋め込まれていた UUID に一致します。

[
uuid(161b9ab8-1a96-40a6-bf8b-aa2d7ec94b6d),
version(1.0)
]
interface pipo
{
    void RpcSleep(int Duration);
    void RpcSleepAsync(int Duration);
    void Shutdown();
}

midl でコンパイルして生成されたクライアント用ソース ファイル pipo_c.cpp には以下のような定数がありました。

static const RPC_CLIENT_INTERFACE pipo___RpcClientInterface = {
  sizeof(RPC_CLIENT_INTERFACE),
  {{0x161b9ab8,0x1a96,0x40a6,{0xbf,0x8b,0xaa,0x2d,0x7e,0xc9,0x4b,0x6d}},
    {1,0}},
  {{0x8A885D04,0x1CEB,0x11C9,{0x9F,0xE8,0×08,0×00,0x2B,0×10,0×48,0×60}},
    {2,0}},
  0,
  0,
  0,
  0,
  0,
  0×00000000
};
RPC_IF_HANDLE pipo_v1_0_c_ifspec = (RPC_IF_HANDLE)& pipo___RpcClientInterface;

AbstractSyntax と TransferSyntaxes の IfUuid に加え、バージョン番号もここで定義されています。
「Transfer Syntax って何よ」って話ですが、それはここに書いてあります。

RPC transfer syntax: A method for encoding messages defined in an Interface Definition Language (IDL) file. Remote procedure call (RPC) can support different encoding methods or transfer syntaxes.
http://msdn.microsoft.com/en-us/library/cc232140(v=prot.10).aspx#rpc_transfer_syntax

ということで、エンコードの方法を示しているようです。ということは、インターフェース UUID と違って、ランダムに生成されているものではないということです。

今回使われている {8A885D04-1CEB-11C9-9FE8-08002B104860} は NDR (Network Data Representation) 2.0 という形式であることを示しています。まあ、これ以上深追いするのは止めておきましょう。すべて [MS-RPCE] の仕様書に書いてあるので、時間があるときにお読み下さい。残念ながら私は一部しか読んでいません・・。

2.2.4.12 NDR Transfer Syntax Identifier
http://msdn.microsoft.com/en-us/library/cc243843(v=PROT.13).aspx

http://msdn.microsoft.com/en-us/library/33b94545-9ae1-4cc8-9ce5-4be893b7bec3(v=prot.13)#NDR

最後に残った {6CB71C2C-9812-4540-0300000000000000} についても、仕様書に書いてあります。これも固定値のようですね。

3.3.1.5.3 Bind Time Feature Negotiation
http://msdn.microsoft.com/en-us/library/cc243715(v=PROT.13).aspx

以上が RPC Bind に含まれる GUID でした。

名前付きパイプのときとは異なり、今回のようなシンプルなメソッドでは認証 (+認可) 動作が発生しません。

わりと後半はぐだぐだになってしまいました (力尽きた・・・) が、サーバー間の非同期 RPC のシリーズはこのへんにしておきます。
他に遊ぶとすれば、デバッガーをアタッチして RPC メソッド呼び出し時のモジュールの動きを、カーネル/ユーザー モード、サーバー/クライアントのそれぞれで見てみると面白いと思います。

例えば、RPC サーバーのメソッドは以下のようなスタックで呼び出されています。

0:007> k
Child-SP          RetAddr           Call Site
00000000`02bcf1c8 000007fe`fe5b23d5 AsyncServer!RpcSleepAsync
00000000`02bcf1d0 000007fe`fe65f695 RPCRT4!Invoke+0×65
00000000`02bcf220 000007fe`fe5a50f4 RPCRT4!NdrAsyncServerCall+0x29c
00000000`02bcf300 000007fe`fe5a4f56 RPCRT4!DispatchToStubInCNoAvrf+0×14
00000000`02bcf330 000007fe`fe59d879 RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0×146
00000000`02bcf450 000007fe`fe59d6de RPCRT4!OSF_SCALL::DispatchHelper+0×159
00000000`02bcf570 000007fe`fe6527b4 RPCRT4!OSF_SCALL::ProcessReceivedPDU+0x18e
00000000`02bcf5e0 000007fe`fe5ec725 RPCRT4!OSF_SCALL::BeginRpcCall+0×134
00000000`02bcf610 000007fe`fe59d023 RPCRT4!Invoke+0x2adf9
00000000`02bcf6c0 000007fe`fe59d103 RPCRT4!CO_ConnectionThreadPoolCallback+0×123
00000000`02bcf770 000007fe`fe15898f RPCRT4!CO_NmpThreadPoolCallback+0x3f
00000000`02bcf7b0 00000000`77c5098a KERNELBASE!BasepTpIoCallback+0x4b
00000000`02bcf7f0 00000000`77c5feff ntdll!TppIopExecuteCallback+0x1ff
00000000`02bcf8a0 00000000`779e652d ntdll!TppWorkerThread+0x3f8
00000000`02bcfba0 00000000`77c6c521 kernel32!BaseThreadInitThunk+0xd
00000000`02bcfbd0 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #3

次に RPC クライアントです。

4. RPC クライアントを書く

同期 RPC のときは、適当なプロトコルを選んでバインドしてから、自動生成されたスタブを呼び出すだけでしたが、非同期 RPC ではもっと面倒です。コールバックの仕組みを自分で用意しなけれななりません。最初に書いたように、今回は I/O 完了ポートを使ってみます。面倒です。

ファイルは、RPC クライアントと同じく 3 つ。ですが、クラスは 2 つ用意します。

  • AsyncClient.h ・・・ AsyncRpcHandler クラス、CAsyncClient クラスの宣言
  • AsyncClient.cpp ・・・ AsyncRpcHandler クラス、CAsyncClient クラスの定義
  • main.cpp ・・・ WinMain、ウィンドウ処理

IDL ファイルのインターフェース定義で書いたように、今回実装する非同期 RPC メソッドは RpcSleepAsync の 1 つだけです。AsyncRpcHandler クラスは、この RpcSleepAsync のスタブを呼び出す処理と、コールバック処理を実装します。

CAsyncClient クラスは、I/O 完了ポートを使ったコールバックの処理を実装します。具体的には、I/O 完了ポート用のワーカー スレッドを準備し、RPC サーバーからコールバックが来たら AsyncRpcHandler の街頭メソッドを呼び出すという処理を行ないます。

RPC クライアントを C 言語だけで書くのは辛そうです。

AsyncClient.h

上に書いたように、2 つのクラスのプロトタイプ宣言です。
NUMBER_OF_THREADS で、I/O 完了ポートで使う待機スレッドの最大数を指定します。

//
// AsyncClient.h
//

#pragma once

#include "resource.h"
#include "..\AsyncCommon.h"

extern HWND g_Dlg;

#define NUMBER_OF_THREADS 5

typedef struct _METHOD_CONTEXT {
    DWORD SessionID;
    DWORD Status;
    OVERLAPPED Overlapped;
} METHOD_CONTEXT, *PMETHOD_CONTEXT;

class AsyncRpcHandler {
private:
    HANDLE mCompletionPort;

    RPC_ASYNC_STATE mAsyncState;
    METHOD_CONTEXT mContext;

public:
    AsyncRpcHandler(HANDLE);
    ~AsyncRpcHandler() {}

    VOID Sleep(DWORD);
    BOOL ProcessComplete();

};

class CAsyncClient {
private:
    HANDLE mCompletionPort;
    HANDLE mThreads[NUMBER_OF_THREADS];
   
    RPC_PROTOCOL_TYPE mProtocol;

    static DWORD CALLBACK WorkerThreadStart(PVOID);
    DWORD WorkerThread();

public:
    WCHAR mEndpoint[MAX_ENDPOINT];
    WCHAR mServer[MAX_ENDPOINT];

    CAsyncClient();
    ~CAsyncClient();

    inline operator HANDLE() const { return mCompletionPort; }
   
    BOOL InitializeThreadPool();
    BOOL Bind();
   
    inline VOID SetProtocolType(LRESULT l) {
        mProtocol= (RPC_PROTOCOL_TYPE)min(l, Rpc_NotSupported);
    }

};

AsyncClient.cpp

クラスを実装します。このファイルにエッセンスがいろいろ詰まっています。

まず、AsyncRpcHandler::Sleep がスタブを呼び出す処理です。同期 RPC と違うのは RPC_ASYNC_STATE をスタブに渡す必要がある点です。ACF ファイルで指定した [async] 属性によって、非同期のスタブとしてプロトタイプ宣言が生成されています。
RpcAsyncInitializeHandle API で RPC_ASYNC_STATE 構造体の Size, Signature, Lock, StubInfo メンバーを埋めてもらいます。その他のメンバーは自分で埋める必要があります。ここで、コールバックの種類や、ユーザー定義データを設定します。今回は I/O 完了ポートを使うので NotificationTypeIoc に RpcNotificationTypeIoc を指定します。
非同期 RPC なので、RPC サーバーの処理に関係なく AsyncRpcHandler::Sleep の処理は滞りなく終了します。

コールバックを受け取った後の処理が AsyncRpcHandler::ProcessComplete  です。これは CAsyncServer クラスの処理として、コールバックが来たときに ProcessComplete メンバーを呼び出すように実装しています。ProcessComplete で重要なのは、RpcAsyncCompleteCall API の実行です。この API は RPC サーバーにおける RPC メソッド本体の RpcSleepAsync 関数でも呼び出していました。
クライアント側で RpcAsyncCompleteCall を実行することで、サーバー側の RpcAsyncCompleteCall に第二引数として渡した戻り値を受け取ることができます。したがって基本的には、クライアントが RpcAsyncCompleteCall を呼び出すのはサーバー側の処理後である必要があります。もし、サーバーが RpcAsyncCompleteCall を呼び出していない段階でクライアントが RpcAsyncCompleteCall を呼ぶと、戻り値が RPC_S_ASYNC_CALL_PENDING となり、判別できます。

CAsyncClient クラスは、I/O 完了ポート関連の処理です。InitializeThreadPool で CreateIoCompletionPort API を実行し、ワーカー スレッドを必要なだけ (ここでは NUMBER_OF_THREADS 定数で指定した分だけ) 作ります。
CAsyncClient::WorkerThread が I/O 完了ポートのワーカースレッドであり、GetQueuedCompletionStatus API で待機に入ります。サーバー側の RPC 処理が完了すると GetQueuedCompletionStatus から制御が返ってくるので、上で説明した完了ルーチンである AsyncRpcHandler::ProcessComplete を実行します。
ここでのポイントは、AsyncRpcHandler クラス インスタンスへのポインターを Overlapped を使って取得している点です。上の説明では飛ばしましたが、クライアントのコールバック関数で RpcAsyncCompleteCall を呼び出す場合に、第一引数に RPC_SYNC_STATE 構造体を渡す必要があります。このとき、メソッドを呼び出す際に指定した RPC_ASYNC_STATE と Signature などの値が一致していないとおかしな動作になります。つまり、まだ実行中のメソッドや、そもそも呼び出してさえいない Signature である RPC_ASYNC_STATE を使って RpcAsyncCompleteCall を呼び出すと、例外が発生します。そのため、AsyncRpcHandler::Sleep の中でメソッド実行時に mAsyncState.u.IOC.lpOverlapped に this ポインターを渡しています。コールバックが来たときに GetQueuedCompletionStatus によって取得される Overlapped には、メソッド呼び出し時の this ポインターが含まれているため、これを使って ProcessComplete を呼び出すことで、メソッド呼び出し時と同じ AsyncRpcHandler クラス インスタンスを保証することができます。

//
// AsyncClient.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncClient.h"
#include "..\idl\pipo.h"

#pragma comment(lib, "rpcrt4.lib")

void __RPC_FAR * __RPC_API midl_user_allocate(size_t len) {
    return(malloc(len));
}

void __RPC_API midl_user_free(void __RPC_FAR * ptr) {
    free(ptr);
}

AsyncRpcHandler::AsyncRpcHandler(HANDLE Port)
    : mCompletionPort(Port)
{}

VOID AsyncRpcHandler::Sleep(DWORD Duration) {
    RPC_STATUS Status= RPC_S_OK;

    Status = RpcAsyncInitializeHandle(&mAsyncState, sizeof(RPC_ASYNC_STATE));
    if (Status) {
        LOGERROR(L"RpcAsyncInitializeHandle failed – 0x%08x", Status);
        return;
    }

    mContext.SessionID= rand();

    mAsyncState.UserInfo = NULL;
    mAsyncState.NotificationType = RpcNotificationTypeIoc;
    mAsyncState.u.IOC.hIOPort= mCompletionPort;
    mAsyncState.u.IOC.lpOverlapped= (LPOVERLAPPED)this;
    mAsyncState.u.IOC.dwCompletionKey= 1;
    mAsyncState.u.IOC.dwNumberOfBytesTransferred= sizeof(AsyncRpcHandler);

     RpcTryExcept
        RpcSleepAsync(&mAsyncState, Duration);
    RpcExcept( EXCEPTION_EXECUTE_HANDLER )
        LOGERROR(L"RPC exception – 0x%08x", RpcExceptionCode());
    RpcEndExcept

    LOGINFO(L"(SleepAsync) invoked. sessid:0x%08x", mContext.SessionID);
}

BOOL AsyncRpcHandler::ProcessComplete() {
    RPC_STATUS Status;
    PVOID Reply= NULL;

    Status= RpcAsyncCompleteCall(&mAsyncState, Reply);
    if ( Status==RPC_S_ASYNC_CALL_PENDING )
        return TRUE;

    if ( Status!=RPC_S_OK ) {
        LOGERROR(L"RpcAsyncCompleteCall failed – 0x%08x", Status);
        return FALSE;
    }

    LOGINFO(L"(SleepAsync) done. sessid:0x%08x", mContext.SessionID);

    delete this;

    return TRUE;
}

CAsyncClient::CAsyncClient()
    : mCompletionPort(NULL) {
    ZeroMemory(mThreads, NUMBER_OF_THREADS*sizeof(HANDLE));
}

CAsyncClient::~CAsyncClient() {
    if ( pipo_IfHandle )
        RpcBindingFree(&pipo_IfHandle);
   
    if ( mCompletionPort!=NULL )
        CloseHandle(mCompletionPort);

    WaitForMultipleObjects(NUMBER_OF_THREADS, mThreads, TRUE, INFINITE);

    for ( int i=0 ; i<NUMBER_OF_THREADS ; ++i ) {
        if ( mThreads[i] )
            CloseHandle(mThreads[i]);
    }
}

DWORD CALLBACK CAsyncClient::WorkerThreadStart(PVOID Param) {
    if ( Param )
        return ((CAsyncClient*)Param)->WorkerThread();

    return 0;
}

DWORD CAsyncClient::WorkerThread() {
    BOOL Ret= FALSE;
    DWORD BytesTransferred= 0;
    ULONG_PTR CompletionKey= NULL;
    LPOVERLAPPED Overlapped= NULL;

    do {
        Ret= GetQueuedCompletionStatus(
            mCompletionPort,
            &BytesTransferred,
            &CompletionKey,
            &Overlapped,
            INFINITE);
        if ( !Ret ) {
            LOGERROR(L"GetQueuedCompletionStatus failed – 0x%08x\n",
              GetLastError());
            goto cleanup;
        }
       
        if ( !((AsyncRpcHandler*)Overlapped)->ProcessComplete() )
            break;
    } while (1);

cleanup:
    ExitThread(0);
    return 0;
}

BOOL CAsyncClient::InitializeThreadPool() {
    BOOL Ret= FALSE;

    if ( !mCompletionPort ) {
        mCompletionPort= CreateIoCompletionPort(INVALID_HANDLE_VALUE,
            NULL, NULL, NUMBER_OF_THREADS);
        if ( mCompletionPort==NULL ) {
            LOGERROR(L"CreateIoCompletionPort failed – 0x%08x",
              GetLastError());
            goto cleanup;
        }
    }

    for ( int i=0 ; i<NUMBER_OF_THREADS ; ++i ) {
        if ( mThreads[i]==NULL ) {
            mThreads[i]= CreateThread(NULL, 0, WorkerThreadStart,
              this, 0, NULL);
            if ( mThreads[i]==NULL )
                LOGERROR(L"CreateThread failed – 0x%08x", GetLastError());
        }
    }
   
    LOGERROR(L"Thread Pool initiated. (%d threads)", NUMBER_OF_THREADS);

    Ret= TRUE;

cleanup:
    return Ret;
}

BOOL CAsyncClient::Bind() {
    BOOL Ret= FALSE;
    RPC_STATUS Status= RPC_S_OK;
    RPC_PROTOCOL &Protocol=
      SupportedProtocols[min(mProtocol, Rpc_NotSupported)];
    RPC_WSTR BindStr= NULL;

    Status= RpcStringBindingCompose(NULL,
        (RPC_WSTR)Protocol.Name,
        (RPC_WSTR)mServer,
        (RPC_WSTR)mEndpoint, NULL, &BindStr);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcStringBindingCompose failed – 0x%08x\n", Status);
        goto cleanup;
    }

    if ( pipo_IfHandle ) {
        Status= RpcBindingFree(&pipo_IfHandle);
        if ( Status!=RPC_S_OK )
            LOGERROR(L"RpcBindingFree failed – 0x%08x\n", Status);
        pipo_IfHandle= NULL;
    }

    Status= RpcBindingFromStringBinding(BindStr, &pipo_IfHandle);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcBindingFromStringBinding failed – 0x%08x\n", Status);
        goto cleanup;
    }

    Ret= TRUE;

cleanup:
    if ( BindStr )
        RpcStringFree(&BindStr);

    return Ret;
}

main.cpp

最後のファイルです。RPC サーバーとほぼ同じです。

RPC の処理とは直接関係ありませんが、WM_INITDIALOG メッセージを受け取った時に CAsyncClient::InitializeThreadPool を呼び出して、I/O 完了ポートのワーカー スレッドを作成します。これは別に WinMain 関数に書いてもいいのですが、ワーカースレッドの初期化がうまくいったというログをダイアログボックスに表示させたいという理由で、ここに書いています。

エンドポイントにバインドする処理は CAsyncClient::Bind ですが、これは IDOK ボタンがクリックされたときに呼び出します。RPC クライアントを複数のプロトコルやエンドポイントに対応させるため、ボタンを押すたびに アンバインド→バインド を実行するようにしています。

最後にポイントが 1 つあります。IDOK のクリック処理の中で、AsyncRpcHandler を new 演算子で動的確保してから AsyncRpcHandler::Sleep 関数を呼び出しています。上で説明したように、メソッド呼び出し時とコールバック時に同じポインターを Overlapped として使えなければならないため、AsyncRpcHandler  をローカル インスタンスとしては使うことができない、というのがその理由です。インスタンスの解放処理は、AsyncRpcHandler::ProcessComplete の中で delete this として実行されます。このデザインが適切かどうかはあまり検証していません。

//
// main.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncClient.h"

CAsyncClient *g_AsyncClient= NULL;
HWND g_Dlg= NULL;

INT_PTR CALLBACK DlgProc(HWND Dlg, UINT Msg, WPARAM w, LPARAM l) {
    HWND Control= NULL;

    switch ( Msg ) {
    case WM_INITDIALOG:
        g_Dlg= Dlg;
       
        Control= GetDlgItem(Dlg, IDC_COMBO_PROTOCOL);

        for ( PRPC_PROTOCOL p= SupportedProtocols ;
              p->Protocol!=Rpc_NotSupported ; ++p )
            SendMessage(Control, CB_ADDSTRING, NULL,
              (LPARAM)p->FriendlyName);

        PostMessage(Control, CB_SETCURSEL, 0, NULL);
        PostMessage(Dlg, WM_COMMAND,
          MAKELONG(IDC_COMBO_PROTOCOL, CBN_SELCHANGE), (LPARAM)Control);

        g_AsyncClient->InitializeThreadPool();

        return TRUE;

    case WM_COMMAND:
        switch ( LOWORD(w) ) {
        case IDCANCEL:
            EndDialog(Dlg, IDOK);
            break;
        case IDOK:
            GetDlgItemText(Dlg, IDC_EDIT_ENDPOINT,
              g_AsyncClient->mEndpoint, MAX_ENDPOINT);
            GetDlgItemText(Dlg, IDC_EDIT_SERVER,
              g_AsyncClient->mServer, MAX_ENDPOINT);
            g_AsyncClient->SetProtocolType(SendMessage(
              GetDlgItem(Dlg, IDC_COMBO_PROTOCOL), CB_GETCURSEL, 0, 0));

            if ( g_AsyncClient->Bind() ) {
                AsyncRpcHandler *Rpc= new AsyncRpcHandler(*g_AsyncClient);
                Rpc->Sleep(1000);
            }
            break;
        case IDC_COMBO_PROTOCOL:
            if ( HIWORD(w)==CBN_SELCHANGE ) {
                LRESULT Selected= SendMessage((HWND)l, CB_GETCURSEL, 0, 0);
                SetWindowText(GetDlgItem(Dlg, IDC_EDIT_ENDPOINT),
                  SupportedProtocols[
                    min(Selected, Rpc_NotSupported)].DefaultEndpoint);
            }
            break;
        }
        break;
    }
    return FALSE;
}

int WINAPI wWinMain(HINSTANCE hInstance,
                    HINSTANCE,
                    PWSTR pCmdLine,
                    int nCmdShow) {
    g_AsyncClient= new CAsyncClient();
    if ( g_AsyncClient ) {
        DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);
        delete g_AsyncClient;
    }

    return 0;
}

ダイアログボックスの外観

最後に、RPC クライアントの外観です。RPC サーバーとほとんど同じです。コピペが冴えます。

image

ここまでがコードの説明でした。
次回の記事で、作成したプログラムを使っていろいろ遊んでみます。

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #2

続きです。今回は RPC サーバーを書きます。
が、その前にクライアントとサーバーの共通コードを貼っておきます。RPC とは直接関係ないです。

2. クライアント/サーバー共通のコード

これがヘッダー。
これだけを見ても意味不明かと思いますが、、、完成までお待ちください。

//
// AsyncCommon.h
//

#pragma once

#define MAX_LOGGING 1000

extern WCHAR ErrorMsg[];

#define LOGINFO(text, code) \
if ( g_Dlg ) { \
    StringCchPrintf(ErrorMsg, MAX_LOGGING, text, code); \
    AppendWindowText(GetDlgItem(g_Dlg, IDC_EDIT1), ErrorMsg); \
}

#define LOGERROR LOGINFO

//#define LOGERROR(text, code) { \
//    StringCchPrintf(ErrorMsg, MAX_LOGGING, text, code); \
//    MessageBox(g_Dlg, ErrorMsg, L"Error", MB_ICONERROR); }

#define MAX_ENDPOINT 32

enum RPC_PROTOCOL_TYPE : unsigned int  {
    Rpc_Tcpip,
    Rpc_NamedPipe,
    Rpc_Lpc,
    Rpc_NotSupported
};

typedef struct _RPC_PROTOCOL {
    RPC_PROTOCOL_TYPE Protocol;
    WCHAR Name[MAX_ENDPOINT];
    WCHAR FriendlyName[MAX_ENDPOINT];
    WCHAR DefaultEndpoint[16];
} RPC_PROTOCOL, *PRPC_PROTOCOL;

extern RPC_PROTOCOL SupportedProtocols[];

BOOL AppendWindowText(HWND Textbox, LPCTSTR Message);

次にソースファイル。
テキストボックスへのログ表示用の関数です。あとは RPC プロトコル用の定数。まあ・・・これも完成するまでは意味不明ですね。

//
// AsyncCommon.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncCommon.h"

WCHAR ErrorMsg[MAX_LOGGING]; // used in LOGINFO, LOGERROR

RPC_PROTOCOL SupportedProtocols[]= {
    {Rpc_Tcpip,        L"ncacn_ip_tcp", L"TCP/IP",     L"50000" },
    {Rpc_NamedPipe,    L"ncacn_np",     L"Named Pipe", L"\\pipe\\asyncrpc" },
    {Rpc_Lpc,          L"ncalrpc",      L"LPC",        L"asyncrpc_lpc" },
    {Rpc_NotSupported, L"N/A",          L"N/A",        L"N/A" }
};

BOOL AppendWindowText(HWND Textbox, LPCTSTR Message) {
    if ( Message==NULL || Textbox==NULL )
        return FALSE;

    size_t Length= 0;
    if ( FAILED(StringCbLength(Message, MAX_LOGGING, &Length)) )
        Length= 0;

    Length= min(Length, MAX_LOGGING);
   
    PWSTR Buffer1= new WCHAR[MAX_LOGGING+1];
    PWSTR Buffer2= new WCHAR[MAX_LOGGING+1];

    if ( !Buffer1 || !Buffer2 )
        return FALSE;
   
    GetWindowText(Textbox, Buffer1, MAX_LOGGING);

    SYSTEMTIME st;
    GetLocalTime(&st);

    StringCchPrintf(Buffer2, MAX_LOGGING,
        L"[%d/%02d/%02d %02d:%02d:%02d.%03d] %s\r\n%s",
        st.wYear,
        st.wMonth,
        st.wDay,
        st.wHour,
        st.wMinute,
        st.wSecond,
        st.wMilliseconds,
        Message,
        Buffer1);

    return SetWindowText(Textbox, Buffer2);
}

 

3. RPC サーバーを書く

いよいよ RPC サーバーです。ファイルは 3 つです。

  • AsyncServer.h ・・・ CAsyncServer クラスの宣言
  • AsyncSercer.cpp ・・・ CAsyncServer クラス、RPC メソッド本体の定義
  • main.cpp ・・・ WinMain、ウィンドウ処理

今回は真っ当に C++ で書きました。C だけだとけっこう面倒なことになると思います。
CAsyncServer クラスは、待機スレッドの処理がメインです。GUI なので、クライアントからの要求を待機するスレッドを作らないとウィンドウがフリーズしてしまうのです。

AsyncServer.h

今回は RPC で使うプロトコルを TCP/IP、名前付きパイプ、LPC の 3 つを選べるようにしたので、その情報をメンバー変数として持たせています。それが RPC_PROTOCOL_TYPE  列挙型です。

//
// AsyncServer.h
//

#pragma once

#include "resource.h"

#include "..\AsyncCommon.h"

extern HWND g_Dlg;

class CAsyncServer {
private:
    HANDLE mThread;
    DWORD WINAPI RpcServerThread();
    static DWORD WINAPI StartRpcServerThread(LPVOID);
   
    RPC_PROTOCOL_TYPE mProtocol;

    VOID StopAndDestroyThread();

public:
    WCHAR mEndpoint[MAX_ENDPOINT];
    int mMaxInstances;

    CAsyncServer();
    ~CAsyncServer();

    inline VOID SetProtocolType(LRESULT l) {
        mProtocol= (RPC_PROTOCOL_TYPE)min(l, Rpc_NotSupported);
    }

    VOID StartStopRpcServer();
};

AsyncServer.cpp

RpcServerThread が待機スレッドです。これは同期でも非同期でも変わりません。

RpcSleepAsync が、今回のメインとなる非同期 RPC メソッドの本体です。ほとんどを MSDN からコピペしています。同期 RPC メソッドとは異なり、第一引数に RPC_ASYNC_STATE 構造体へのポインターを受け取ります。ここで重要なのは RpcAsyncCompleteCall API の実行です。この API は、クライアントとサーバーの両方のメソッドから呼び出す必要があるのがミソです。サーバー側で RpcAsyncCompleteCall を呼び出すことで、クライアント側にコールバックが発生します。

RpcAsyncCompleteCall function
http://msdn.microsoft.com/en-us/library/windows/desktop/aa375572(v=vs.85).aspx

//
// AsyncServer.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncServer.h"
#include "..\idl\pipo.h"

#pragma comment(lib, "rpcrt4.lib")

DWORD CAsyncServer::RpcServerThread() {
    RPC_STATUS Status= RPC_S_OK;
    RPC_PROTOCOL &Protocol=
      SupportedProtocols[min(mProtocol, Rpc_NotSupported)];

    if ( Protocol.Protocol==Rpc_Tcpip ) {
        RPC_POLICY Policy;
        Policy.Length= sizeof(RPC_POLICY);
        Policy.NICFlags= 0;
        Policy.EndpointFlags= 0;
        //Policy.EndpointFlags= RPC_C_USE_INTRANET_PORT;
        //Policy.EndpointFlags= RPC_C_USE_INTERNET_PORT;

        //Status = RpcServerUseProtseqEx((RPC_WSTR)Protocol.Name, mMaxInstances, NULL, &Policy);
        //if ( Status!=RPC_S_OK ) {
        //    LOGERROR(L"RpcServerUseProtseqEpEx failed – 0x%08x", Status);
        //    goto cleanup;
        //}

    }

    Status = RpcServerUseProtseqEp((RPC_WSTR)Protocol.Name,
      mMaxInstances, (RPC_WSTR)mEndpoint, NULL);
    if ( Status==RPC_S_DUPLICATE_ENDPOINT ) {
        LOGINFO(L"The endpoint ‘%s’ is already registered.", mEndpoint);
    }
    else if ( Status!=RPC_S_OK ) {
        LOGERROR(L"RpcServerUseProtseqEp failed – 0x%08x", Status);
        goto cleanup;
    }
 
    Status= RpcServerRegisterIf(pipo_v1_0_s_ifspec, NULL, NULL);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcServerRegisterIf failed – 0x%08x", Status);
        goto cleanup;
    }
 
    LOGINFO(L"RPC Server listening…", 0);

    Status = RpcServerListen(1, mMaxInstances, 0);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcServerListen failed – 0x%08x", Status);
       
        Status= RpcServerUnregisterIf(NULL, NULL, FALSE);
        if ( Status!=RPC_S_OK )
            LOGERROR(L"RpcServerUnregisterIf failed – 0x%08x", Status);

        goto cleanup;
    }

cleanup:
    ExitThread(Status);
    return Status;
}

DWORD CAsyncServer::StartRpcServerThread(LPVOID Param) {
    if ( Param==NULL )
        return 0;
    return ((CAsyncServer*)Param)->RpcServerThread();
}

CAsyncServer::CAsyncServer()
    : mThread(NULL),
      mProtocol(Rpc_NotSupported),
      mMaxInstances(1) {
    mEndpoint[0]= 0;
}

CAsyncServer::~CAsyncServer() {
    StopAndDestroyThread();
}

VOID CAsyncServer::StopAndDestroyThread() {
    if ( mThread ) {
        Shutdown();
        WaitForSingleObject(mThread, INFINITE);

        CloseHandle(mThread);
        mThread= NULL;
    }
}

VOID CAsyncServer::StartStopRpcServer() {
    if ( mThread )
        StopAndDestroyThread();
    else {
        mThread= CreateThread(NULL, 0, CAsyncServer::StartRpcServerThread,
          this, 0, NULL);
        if ( mThread==NULL )
            LOGERROR(L"CreateThread failed – 0x%08x", GetLastError());
    }
}

void __RPC_FAR * __RPC_USER midl_user_allocate(size_t len) {
    return malloc(len);
}

void __RPC_USER midl_user_free(void __RPC_FAR * ptr) {
    free(ptr);
}

void RpcSleep(int Duration) {
    LOGINFO(L"(Sleep) start. duration:%umsec…", Duration);
    Sleep(Duration);
    LOGINFO(L"(Sleep) done.", 0);
}

void Shutdown() {
    RPC_STATUS Status= RPC_S_OK;

    Status= RpcMgmtStopServerListening(NULL);
    if ( Status!=RPC_S_OK )
        LOGERROR(L"(Shutdown) RpcMgmtStopServerListening failed – 0x%08x",
          Status);

    Status = RpcServerUnregisterIf(NULL, NULL, FALSE);
    if ( Status!=RPC_S_OK )
        LOGINFO(L"(Shutdown) RpcServerUnregisterIf failed – 0x%08x", Status);

    LOGINFO(L"(Shutdown) done.", 0);
}

//
//
http://msdn.microsoft.com/en-us/library/windows/desktop/aa378667(v=vs.85).aspx
//

#define ASYNC_CANCEL_CHECK  100
#define DEFAULT_ASYNC_DELAY 10000

void RpcSleepAsync(IN PRPC_ASYNC_STATE pAsync, IN int Duration) {
    int nReply = 1;
    RPC_STATUS Status;
    unsigned long nTmpAsychDelay;
 
    LOGINFO(L"(SleepAsync) start. duration:%umsec…", Duration);

    if (Duration < 0)
        Duration = DEFAULT_ASYNC_DELAY;
    else if (Duration < 100)
        Duration = 100;

    // We only call RpcServerTestCancel if the call takes longer than ASYNC_CANCEL_CHECK ms
    if(Duration > ASYNC_CANCEL_CHECK){
        nTmpAsychDelay= Duration/100;
        for ( int i=0 ; i<100 ; ++i ){
            Sleep(nTmpAsychDelay);
 
            if (i%5 == 0){
                //LOGINFO(L"(SleepAsync) %lu ms…", Duration);
 
                Status=  RpcServerTestCancel(RpcAsyncGetCallHandle(pAsync));
                if ( Status==RPC_S_OK ) {
                    LOGINFO(L"(SleepAsync) canceled.", 0);
                    break;
                }
                else if ( Status!=RPC_S_CALL_IN_PROGRESS ) {
                    LOGINFO(L"(SleepAsync) RpcAsyncInitializeHandle returned 0x%x", Status);
                    exit(Status);
                }
            }
        }
    }
    else
        Sleep(Duration);
 
    Status= RpcAsyncCompleteCall(pAsync, &nReply);
    LOGINFO(L"(SleepAsync) done.", 0);

    if ( Status!=RPC_S_OK ) {
        LOGERROR(L"(SleepAsync) RpcAsyncCompleteCall failed – 0x%08x",
          Status);
        exit(Status);
    }
}

main.cpp

最後のファイル。ほとんど UI 部分の処理です。
IDOK ボタンがクリックされると CAsyncServer::StartStopRpcServer を実行し、待機スレッドを開始します。他には特に何もしません。

//
// main.cpp
//

#include <Windows.h>

#include "AsyncServer.h"

HWND g_Dlg= NULL;
CAsyncServer *g_AsyncServer= NULL;

INT_PTR CALLBACK DlgProc(HWND Dlg, UINT Msg, WPARAM w, LPARAM l) {
    HWND Control= NULL;

    switch ( Msg ) {
    case WM_INITDIALOG:
        g_Dlg= Dlg;
       
        Control= GetDlgItem(Dlg, IDC_COMBO_PROTOCOL);

        for ( PRPC_PROTOCOL p= SupportedProtocols ;
              p->Protocol!=Rpc_NotSupported ; ++p )
            SendMessage(Control, CB_ADDSTRING, NULL,
               (LPARAM)p->FriendlyName);

        SetDlgItemInt(Dlg, IDC_EDIT_INSTANCES, 10, FALSE);
        PostMessage(Control, CB_SETCURSEL, 0, NULL);
        PostMessage(Dlg, WM_COMMAND,
          MAKELONG(IDC_COMBO_PROTOCOL, CBN_SELCHANGE), (LPARAM)Control);
        return TRUE;

    case WM_COMMAND:
        switch ( LOWORD(w) ) {
        case IDCANCEL:
            EndDialog(Dlg, IDOK);
            break;
        case IDOK:
            g_AsyncServer->SetProtocolType(SendMessage(
               GetDlgItem(Dlg, IDC_COMBO_PROTOCOL), CB_GETCURSEL, 0, 0));
            g_AsyncServer->mMaxInstances=
               GetDlgItemInt(Dlg, IDC_EDIT_INSTANCES, NULL, FALSE);
            GetDlgItemText(Dlg, IDC_EDIT_ENDPOINT,
               g_AsyncServer->mEndpoint, MAX_ENDPOINT);           
            g_AsyncServer->StartStopRpcServer();
            break;
        case IDC_COMBO_PROTOCOL:
            if ( HIWORD(w)==CBN_SELCHANGE ) {
                LRESULT Selected= SendMessage((HWND)l, CB_GETCURSEL, 0, 0);
                SetWindowText(GetDlgItem(Dlg, IDC_EDIT_ENDPOINT),
                  SupportedProtocols[
                  min(Selected, Rpc_NotSupported)].DefaultEndpoint);
            }
            break;
        }
        break;
    }
    return FALSE;
}

int WINAPI wWinMain(HINSTANCE hInstance,
                    HINSTANCE,
                    PWSTR pCmdLine,
                    int nCmdShow) {
    g_AsyncServer= new CAsyncServer();
    if ( g_AsyncServer ) {
        DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);
        delete g_AsyncServer;
    }
    return 0;
}

ダイアログボックスの外観

すっかり忘れていました。作成したダイアログボックスはこんな外観です。
起動して、[Named Pipe] を選択して [Start/Stop] をクリックするとこんな感じに待機スレッドが開始されたことが表示されます。それぞれのコントロールの ID は、、、main.cpp からお察し下さい。

image

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #1

以前、同一マシン内での RPC について、名前付きパイプと LPC のそれぞれの方法で通信するクライアントとサーバーを作りました。このときは同期 RPC、すなわちクライアントがメソッドを呼び出すと、サーバー側での処理が終わるまで制御が返ってこない RPC でした。今回は非同期 RPC 通信についてプログラムを書いたので記事にします。

[Win32] [C++] Local RPC over Named Pipe and LPC
http://msmania.wordpress.com/2011/07/10/win32-c-local-rpc-over-named-pipe-and-lpc/

4 回に分けて書くことになりました。今回が #1 のインターフェース定義編です。

#1 – インターフェース定義編
http://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port/

#2 – RPC サーバー編
http://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port-2/

#3 – RPC クライアント編
http://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port-3/

#4 – ネットワーク キャプチャー編
http://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port-4/

非同期 RPC では、クライアントがメソッドを呼び出しても、RPC サーバーの処理に関係なく制御がすぐに返ってきます。このため、実際に RPC サーバーでメソッドの処理が終わったときにコールバックを受ける必要が出てきます。このときのコールバック方法には複数の選択肢があり、いずれかをクライアント側が提示することができます。正確には、メソッドを呼び出すときのパラメーターである RPC_ASYNC_STATE 構造体の RPC_NOTIFICATION_TYPES 列挙値で指定します。

  • コールバックなし
  • イベント オブジェクト
  • APC (Asynchronous Procedure Call)
  • I/O 完了ポート
  • ウィンドウ メッセージ
  • コールバック関数

種類が豊富ですね。APC は使ったことがないのであまり知りませんが、それ以外は何となく想像がつきます。
さて、この中で比較的実装が複雑になりそうな I/O 完了ポートを使ってサンプルプログラムを作ります。ちなみに、MSDN に載っている非同期 RPC のサンプルはイベント オブジェクトを使うものでした。

RPC_ASYNC_STATE structure
http://msdn.microsoft.com/en-us/library/windows/desktop/aa378490(v=vs.85).aspx

RPC_NOTIFICATION_TYPES enumeration
http://msdn.microsoft.com/en-us/library/windows/desktop/aa378638(v=vs.85).aspx

一つの記事で書くにはちょっと複雑なプログラムになってしまったので、まず最初にプログラムの全体像を紹介します。

プロジェクトのフォルダー構造は抜粋するとこんな感じです。
今回は GUI で書きました。64bit ネイティブでビルドしましたが、32bit でも普通にビルドできます。たぶん。

AsyncRpc
│  AsyncCommon.cpp … クライアント/サーバー共通コード
│  AsyncCommon.h
│ 
├─AsyncClient
│      main.cpp
│      AsyncClient.h
│      AsyncClient.cpp
│      resource.h
│      AsyncClient.rc
│             
├─AsyncServer
│  │  main.cpp
│  │  AsyncServer.h
│  │  AsyncServer.cpp
│  │  resource.h
│  │  AsyncServer.rc
│  │ 
│  └─x64
│      └─Debug
│              AsyncClient.exe
│              AsyncClient.pdb
│              AsyncServer.exe
│              AsyncServer.pdb
│             
└─idl
        pipo.idl … インターフェース定義関連
        pipo.acf
        pipo.h
        pipo_c.c
        pipo_s.c

1. インターフェース定義を作る (IDL, ACF ファイル)

まずは短いところから。IDL ファイルと ACF ファイルをテキスト エディターで書きます。ひな型の作成に uuidgen /i コマンドを使うこともできます。(前回の記事参照)

//
// pipo.idl
//
// generated with ‘uuidgen /i /opipo.idl’
// compile with ‘midl pipo.idl’
//
//
http://msdn.microsoft.com/en-us/library/aa367088
//

[
uuid(161b9ab8-1a96-40a6-bf8b-aa2d7ec94b6d),
version(1.0)
]
interface pipo
{
    void RpcSleep(int Duration);
    void RpcSleepAsync(int Duration);
    void Shutdown();
}

IDL ファイルは普通ですね。ACF ファイルはこんな感じです。

//
// pipo.acf
//
//
http://msdn.microsoft.com/en-us/library/aa366717(v=VS.85).aspx
//
 
[
implicit_handle(handle_t pipo_IfHandle)
]
interface pipo
{
    [async] RpcSleepAsync();
}

非同期 RPC にしたい関数には、ACF ファイル内で [async] 属性を付けておきます。詳細はそれぞれのファイルの先頭に書いた MSDN のページを参考にして下さい。

ファイルが書けたら、Windows SDK に含まれる midl.exe で IDL ファイルをコンパイルします。 ACF ファイルは midl が自動的に読み込みます。

> midl pipo.idl
Microsoft (R) 32b/64b MIDL Compiler Version 7.00.0555
Copyright (c) Microsoft Corporation. All rights reserved.
64 bit Processing .\pipo.idl
pipo.idl
64 bit Processing .\pipo.acf
pipo.acf

これで、インターフェースについてのヘッダーとソース ファイルが自動生成されました。

後で書くプログラムの仕様上、クライアント用ソース ファイルの pipo_c.c に含まれるインターフェース ハンドルのグローバル変数を、NULL で初期化しておきます。

これが修正前。

/* Standard interface: pipo, ver. 1.0,
   GUID={0x161b9ab8,0x1a96,0x40a6,{0xbf,0x8b,0xaa,0x2d,0x7e,0xc9,0x4b,0x6d}} */

handle_t pipo_IfHandle;

修正後。

/* Standard interface: pipo, ver. 1.0,
   GUID={0x161b9ab8,0x1a96,0x40a6,{0xbf,0x8b,0xaa,0x2d,0x7e,0xc9,0x4b,0x6d}} */

handle_t pipo_IfHandle= NULL;

この記事はここまで。
次回は RPC サーバーを作ります。

[Win32] [COM] VDS Object Enumeration

NTFS、パーティション マネジメントあたりがマイブームなので、VDS の COM インターフェースを使って各種オブジェクトを列挙するプログラムを書いてみた。

サンプルはこのへん。

Loading VDS (Windows)
http://msdn.microsoft.com/en-us/library/aa383037.aspx

Working with Enumeration Objects (Windows)
http://msdn.microsoft.com/en-us/library/aa383988.aspx

コードは下に貼りますが、特に捻りもない普通のコードです。
ConsumeVDS で IVdsService インターフェースを取得して、EnumVdsObjects でソフトウェア プロバイダーの,インターフェース (IVdsPack) を取得して EnumVdsVolumes, EnumVdsDisks でそれぞれボリュームとディスクを列挙。
Dump***Prop 関数は、GetProperties で取ってきた構造体をパースして表示しているだけなので省略。

そう言えば、衝撃の事実が上記 MSDN ページに。
VDS とダイナミック ディスクは星になるようです。

[Both the Virtual Disk Service and dynamic disks are deprecated as of Windows 8 Consumer Preview and Windows Server 8 Beta, and may be unavailable in subsequent versions of Windows. For more information, see Windows Storage Management API.]

VDS サービスはスタートアップ種別が手動であり、IVdsVolume インターフェースを取得するため、IVdsServiceLoader::LoadService を呼んだときに起動され、インターフェースを破棄するとサービスが停止します。このデザインはパフォーマンス的にあまりよくない気がします。

そんなこんなでソース。

//
// vds.cpp
//
//
http://msdn.microsoft.com/en-us/library/aa383037.aspx
// http://msdn.microsoft.com/en-us/library/aa383988.aspx
//

#include <initguid.h>
#include <vds.h>
#include <stdio.h>

#pragma comment( lib, "rpcrt4.lib" )

void Logging(LPCWSTR fmt, DWORD err) {
    //SYSTEMTIME st;
    //GetSystemTime(&st);
    //wprintf(L"[%d/%02d/%02d %02d:%02d:%02d.%03d] ",
    //    st.wYear,
    //    st.wMonth,
    //    st.wDay,
    //    st.wHour,
    //    st.wMinute,
    //    st.wSecond,
    //    st.wMilliseconds);

    wprintf(fmt, err);
}

#define GET_LODWORD(ll) (((PLARGE_INTEGER)&ll)->LowPart)
#define GET_HIDWORD(ll) (((PLARGE_INTEGER)&ll)->HighPart)

void EnumVdsVolumes(IVdsPack *VdsPack) {
    HRESULT Ret;
    ULONG Fetched= 0;
    IUnknown *Unknown= NULL;
    IEnumVdsObject *EnumVolumes= NULL;

    Ret= VdsPack->QueryVolumes(&EnumVolumes);
    if ( FAILED(Ret) ) {
        Logging(L"IVdsPack::QueryVolumes failed – 0x%08x\n", Ret);
        goto cleanup;
    }

    do {
        IVdsVolume *Volume= NULL;
        IVdsVolumeMF *VolumeMF= NULL;
               
        VDS_VOLUME_PROP PropVol;
        VDS_FILE_SYSTEM_PROP PropFs;

        Ret= EnumVolumes->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsVolume, (void**)&Volume);
        Unknown->Release();
        if ( FAILED(Ret) ) {
            Logging(L"IID_IVdsVolume::QueryInterface failed – 0x%08x\n",
              Ret);
            continue;
        }

        Ret= Volume->GetProperties(&PropVol);
        if ( Ret==S_OK || Ret==VDS_S_PROPERTIES_INCOMPLETE )
            DumpVolumeProp(&PropVol);
        if ( Ret==VDS_S_PROPERTIES_INCOMPLETE )
            wprintf(L"      ** IID_IVdsVolume::GetProperties returned VDS_S_PROPERTIES_INCOMPLETE(0x%08x)\n\n", VDS_S_PROPERTIES_INCOMPLETE);
        else if ( FAILED(Ret) )
            Logging(L"IID_IVdsVolume::GetProperties failed – 0x%08x\n", Ret);

        Ret= Volume->QueryInterface(IID_IVdsVolumeMF, (void**)&VolumeMF);
        Volume->Release();
        if ( Ret!=S_OK )
            Logging(L"IID_IVdsVolumeMF::QueryInterface failed – 0x%08x\n",
              Ret);

        Ret= VolumeMF->GetFileSystemProperties(&PropFs);
        if ( Ret==VDS_E_NO_MEDIA )
            wprintf(L"      ** IID_IVdsVolumeMF::GetProperties returned VDS_E_NO_MEDIA(0x%08x)\n\n", VDS_E_NO_MEDIA);
        else if ( FAILED(Ret) )
            Logging(L"IID_IVdsVolumeMF::GetFileSystemProperties failed – 0x%08x\n", Ret);
        else
            DumpFileSystemProp(&PropFs);

    } while(1);
   
cleanup:
    if ( EnumVolumes )
        EnumVolumes->Release();

    return;
}

void EnumVdsDisks(IVdsPack *VdsPack) {
    HRESULT Ret;
    ULONG Fetched= 0;
    IUnknown *Unknown= NULL;
    IEnumVdsObject *EnumDisks= NULL;

    Ret= VdsPack->QueryDisks(&EnumDisks);
    if ( FAILED(Ret) ) {
        Logging(L"IVdsPack::QueryDisks failed – 0x%08x\n", Ret);
        goto cleanup;
    }

    do {
        IVdsDisk *Disk= NULL;
        VDS_DISK_PROP DiskProp;

        Ret= EnumDisks->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsDisk, (void**)&Disk);
        Unknown->Release();
        if ( FAILED(Ret) ) continue;

        Ret= Disk->GetProperties(&DiskProp);
        if ( FAILED(Ret) )
            Logging(L"IID_IVdsDisk::GetProperties failed – 0x%08x\n", Ret);
        else
            DumpDiskProp(&DiskProp);

    } while (1);

cleanup:
    if ( EnumDisks )
        EnumDisks->Release();

    return;
}

void EnumVdsObjects(IVdsService *VdsService) {
    HRESULT Ret;

    ULONG Fetched= 0;
    IEnumVdsObject *EnumSwProviders= NULL;
    IEnumVdsObject *EnumPacks= NULL;
    IUnknown *Unknown= NULL;

    IVdsProvider *Provider= NULL;
    IVdsSwProvider *SwProvider= NULL;
    IVdsPack *VdsPack= NULL;

    VDS_PROVIDER_PROP ProviderProp;
    VDS_PACK_PROP PackProp;

    Ret= VdsService->QueryProviders(VDS_QUERY_SOFTWARE_PROVIDERS,
      &EnumSwProviders);
    if ( Ret!=S_OK ) {
        Logging(L"IVdsService::QueryProviders failed – 0x%08x\n", Ret);
        return;
    }

    do {
        Ret= EnumSwProviders->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsProvider, (void**)&Provider);
        Unknown->Release();
        if ( FAILED(Ret) ) continue;

        Ret= Provider->GetProperties(&ProviderProp);
        if ( FAILED(Ret) )
            Logging(L"IID_IVdsProvider::GetProperties failed – 0x%08x\n",
              Ret);
        else
            DumpProviderProp(&ProviderProp);

        Ret= Provider->QueryInterface(IID_IVdsSwProvider,
          (void**)&SwProvider);
        Provider->Release();
        if ( FAILED(Ret) ) continue;

        Ret= SwProvider->QueryPacks(&EnumPacks);
        SwProvider->Release();
        if ( FAILED(Ret) ) continue;

        do {
            Ret= EnumPacks->Next(1, &Unknown, &Fetched);
            if ( Ret==S_FALSE ) break;
            if ( FAILED(Ret) ) goto cleanup;

            Ret= Unknown->QueryInterface(IID_IVdsPack, (void**)&VdsPack);
            Unknown->Release();
            if ( FAILED(Ret) ) continue;

            Ret= VdsPack->GetProperties(&PackProp);
            if ( FAILED(Ret) )
                Logging(L"IID_IVdsPack::GetProperties failed – %08x\n", Ret);
            else
                DumpPackProp(&PackProp);
           
            EnumVdsDisks(VdsPack);
            EnumVdsVolumes(VdsPack);

        } while (1);

        EnumPacks->Release();
        EnumPacks= NULL;

    } while (1);

cleanup:
    if ( EnumPacks )
        EnumPacks->Release();

    if ( EnumSwProviders )
        EnumSwProviders->Release();

    return;
}

void ConsumeVDS() {
    HRESULT Ret= 0;
    IVdsServiceLoader *VdsServiceLoader= NULL;
    IVdsService *VdsService= NULL;
   
    Ret = CoInitialize(NULL);
    if ( Ret!=S_OK ) {
        Logging(L"CoInitialize failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    Ret= CoCreateInstance(CLSID_VdsLoader,
        NULL,
        CLSCTX_LOCAL_SERVER,
        IID_IVdsServiceLoader,
        (void **) &VdsServiceLoader);
    if ( Ret!=S_OK ) {
        Logging(L"CoCreateInstance(IVdsServiceLoader) failed – 0x%08x\n",
          Ret);
        goto cleanup;
    }
   
    Ret= VdsServiceLoader->LoadService(NULL, &VdsService);
    VdsServiceLoader->Release();
    VdsServiceLoader= NULL;
   
    if ( Ret!=S_OK ) {
        Logging(L"IVdsServiceLoader::LoadService failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    Ret= VdsService->WaitForServiceReady();
    if ( Ret!=S_OK ) {
        Logging(L"IVdsService::WaitForServiceReady failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    EnumVdsObjects(VdsService);

cleanup:
    if ( VdsService )
        VdsService->Release();

    if ( VdsServiceLoader )
        VdsServiceLoader->Release();

    CoUninitialize();

    return;
}

フォロー

新しい投稿をメールで受信しましょう。

現在27人フォロワーがいます。