アプリケーションからのI/O Controlをドライバで受け取る方法


皆さん、こんにちは。A寿です。


 


突然ですが、皆さんは象の背中に乗ったことはありますか?・・・このお話にご興味のある方は本文の最後の【閑話休題】までどうぞ。


 


 


さて、先日は、「アプリケーションからSCSIコマンドを発行する方法」として、I/O ControlSCSIコマンドを発行する方法をご紹介いたしました。そのI/O Controlをドライバ側でどのように受け取ればよいのかが気になるところではないかと思います。また、特定のI/O ControlSCSIコマンドを自分が開発しているドライバで受け取って、何らかの処理を加えたいと考える方もいらっしゃるかと思います。そこで、本日は、「アプリケーションからのI/O Controlをドライバで受け取る方法」をご案内したいと思います。


 


  Storage Samples


  http://msdn.microsoft.com/en-us/library/dd163407.aspx


 


のドキュメントに、たくさんのストレージ サンプル ドライバが紹介されておりますが、今回はディスククラスドライバの上位フィルタドライバのサンプルである、diskperfドライバを使って、前回のSPTIアプリケーションが発行したIOCTL_SCSI_PASS_THROUGHI/O Controlを受け取って、そのI/O Controlと一緒に渡されるSCSI_PASS_THROUGH構造体の中身をデバッガに表示させてみたいと思います。表示結果を前回のブログと比較することにより、確かにアプリケーションからドライバにSCSIコマンドを渡せていることがわかると思います。


 


まず、一般的な、アプリケーションがI/O Controlを発行した場合に、それを受け取るドライバ側の処理を説明いたしますと、次の手順となります。


 


  (1) ドライバのIRP_MJ_DEVICE_CONTROLのコールバック(*1)で、目的のI/O Controlが入ってきたかどうかを判断する


  (2) 目的のI/O Controlが入ってきていたら、そのI/O Controlに対する処理を行う


 


      (*1) コールバックとは、K里さんのブログ「DriverObject DriverEntry」で「Dispatch Routine」と言われているものと同じです。


          dispatch routineの言葉の定義は、例えば、


            Windows Driver Kit: Glossary D  (http://msdn.microsoft.com/en-us/library/ms789534.aspx) の項目に


            dispatch routine


              An IRP-processing routine in a kernel-mode driver. Drivers export entry points for these routines through a dispatch table


              in the DRIVER_OBJECT structure.


          と載っています。


 


たったこれだけなので、非常に簡単です。しかも、ストレージの分野に限らず、I/O Controlを処理する必要があるカーネルドライバであれば、WDKのサンプルにたいてい(1)(2)の処理が含まれていますので、これを真似することができます。


 


それでは、SPTIアプリケーションが、IOCTL_SCSI_PASS_THROUGHMODE SENSESCSIコマンドをあるドライブレターを持つディスクに発行したとして、それを受け取るdiskperfドライバの動きを見ていきましょう。


 


 


(1) ドライバのIRP_MJ_DEVICE_CONTROLのコールバックで、目的のI/O Controlが入ってきたかどうかを判断する


 


diskperfドライバのサンプルは、


 


  C:\WinDDK\6001.18002\src\storage\filters\diskperf


 


のフォルダにあります。diskperfドライバのサンプルの説明は、このフォルダ内のdiskperf.htmにもありますし、


 


  DiskPerf Filter Driver


  http://msdn.microsoft.com/en-us/library/dd163417.aspx


 


にもあります。このdiskperfフォルダのdiskperf.cdiskperfドライバのソースコードのほぼ全てが含まれます。


 


ドライバのIRP_MJ_DEVICE_CONTROLのコールバックは、DriverEntry()から見つけます。diskperf.c276行目から以下のようにDriverEntry()が始まっています。


 






276 NTSTATUS


277 DriverEntry(


278     IN PDRIVER_OBJECT DriverObject,


279     IN PUNICODE_STRING RegistryPath


280     )


 


そこからDriverEntry()の中を下へ進んでいきますと、


 






336     //


337     // Set up the device driver entry points.


338     //


339


340     DriverObject->MajorFunction[IRP_MJ_CREATE]          = DiskPerfCreate;


341     DriverObject->MajorFunction[IRP_MJ_READ]            = DiskPerfReadWrite;


342     DriverObject->MajorFunction[IRP_MJ_WRITE]           = DiskPerfReadWrite;


343     DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  = DiskPerfDeviceControl;


344     DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL]  = DiskPerfWmi;


