第10回  「ファイルへの書き込み関数」



自作関数を読む前に

ここから先を読む前に、古いサンプルプログラムを使用されている場合は、
新しいサンプルプログラム(2000/08/01 更新)をDLしておいて下さい。
関数の中身が随分変わっています。


さて、自作関数の説明をする前に一つお話しておく事があります。
それは、以降の説明が理解できなくても、特に問題は無いという事です。

例えば、冷蔵庫や電子レンジなどの電化製品は、その構造を知らなくても使う事はできますよね。
それと同じで、関数というのは使い方さえ正しく理解しておけば、内部構造までは知らなくても構わないのです。

勿論、理解できた方が望ましいのは言うまでもありません。
そうすれば、自分で関数を作ったり、改良したりする事ができるわけですから。

これらの点を踏まえた上で、以降の説明を読んでいって下さい。


ファイル書き換え関数(BinFileEdit)

では、早速ファイル書き換えを行う自作関数を見ていきましょう。
この関数は、「ファイルのフルパス名」と「改造コード」を受け取り、指定のファイルを書き換える関数です。


BinFileEdit ソースプログラム1
Function BinFileEdit(ByVal FileName As String, ByVal Code As String) As Long
On Error GoTo ErrSet

01>  Dim StartAddress As Long  '書き換えを開始するアドレスを格納
02>  Dim EndAddress As Long   '書き換えを終了するアドレスを格納
03>  Dim Data As String      '書き込むデータを一時的に格納
04>  Dim PutValue As Byte    '書き込えるデータを格納
05>  Dim i As Long, j As Long   'カウンタ
06>  Dim FileNumber As Long   'ファイル番号

07>  '改造コードの種類を判定&処理
08>  If InStr(1, Code, ">") Then
09>    StartAddress = CLng("&h" & Left(Code, InStr(1, Code, ">") - 1))
10>    EndAddress = CLng("&h" & Mid(Code, InStr(1, Code, ">") + 1, _
          InStr(1, Code, "-") - InStr(1, Code, ">") - 1))
11>  Else
12>    StartAddress = CLng("&h" & Left(Code, InStr(1, Code, "-") - 1))
13>    EndAddress = StartAddress
14>  End If
15>  Data = Mid(Code, InStr(1, Code, "-") + 1)

16>  '改造コードの1文字目が「S,M,L,$」のいずれかであれば、対応する16進数の文字列に変換する
17>  Select Case UCase(Left(Data, 1))
18>    Case "S", "M", "L", "$"
19>      Data = DecOrAsciiToHex(Right(Data, Len(Data) - 1), Left(Data, 1))
20>    Case Else
21>  End Select

22>  If Len(Data) Mod 2 <> 0 Then
23>    Data = "0" & Data
24>  End If


長いので2回に分けて説明します。
ここで紹介している部分は、改造コードの表記の差を吸収している部分です。

8〜14行目では、書き換えを行う範囲の決定をしています。
MaferLite の改造コードの書式では、以下のようになっています。

1.X>Y−FF 「X〜Yのアドレス部分を、FFに書き換える」
2.X−FF   「Xのアドレス部分を、FFに書き換える」

つまり、このどちらの書き方をされていても正しく処理できるようにしているのです。
一ヶ所しか書き換えない場合でも、「EndAddress」を指定しているのはこのためです。

では、9行目の説明をしていきます。
ここでは、上記の1.のパターンの時に、「>」の左側(アドレス)の部分を「StartAddress」に格納しています。

「Left」や「InStr」については第9回で既に説明済みなので省略しますが、
これは、「>」の左側の文字数を調べて取り出しているんでしたね。

CLng」というのは、与えられたデータを「Long型」に変換するための標準関数です。
「Left」で取り出したアドレス部分は、「String型」(文字列型)です。

つまり、このままでは数値とみなされませんので、ファイル書き換えの際に不都合が生じます。

例えば、「1」というデータがあったとしましょう。
これが数値型であれば、足し算や引き算等の計算を行う事ができますが、
文字列型では、そういった事はできません。「あ+ん」という計算式はおかしいですよね?

そんなわけで、計算などに使用する場合の変数は、基本的に数値型でなくてはなりません。

しかし、今回はそれだけではなく、取り出したアドレスに「&h」という文字を左側にくっつけています。
これは一体何のためでしょうか?

実は、単に「CLng」を使って数値型にするだけでは、「10進数」になってしまうのです。
ですが、文字列の左に「&h」が付いていると、「16進数」として認識してくれます。
アドレスは16進数で指定する必要があるので、このような処理を行っているわけです。


