[FileSystemObject] ファイルを ASCII 形式で開いて WriteLine したら VBScript 実行エラー (800A0005)

今回の内容は、「Unicode 文字テーブルと Shift-JIS テーブルでアサインされたコードが違う文字を ANSI アプリケーションで扱うと文字化けすることの対処方法と原因について」です。
まず、"文字化け" とは何かということですが、端的にいってしまえば "本来表示されるべき文字と異なる文字が表示されてしまう状態" です。皆さんも文字記号が "?" に変化してしまうという現象をよく目にされるのではないでしょうか。

このトピックの元になったお問い合わせは、Windows Update API を用いて、Windows Update の "更新履歴の表示" に表示される更新プログラムの履歴を取得し、そのタイトル(更新履歴の表示に表示されている更新プログラム名です) をウインドウに表示したり、VBScript の FileSystemObject (FSO) でログ出力するといったことをされているお客様からよせられたものでした。
一部の更新プログラムでは、タイトルをあらわす文字列の中に不思議なコードが存在しているものがあり、そのタイトル文字列を取得して表示すると文字列が途切れたり、FSO での出力処理でスクリプト エラーが発生したりするということでした。

- 現象
この VBScript では、ループで更新プログラム一個 = ファイル一行として FileSystemObject の WriteLine メソッドでファイルに書き込みをしているのですが、一部の更新プログラムのタイトルを引数として指定すると、以下のようなエラーが発生します。

エラー : プロシージャの呼び出し、または引数が不正です。
コード : 800A0005
ソース : Microsoft VBScript 実行時エラー

image

- 対処
ファイルを Unicode ファイルとして開くことで、処理は続行できます。
このケースの場合、OpenTextFile() メソッドを使用していましたので、第 5 引数に "TristateTrue" = -1 を指定することによって Unicode ファイルとして扱うことが出来ます。

・変更前
Set objFile = objFSO.OpenTextFile("test.txt", 2, 1)
・変更後
Set objFile = objFSO.OpenTextFile("test.txt", 2, 1, -1)

image

- 原因
ファイル操作に使用する FileSystemObject (FSO) はファイルを扱う際にファイルのフォーマットを指定することができます。フォーマット指定は OpenTextFile() メソッドを使用する際の第 5 引数 format で設定します。この引数はオプションですが指定しない場合、既定で ASCII ファイルとして扱います。
ASCII ファイルを扱う設定では書き込み文字列の中に Unicode 文字列が含まれていた場合、文字コードのフォーマットが適切ではないと判断され、結果として書き込みに失敗することがあります。
U+2013 en ダッシュ、U+00A0 ノーブレーク スペース、U+00AE 登録商標などを含む場合にこの現象が発生することを確認しています。

(参考)
OpenTextFile メソッド
https://msdn.microsoft.com/ja-jp/library/cc428044.aspx
OpenAsTextStream メソッド
(※第 5 引数にあたる値の数値の説明があります)
https://msdn.microsoft.com/ja-jp/library/cc428042.aspx

おまけ : Windbg で見てみよう
さて、これをどうやって確認したかですが、Windbg ツールを使って簡単に調べることが出来ます。
今回私が行った調査は、まず問題を再現させたあと、以下のポイントに MsgBox を仕掛けることからはじめました。
スクリプトは、問題の文字コードを含んだ更新プログラムまでは処理が進むので、ファイルに出力 (WriteLine) することができました。出力されたファイルと、環境の Windows Update の更新履歴の表示を見比べ、どこの部分まで処理が進んでいるかを確かめたところ、ATI 社の更新プログラムの前まで進んでいることがわかりました。そこで、問題ではないかとめぼしをつけた更新プログラムまでの更新プログラム個数が 1351 であるということを確認しました。

ポイント 1 : OpenTextFile メソッドの直前
例)
MsgBox "TEST" ' ← ここを追加
Set objFile = objFSO.OpenTextFile("test.txt", 2, 1)