345


346     DriverObject->MajorFunction[IRP_MJ_SHUTDOWN]        = DiskPerfShutdownFlush;


347     DriverObject->MajorFunction[IRP_MJ_FLUSH_BUFFERS]   = DiskPerfShutdownFlush;


348     DriverObject->MajorFunction[IRP_MJ_PNP]             = DiskPerfDispatchPnp;


349     DriverObject->MajorFunction[IRP_MJ_POWER]           = DiskPerfDispatchPower;


 


のように、ドライバのコールバックの登録をするために、DriverObjectMajorFunctionを設定しているところが見つかります。IRP_MJ_DEVICE_CONTROLのコールバックは、上記の343行目のように、「DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  = 」となっているところに代入している関数名を見れば、わかります。diskperfでは、DiskPerfDeviceControl()の関数、ということになります。


 


それでは、diskperf.c1287行目から始まるDiskPerfDeviceControl()を見ていきましょう。


 






1287 NTSTATUS


1288 DiskPerfDeviceControl(


1289     PDEVICE_OBJECT DeviceObject,


1290     PIRP Irp


1291     )


 


このサンプルを見ますと、ドライバでのI/O Controlの一般的な処理がわかると思います。DiskPerfDeviceControl()そのものの処理の説明をしてしまうと本題から外れるので省略しますが、ドライバでのI/O Controlの一般的な処理としては、だいたい次の1-1.1-3.のような処理をします。


 


1-1. 1st parameterが指しているDEVICE_OBJECT構造体から、DEVICE_EXTENSION構造体を受け取ります。


 


DiskPerfDeviceControl()では、1314行目の処理に該当します。


 






1314     PDEVICE_EXTENSION  deviceExtension = DeviceObject->DeviceExtension;


 


DEVICE_EXTENSION構造体とは、ドライバ開発者がデバイス単位に持たせたい固有の情報を定義するための構造体です。ドライバが管理するデバイスの情報は、DEVICE_OBJECT構造体に持たせるのですが、DEVICE_OBJECT構造体は開発者が定義を変更することはできません。これは、DEVICE_OBJECT構造体をドライバ間など様々なモジュールとやり取りする必要があるためです。diskperfドライバにも、このドライバ固有のDEVICE_EXTENSION構造体があり、diskperf.c46行目から116行目にかけてその定義を見ることができます。またdiskperf内の様々な関数で、DEVICE_EXTENSION構造体のメンバ変数を変更する処理が行われていることがわかります。ここでは掲載しませんが、ご興味のある方はご覧ください。


 


1-2. 2nd parameterIRP構造体へのポインタをIoGetCurrentIrpStackLocation()に入れて、IO_STACK_LOCATION構造体を受け取ります。


 


DiskPerfDeviceControl()では、1315行目の処理に該当します。


 






1315     PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(Irp);


 


この処理は、今回、非常に重要です。なぜなら、IO_STACK_LOCATION構造体に、アプリケーションから渡されたI/O Controlコードが含まれているからです。IO_STACK_LOCATION構造体の定義は、C:\WinDDK\6001.18002\inc\ddk\wdm.hにあります。(WDKをインストールされていない方は、


 


  IO_STACK_LOCATION


  http://msdn.microsoft.com/en-us/library/aa491675.aspx


 


でも確認できます。)


 


wdm.h19145行目からIO_STACK_LOCATIONの定義が始まりますが、その中のParameters共用体の中にあるwdm.h19328行目の構造体の中に、今回必要となる、IoControlCodeがあります。


 






19321         //


19322         // System service parameters for:  NtDeviceIoControlFile


19323         //


19324         // Note that the user's output buffer is stored in the UserBuffer field


19325         // and the user's input buffer is stored in the SystemBuffer field.


19326         //


19327


19328         struct {


19329             ULONG OutputBufferLength;


19330             ULONG POINTER_ALIGNMENT InputBufferLength;


19331             ULONG POINTER_ALIGNMENT IoControlCode;


19332             PVOID Type3InputBuffer;


19333         } DeviceIoControl;


 


 