15行目では、「X−FF」の「−」より右の部分、つまり書き換える値を取り出しています。
そして、16〜21行目では、その書き換える値を16進数に変換する処理を行っています。

「X−FF」のように、最初から16進数で指定されている場合はよいのですが、
「X−M99」という指定をされた場合には、「M99」を適切な16進数に変換する必要があります。

MaferLite の書式で「M99」というのは、10進数「99」を2バイトの16進数で表したものと同値でした。
その変換を行ってから、「Data」に書き戻しているのですが、その際に「DecOrAsciiToHex
という自作関数を使用しているため、詳しくは後述します。


22〜24行目では、「Data」の文字数を2の倍数(偶数)に揃えています。
第3回でバイナリエディタの画面を見た時の事を思い出して下さい。

1バイトの値は、必ず2文字で表現されていましたよね?
実際に書き換える時も、やはり1バイト(2文字ずつ)書き換えていきます。

ところが通常、数値型のデータというのは、上位の桁の「0」は省略してしまいます。
例えば、「0123」なら「123」のように表示されるという事です。

そこで、「Data」の文字数を「2」で割った余りが「0」でないならば、
言い換えると「奇数」ならば、左に「0」を追加するというわけです。


BinFileEdit ソースプログラム2
FileNumber = FreeFile  '使用可能なファイル番号を取得
Open FileName For Binary As #FileNumber
For i = 0 To EndAddress - StartAddress Step Len(Data) / 2
  For j = 0 To Len(Data) / 2 - 1
    PutValue = CLng("&h" & Mid(Data, j * 2 + 1, 2))
    '一度に書き込む値は1バイト(00〜FF)なので、一回に2文字書き込むことになる。
    Put #FileNumber, StartAddress + i + j + 1, PutValue
  Next j
Next i

Close #FileNumber
BinFileEdit = 0
Exit Function

ErrSet:
  BinFileEdit = Err.Number
End Function



後半の部分では、実際の書き換えを行っています。

1,2行目では、ファイルを開く処理を行っています。
この時、「For Binary」を指定して、バイナリモードで開く必要があります。

基本的にバイナリファイルを開くときには、バイナリモードを使用します。
バイナリファイルを開くのに、テキストエディタを使わないのと同じ事ですね。

3〜9行目で、開いたファイルへの書き込みを行っています。
見てのように2重ループになっており、値を16進数の数値データに変換しつつ、
1バイト(2文字)ずつ書き込みを行っています。

外側のループで、指定したアドレスの範囲までの繰り返しを行い、
内側のループで、書き込むデータの文字数に応じた分だけの書き込みを行っています。

例えば、「X−F1F2」というコードが指定されていた場合は、
外側のループは1回、内側のループは2(←文字数/2)×1(←外側ループ分)回実行されます。

同様に、「1>4−FF」というコードであれば、
外側のループは4回、内側のループは1×4回実行されます。

もう1つ見ておくと、「1>4−F1F2」ならば、
外側のループは2回、内側のループは2×2回という事になります。

分かりましたでしょうか?
この辺りは、文章での説明が難しいため、実際にトレースして確かめておいて下さい。


最後に使用したファイルを閉じ、「BinFileEdit」関数が成功した印として「0」を戻り値に格納しています。
基本的に、開いたファイルは必ず閉じなくてはなりません。
ファイルに限らず、オープンしたものは必ずクローズするものだと覚えておくと良いでしょう。

本やノートでいうと、「開かないと使えない」、「使い終わったら必ず閉じる」という所です。
開きっぱなしにしておくと、後で痛い目をみますのでご注意下さい。

よくある失敗として、クローズの処理は書いてあるのに、エラーが生じたときにジャンプしてしまい
肝心のクローズの処理が実行されない、というパターンがあります。


最後の行では、途中で何らかのエラーが生じた場合は「ErrSet:」へジャンプし、
エラー番号を関数の戻り値として返すための処理をしています。


16進数の変換

それでは、先ほど後回しにすると言っていた、10進数の数値や文字列を16進数に変換する関数を見ていきます。
引数として、「変換するデータ」、「変換するデータの元の型」を指定します。


DecOrAsciiToHex ソースプログラム
Function DecOrAsciiToHex(ByVal DecOrAsciiData As String, ByVal DataType As String) As String
Dim ReturnData As String '戻り値が確定するまでの値を格納
Dim CodeString As String '文字コードに対応するアスキー文字列に変換した16進数を一時的に格納
Dim CodeStringCnt As Integer '何文字目を変換しているか

ReturnData = ""