ポイント 2 : 1348 個目あたりかどうか判定するループを施し、問題のプログラムの直前のタイミングで MsgBox を表示させる
例)
(実際にはこの上に for 文がある)
Set objHistoryEntry = objHistoryEntryCollection.Item(i)
if i > 1347 Then
MsgBox "test" & i
WScript.Echo "-----------------------"
WScript.Echo objHistoryEntry.Operation
WScript.Echo GetOperationMeaning(objHistoryEntry.Operation)
WScript.Echo objHistoryEntry.Date
WScript.Echo objHistoryEntry.ResultCode
WScript.Echo GetResultCodeMeaning(objHistoryEntry.ResultCode)
WScript.Echo objHistoryEntry.Title
WScript.Echo "-----------------------"
End If

これで、管理者権限でコマンドプロンプトを開いたら (Vista 以降の場合)、Cscript でも WScript でもいいので、実行してやってください。
そうすると、ポップアップが出てきます。
このタイミングで、管理者権限で Windbg を立ち上げます。シンボルパスは、皆さんが利用できるパスで検証しています。

SRV*C:\Symbols.Pub*https://msdl.microsoft.com/download/symbols

[File] メニューから、[Attach to a process] を選択し、"Wscript.exe" を指定して [OK] を押します。
image

こういう風にでてきたら、アタッチ成功です。image

さて、ここからが問題です。ブレークポイントを指定するといったって、FileSystemObject って、いったい何のモジュールなのでしょうか。
Bing でサーチするとこんなドキュメントが。

image

Visual Basic で FileSystemObject を使用する方法
https://support.microsoft.com/kb/186118/ja
"FileSystemObject は、Scrrun.dll に含まれています。"

これで、Scrrun.dll が実体だということがわかりました。では、Scrrun.dll のどの関数が、スクリプトで使われている関数なんでしょうか。
探し方のコツは、"*" ワイルドカードを使うことです。今回、ストリームが何かを確認したいので、OpenTextFile と Write という関数あたりかな、と推測しておきます。Scrrun.dll のシンボルを .reload /f で読み込ませて、x コマンドを使うと探せます。実際にやって見ましょう。
lmvm <ファイル名> で、ファイルの詳細を見ることが出来ます。
lmvm scrrun で見てみると、(pdb symbols) と出ているので、シンボルが読めてるとわかります。

image

参考 : シンボルが読めていない場合

image

さて、それではそれぞれ関数を探してみましょう。x <ファイル名>!*<関数に含まれている文字列> などで DLL の中で使われている実際の関数名や、関数のアドレスがわかります。

0:004> x scrrun!*OpenTextFile*
6d552f89 scrrun!DoOpenTextFile = <no type information>
6d5673eb scrrun!DoOpenTextFile = <no type information>
6d552940 scrrun!CFileSystem::OpenTextFile = <no type information>

名前的に、一番したの 6d552940、scrrun!CFileSystem::OpenTextFile がそれっぽいですね。MSDN を見てみますと、OpenTextFile はわかりづらいんですが、OpenAsTextStream を見ると、第五引数 format で、-1 を指定したら unicode、0 もしくは -2 が指定されていたら ASCII (ANSI) ファイルになるというようです。

--------------------------------------------
引数 format の設定値は次のとおりです。

定数 値 内容
TristateUseDefault -2 システム デフォルトを使ってファイルを開きます。
TristateTrue -1 ファイルを Unicode ファイルとして開きます。
TristateFalse 0 ファイルを ASCII ファイルとして開きます。