1-3. IO_STACK_LOCATION構造体に含まれるI/O Controlコードを参照し、目的のI/O Controlなら、そのI/O Controlに対する処理を行います。


 


DiskPerfDeviceControl()では、1320行目以降の処理に該当します。


 






1320     if (currentIrpStack->Parameters.DeviceIoControl.IoControlCode ==


1321         IOCTL_DISK_PERFORMANCE) {


 


この関数の例では、IOCTL_DISK_PERFORMANCEI/O Controlが来たら、処理を行うようになっています。1-2.でお話したように、IO_STACK_LOCATION構造体のParameters共用体のDeviceIoControl構造体のIoControlCodeを見ることでI/O Controlコードを知ることができることがおわかりになるかと思います。


 


 


以上の内容を踏まえ、当初の目的である、IOCTL_SCSI_PASS_THROUGHを区別するための処理を、このDiskPerfDeviceControl()に追加してみましょう。DiskPerfDeviceControl()では、これ以外のI/O Controlは全て1426行目のelse文以降で処理していますので、IOCTL_SCSI_PASS_THROUGHはここに入ってくることが考えられます。抜粋いたしますと、次の箇所です。


 






1426     else {


1427


1428         //


1429         // Set current stack back one.


1430         //


1431


1432         Irp->CurrentLocation++,


1433         Irp->Tail.Overlay.CurrentStackLocation++;


1434


1435         //


1436         // Pass unrecognized device control requests


1437         // down to next driver layer.


1438         //


1439


1440         return IoCallDriver(deviceExtension->TargetDeviceObject, Irp);


1441     }


 


サンプルの処理は基本的に尊重するのがベターですので、今回はこのelseの中身をそのまま流用します。elseの中では、下位ドライバにIRPを渡すための処理を行っていますので、その処理を行う前の1428行目に、


 






            if (currentIrpStack->Parameters.DeviceIoControl.IoControlCode ==


               IOCTL_SCSI_PASS_THROUGH) {


            }


 


というコードを入れてみましょう。このif文により、IOCTL_SCSI_PASS_THROUGHが来た時の処理を、このif文の中で行うことができます。


 


 


(2) 目的のI/O Controlが入ってきていたら、そのI/O Controlに対する処理を行う


 


それでは、上記のif文の中で、IOCTL_SCSI_PASS_THROUGHの時の処理を記述しましょう。今回は、SPTIアプリケーションがIOCTL_SCSI_PASS_THROUGHと一緒に送ったSCSI_PASS_THROUGH構造体の中身をデバッガに表示させる、ということを目的にしています。SCSI_PASS_THROUGH構造体は、DiskPerfDeviceControl()2nd parameterIrpから受け取ることができます。より具体的には、Irp->AssociatedIrp.SystemBufferから受け取ることができます。


 


ここで、前回の「アプリケーションからSCSIコマンドを発行する方法」で示した、DeviceIoControl()関数を再度掲載します。


 






    status = DeviceIoControl(fileHandle,


                             IOCTL_SCSI_PASS_THROUGH,


                             &sptwb,


                             sizeof(SCSI_PASS_THROUGH),


                             &sptwb,


                             length,


                             &returned,


                             FALSE);


 


この関数の3rd parameterが入力バッファのポインタ、4th parameterがそのサイズでした。上記のように、DeviceIoControl()関数の3rd,4th parameterを指定することで、SCSI_PASS_THROUGH構造体がIrp->AssociatedIrp.SystemBufferにコピーされるため、この構造体をカーネルドライバ側で受け取ることができます。


 


さて、受け取り方は簡単です。次のように、受け取りたい構造体のポインタに代入するだけです。その際、適切にキャストしておくのが安全です。


 






    PSCSI_PASS_THROUGH spt = NULL;


    spt = (PSCSI_PASS_THROUGH) Irp->AssociatedIrp.SystemBuffer;


 


これで、「spt->(メンバ変数)」のようにすれば、アプリケーションから受け取ったSCSI_PASS_THROUGH構造体のデータを参照することができます。


 


以上の内容から、diskperf.c1428行目に、IOCTL_SCSI_PASS_THROUGHを受け取ってSCSI_PASS_THROUGH構造体のデータを表示するコードを追加すると、以下のようになります。


 






1426     else {


1427


1428        if (currentIrpStack->Parameters.DeviceIoControl.IoControlCode ==


1429            IOCTL_SCSI_PASS_THROUGH) {


1430                PSCSI_PASS_THROUGH spt = NULL;


1431


1432                KdPrint(("DiskPerfDeviceControl: IOCTL_SCSI_PASS_THROUGH\n"));


1433                   


1434                spt = (PSCSI_PASS_THROUGH) Irp->AssociatedIrp.SystemBuffer;


1435                KdPrint(("spt->Length = %d\n", spt->Length));


1436                KdPrint(("spt->PathId = %d\n", spt->PathId));


1437                KdPrint(("spt->TargetId = %d\n", spt->TargetId));


1438                KdPrint(("spt->Lun = %d\n", spt->Lun));


1439                KdPrint(("spt->CdbLength = %d\n", spt->CdbLength));


1440                KdPrint(("spt->SenseInfoLength = %d\n", spt->SenseInfoLength));


1441                KdPrint(("spt->DataIn = %d\n", spt->DataIn));


1442                KdPrint(("spt->DataTransferLength = %d\n", spt->DataTransferLength));


1443                KdPrint(("spt->TimeOutValue = %d\n", spt->TimeOutValue));


1444                KdPrint(("spt->DataBufferOffset = %d\n", spt->DataBufferOffset));


1445                KdPrint(("spt->SenseInfoOffset = %d\n", spt->SenseInfoOffset));


1446                KdPrint(("spt->Cdb[0] = %d\n", spt->Cdb[0]));


1447                KdPrint(("spt->Cdb[2] = %d\n", spt->Cdb[2]));


1448                KdPrint(("spt->Cdb[4] = %d\n", spt->Cdb[4]));


1449         }


1450         //


1451         // Set current stack back one.


1452         //


1453


1454         Irp->CurrentLocation++,


1455         Irp->Tail.Overlay.CurrentStackLocation++;


1456


1457         //


1458         // Pass unrecognized device control requests


1459         // down to next driver layer.


1460         //


1461


1462         return IoCallDriver(deviceExtension->TargetDeviceObject, Irp);


1463     }


 


IOCTL_SCSI_PASS_THROUGHや、SCSI_PASS_THROUGH構造体の定義は、C:\WinDDK\6001.18002\inc\api\ntddscsi.hにありますので、


 






  #include "ntddscsi.h"


 


の行をdiskperf.cに追加しておきましょう。今回は、最後の#include文である、35行目の「#include "ntstrsafe.h"」の後に追加しました。


 


以上のようにコードを追加しましたら、diskperfドライバをビルドして、動作確認をしてみましょう。今回は、Vista x86のターゲットPC上にインストールして、かつ、デバッガにプリント文を出力させたいので、"Windows Vista and Windows Server 2008 x86 Checked Build Environment"のビルド環境を使って、ビルドしました。(ドライバのビルドについては、なおきお~さんの「ドライバのビルド方法」の記事を参考にしてください。)ビルドしますと、diskperf\objchk_wlh_x86\i386に、diskperf.sysというドライバファイルとdiskperf.pdbというシンボルファイルができます。


 


それではこのドライバをインストールしましょう。diskperf.htmの「Running the Sample」の項目を読みますと、diskperf.infを右クリックして、「インストール」を選べばインストールできると書いてあります。今回のテスト環境として、Windows Server 2008 R2 RCHyper-V上に、Vista SP2 x86の仮想マシンを用意しました。このVista SP2 x86上のフォルダに、diskperf.sysdiskperf.infをコピーします。そして、diskperf.inf56行目の[SourceDisksNames.x86]セクションに書いてあるとおり、diskperf.infをコピーしたフォルダにi386フォルダを作成し、そこにdiskperf.sysを入れます。(もしi386フォルダを作らずに、それ以外のフォルダにdiskperf.sysを置いた場合は、インストール時にdiskperf.sysの場所を指定するよう要求されます。)


 






54 ; WinXP and later


55


56 [SourceDisksNames.x86]


57 1 = %diskid1%,,,\i386


 


diskperf.infを右クリックしてインストールします。(この時、デバッグしていないので詳細は不明ですが、Vistaの環境によっては、「選択されたINFファイルでは、このインストールの方法はサポートされていません。」というインストールエラーのダイアログが出ることがあります。そのような場合には、[DefaultInstall.NT][DefaultInstall]に、[DefaultInstall.NT.Services][DefaultInstall.Services]に変更してみて試してみてください。)


 


diskperf.infをインストールすると、OSの再起動を求められますので、再起動します。OSが再起動した後、diskperf.infがインストールされたことをデバイスマネージャで確認してみます。以下の図のように、[ディスク ドライブ]のツリーにある[Virtual HD ATA Device]を右クリックし、[プロパティ]を開きます。




 


 


 


次に、[Virtual HD ATA Deviceのプロパティ][ドライバ]タブをクリックし、[ドライバの詳細]をクリックします。


 


 


 




[ドライバ ファイルの詳細]diskperf.sysがあることがわかります。


 


 


 


それでは、以前のブログ「Hyper-Vなどの仮想OSwindbgをアタッチする方法」と同じ方法で、仮想マシンのVistaWinDbgを接続しましょう。接続したら、WinDbg[File][Symbol File Path...]diskperf.pdbのあるフォルダへのフルパス、[Source File Path...]diskperf.cのあるフォルダへのフルパスを指定してください。例えば、C:\develop\diskperf\objchk_wlh_x86\i386のフォルダにdiskperf.pdbがあるなら、[Symbol File Path...]には「C:\develop\diskperf\objchk_wlh_x86\i386」を指定します。C:\develop\diskperf\のフォルダにdiskperf.cがあるなら、[Source File Path...]に「C:\develop\diskperf\」を指定します。


 


WinDbgの設定が終わりましたら、せっかくですので、デバッガからdiskperf.sysがディスククラスドライバの上位フィルタドライバになっていることを確認してみましょう。まずは、WinDbgBreakボタン( )をクリックし、ターゲットPCである仮想マシンのVistaの動きを止めましょう。Breakボタンをクリックすると、以下のような表示がCommandウィンドウに出力されます。


 






Break instruction exception - code 80000003 (first chance)


*******************************************************************************


*                                                                             *


*   You are seeing this message because you pressed either                    *


*       CTRL+C (if you run kd.exe) or,                                        *


*       CTRL+BREAK (if you run WinDBG),                                       *


*   on your debugger machine's keyboard.                                      *


*                                                                             *


*                   THIS IS NOT A BUG OR A SYSTEM CRASH                       *


*                                                                             *


* If you did not intend to break into the debugger, press the "g" key, then   *


* press the "Enter" key now.  This message might immediately reappear.  If it *


* does, press "g" and "Enter" again.                                          *


*                                                                             *


*******************************************************************************


nt!RtlpBreakWithStatusInstruction:


81906464 cc              int     3


kd>


 


次に、この前のK里さんの「DriverObject DriverEntry」で説明がありましたように、!drvobjdiskperf.sysに対して実行してみます。すると、以下のように表示されます。


 






kd> !drvobj diskperf


Driver object (8440df38) is for:


 \Driver\diskperf


Driver Extension List: (id , addr)


 


Device Object list:


844a72c8 


 


この一番最後のDevice Object listに表示されている8桁の値(844a72c8)が、diskperf.sysが作成したデバイスオブジェクトへのアドレスです。今回は、仮想マシン上にOSの入っているディスク(VHD)が一つしかないので、リストにあるアドレスは一つとなっています。このアドレスを、!devstackの引数として実行しますと、以下のように、このデバイスオブジェクトが所属するデバイス スタックが表示されます。


 






kd> !devstack 844a72c8


  !DevObj   !DrvObj            !DevExt   ObjectName


> 844a72c8  \Driver\diskperf   844a7380 


  844c67b8  \Driver\partmgr    844c6870 


  844c6ac8  \Driver\disk       844c6b80  DR0


  8439d390  \Driver\storflt    843f84e0 


  83974848  \Driver\ACPI       842a2cb8 


  8395da40  \Driver\atapi      8395daf8  IdeDeviceP0T0L0-0


!DevNode 83972e30 :


  DeviceInst is "IDE\DiskVirtual_HD______________________________1.1.0___\5&35dc7040&0&0.0.0"


  ServiceName is "disk"


 


確かに、ディスククラスドライバであるdisk.sysの上位に、diskperf.sysがあることがわかります。この方法を使っていただくと、自分がインストールしたドライバが、デバイススタックの正しい位置に入ったかどうかを確認できますので、機会があれば、ぜひお試しください。


 


ちょっと脱線しましたので、本題に戻ります。実は、ご存知の方もいらっしゃるかと思いますが、このまま仮想マシンのVista上でI/O Controlを発行しても、KdPrint()の表示内容はデバッガに出力されません。Vista以前のOSの場合であれば、Checked BuildするだけでKdPrint()の内容はデバッガに出力されましたが、Vistaからは、デバッガ上での設定またはターゲットPC側のレジストリ設定をする必要があります。そのようになった経緯や、設定の詳細などは、今回の本題ではないので、別の機会に譲りますが、興味のある方は、以下のドキュメントをご参照ください。


 


  Reading and Filtering Debugging Messages


  http://msdn.microsoft.com/en-us/library/ms792789.aspx


 


KdPrint()の内容をデバッガに出力するには、デバッガのCommandウィンドウ上で、


 






kd> ed nt!Kd_DEFAULT_Mask 0xf


 


と実行します。ここまで終わりましたら、デバッガ上で行う作業は終了です。止めていたVistaを以下のコマンドで動かしましょう。


 






kd> g


 


さて、それでは、いよいよ仮想マシンのVista上で、ディスクに対して、I/O Controlを発行してみましょう。まず、管理者権限でコマンドプロンプトを起動します。前回のブログ「アプリケーションからSCSIコマンドを発行する方法」で作成したspti.exeを、Vista上のあるフォルダ(今回はC:\sptiとします)にコピーします。コマンドプロンプト上で、C:\sptiに移動し、diskperf.sysが入っているディスク(今回はC:ドライブとします)に、


 






C:\spti> spti.exe c:


 


を実行します。すると、SCSI_PASS_THROUGH構造体の内容が、デバッガのCommandウィンドウに以下のように出力されます。


 






DiskPerfDeviceControl: IOCTL_SCSI_PASS_THROUGH


spt->Length = 44


spt->PathId = 0


spt->TargetId = 1


spt->Lun = 0


spt->CdbLength = 6


spt->SenseInfoLength = 32


spt->DataIn = 1


spt->DataTransferLength = 192


spt->TimeOutValue = 2


spt->DataBufferOffset = 80


spt->SenseInfoOffset = 48


spt->Cdb[0] = 26


spt->Cdb[2] = 63


spt->Cdb[4] = 192


 


出力されている値が、「アプリケーションからSCSIコマンドを発行する方法」で掲載した、SCSI_PASS_THROUGH構造体の値と同じであることがご確認いただけると思います。つまり、アプリケーションからIOCTL_SCSI_PASS_THROUGHとともに送ったSCIS_PASS_THROUGH構造体が、diskperfドライバに到達したことがおわかりいただけると思います。


 


 


――――――――――――――――


 


【閑話休題】皆さんは象の背中に乗ったことはありますか?


 


私は、千葉にある、象で有名な某動物園で乗ったことがあります。象に乗るまで、象の頭に髪が生えているということを知らなかったのですが、固そうな髪が意外とふさふさ(?)生えていて驚きました。今、私の頭は象よりはふさふさですが、将来もそうありたいものだ、と思いました。その動物園では56頭の象がショーをしてくれるのが圧巻でした。「こんなに象がいたら、食費が大変だ。入場料だけでまかなえるのかな」と思っていたら、たくさんの子供さんたちに、象たちが象のぬいぐるみを1つ数千円で鼻で渡してあげていたり、象と一緒に写真をとったり、象にえさ(数百円の野菜)あげたりするなどのサービスがあったので、「なるほど、お客様にいい思い出を作ってあげて、代わりに象たちがご飯を食べられる、いいシステムだなあ」と思いました。(発想が貧乏症ですみません。小学校時代、母親に「石焼き芋、買って」とお願いしたら「家で焼いた方が安いから、がまんしなさい」と言われた生い立ちがあるせいかもしれません。)小さいお子様のいらっしゃる方には、ぜひ思い出作りに行ってほしい場所です。(私は地方出身なので存じ上げませんが、もしかすると関東の方は皆さん行かれたことがあるのかもしれませんね。)


 

Skip to main content