'1文字目が、「S,M,L,$」のいずれかであれば、対応する16進数の文字列に変換する
Select Case DataType
  Case "S"
    ReturnData = BigEToLittleE(Hex(CLng(DecOrAsciiData)), 1)
  Case "M"
    ReturnData = BigEToLittleE(Hex(CLng(DecOrAsciiData)), 2)
  Case "L"
    ReturnData = BigEToLittleE(Hex(CLng(DecOrAsciiData)), 4)
  Case "$"
    For CodeStringCnt = 1 To Len(DecOrAsciiData)
      CodeString = Hex(Asc(Mid(DecOrAsciiData, CodeStringCnt, 1)))
      ReturnData = ReturnData & CodeString
    Next CodeStringCnt
  Case Else
End Select

DecOrAsciiToHex = ReturnData
End Function



この関数では、変換するデータの元の型として以下の4種類があります。

S・・・10進数(1バイト)
M・・・10進数(2バイト)
L・・・10進数(4バイト)
$・・・文字列

この中では「$」が最も簡単なので、こちらから説明していきましょう。
第3回でバイナリエディタの画面を見た時の事を思い出して下さい。

アルファベットの小文字の「」は、16進数では「61」で表されていましたよね。
つまり、「$a」という指定がされていれば、「61」に変換すればよいのです。

具体的には、与えられた文字列を1文字ずつ取り出し、その文字を示す16進数に変換していきます。
Asc」という関数は、与えられた文字を示す数値を10進数を返す標準関数です。
後はその値を「Hex」関数を使用して、16進数にすればOKです。


で、問題なのはこの次です。
とりあえず「M」についてのみ説明していきます。(「S」「L」も基本は同じ)

例えば、「M999」という指定がされた場合を考えてみましょう。
10進数の「999」は、16進数では「3E7」です。

ということは、前に「0」を付けて「03E7」とすれば良いのでしょうか?
実はそう簡単にはいかないのです。

結論から言いますと、この場合は「E703」とする必要があります。
何故でしょうか?

実は、x86系のCPU(ペンティアム等)はリトルエンディアンを採用しているため、下位バイトが前にくるのです。
まあ、リトルエンディアン云々はさておき、こうなるんだなぁぐらいに覚えておいて下さい。
(ビッグエンディアンであれば、このような変換の必要はありません)

例えば、「3,000,000」を変換する場合を考えてみましょう。
単に16進数に変換すると「2DC6C0」となります。
後はこれを1バイト(2文字)単位で前後を逆転させて「C0C62D」となるわけです。

この前後の変換を行っているのが、「BigEToLittleE」です。
では、この関数を見ていきましょう。


BigEToLittleE ソースプログラム
Function BigEToLittleE(Src As String, ByVal Keta As Long) As String

Dim i As Long

'バイト数を2倍にして、桁数に変換する
Keta = Keta * 2
'上位の桁の"0"を補い、桁数を揃える
Src = Right(String(Keta, "0") & Src, Keta)

For i = Keta - 1 To 1 Step -2
'1バイトずつ左右を反転
  BigEToLittleE = BigEToLittleE & Mid(Src, i, 2)
Next i

End Function



引数として、「元の16進数」、「バイト数」が与えられます。
そして、1バイトは2文字で表されますので、バイト数を2倍して桁数を求めます。

続いて、上位の桁に「0」を補って、元の16進数の長さをバイト数と同じに揃えます。
例えば、元のデータが「FF」、バイト数が「2」だとすると、「00FF」にするわけです。

実際の処理では、とりあえず指定したバイト数分の「0」を左側にくっつけておいて、
それから、必要な文字数分だけ右側から取り出しています。

上記の例では、「FF」に2バイト分の「0」をくっつけるので、「0000FF」となり、
そこから2バイト分の文字を右側から取り出して、「00FF」を得ています。

この時使用している「String」は、指定した文字数だけ、指定の文字を並べた文字列を返す標準関数です。
今回は、桁数分だけ「0」という文字を並べた文字列を得るために使用しています。

後は、そうしてできた文字列を、後ろから2文字ずつ取り出して格納していけばできあがりです。

ところで、勘の良い方は気づかれたかもしれませんが、指定されたバイト数が1バイトの時は
実質的に変換を行っていません。

与えられたデータが1バイトという事は、例えば「FF」というデータが与えられた場合は、
変換後でも「FF」となるわけですから・・・


では、今回はここまでです。
次回はメモリ・レジストリ書き換えの為に必要となる、APIの使用法について解説していきます。



前に戻る     次に進む

講座の初めに戻る