[https://msdn.microsoft.com/ja-jp/library/cc428042.aspx](https://msdn.microsoft.com/ja-jp/library/cc428042.aspx "https://msdn.microsoft.com/ja-jp/library/cc428042.aspx

")

--------------------------------------------

ところが、Windbg は kv コマンドをつかっても、第三引数までしか出ません。どうしたら、第四引数以降がわかるのでしょうか。
答えは、Child EBP から見ていく、です。
ためしに、scrrun!CFileSystem::OpenTextFile の ChildEBP = 0023ec38 をダンプしてみます。ちょびっとだけだと寂しいので、豪気に L50 とか指定して、たくさん表示してやりましょう。といっても、見るところはちょっとなんですが。
見てみると…。

0:000> kvn
# ChildEBP RetAddr Args to Child
00 0020ee18 76797951 02de1118 030d6844 00000002 scrrun!CFileSystem::OpenTextFile (FPO: [6,68,0])

image

これが、

image

です。続いてますね、引数。これをみると、第五引数 format に相当するのは、00000000 です。第五引数 format を省略したバージョンです。0 が入ってますね。ASCII です。 ちなみに、Args to Child で始まるところから、ちょうど引数が開始されます。以下の場合、第一引数 object は 030d6844、第三引数 iomode は 2 であることがわかります。

上記例では、第一引数 = 03101138、第二引数 = 03943cac、第三引数 = 00000002、第四引数 = 0000ffff、第五引数が 00000000 となります。

image

つぎは処理をそのまま進めちゃいます。上の設定をしていれば、問題が発生する直前でまたメッセージボックスが出て止まるはずです。
このタイミングで、一時停止ボタンを押してとめ、こんどは WriteLine を探します。要領はさっきの OpenTextFile と一緒です。

↓一時停止ボタン

image

0:004> x scrrun!*WriteLine*
6d55450c scrrun!CTextStream::WriteLine = <no type information>
0:004> bp 6d55450c

こうして特定できました。

image

MSDN で WriteLine の説明を見ると、書き込み対象文字列は第二引数のようです。

object.WriteLine([string])引数
object
必ず指定します。TextStream オブジェクトの名前を指定します。
string
省https://ァイルに書き込むテキストを指定します。省略した場合は、改行文字だけがファイルに書き込まれます。
https://msdn.microsoft.com/ja-jp/library/cc428063.aspx

どうも、見てみると化けてる文字があるようです。でも、この時点ではこれは実際のデータが壊れてるわけではなくて、Windbg の表示の問題です。しかし、化けるということは、こいつがトリガーになっている可能性があるかもしれないと疑うには余りあるかと思います。

image

どんな文字コードが入っているのか見てみます。dw コマンドで一文字ずつだせるように見ていきます。途中で切れてしまいそうになるので、L100 をまたつけて、長々と出力してやります。
image

これでみてみると、A = 0031、M = 0009、 D = 0044 とかのようですが、この辺は Shift-JIS でも共通で、0x31、0x09、0x44 と共通です。一見、ASCII でファイルを扱ってもいけるようですが、2013 って入ってますね。これが気になります。Shift-JIS で 2013 って、ないです。かわりに、Unicode であれば、U+2013、en ダッシュという文字があります。en ダッシュは、いわゆる “ハイフン” の仲間のひとつです。

image

たしかに、Windows Update の履歴でも一致します。U+2013 はデバッガ上の文字をダンプしたところに、2 つだけあります。実際、履歴でも 2 つあります。これできまりですね。

U+2013 は Shift-JIS だと、(0x829C) です。もし、ASCII でファイルストリームを開いていると、ANSI ⇒ Unicode 変換が発生し、文字化け等が起こる可能性があります。もしファイルを Unicode で開いていれば、取得した文字列をそのまま扱うので Unicode 文字であれば、こうした変換が起こることはありません。

ちなみに、実際の OS のソース コードでは、ファイルのフォーマットと文字列のフォーマットを内部でフラグとして持っていて、書き込みの際に合致しない場合に上記の実行時エラーを出すという処理をしていました。例外が発生したとかではないので、デバッグとかはちょっと面倒です。今回は実際に内部をみたところ、ファイルは ASCII なのに、文字列のフォーマットが Unicode であるというフラグがそれぞれたっていて、マッチングするところで処理が終了していました。

あとは細々とステップ実行 (p、t) などをしたり、行き過ぎたらまたデバッグを最初のアタッチからやり直したりして調査します。

image

次回は、この文字列を C++ の GUI で扱った場合の文字化けについて記載したいと思います。

~ ういこう@garbled ~