IRP 派遣函數 與通信方式
一丶IRP
1.1 IRP介紹 理論知識
在Windows內核中,有一種數據結構叫做 IRP(I/O Request Package) 也就是輸入輸出請求包。它是與輸入輸出相關的重要數據結構 只要了解了IRP 那么驅動開發以及內核你就會了解一大半了。
當上層 應用程序 與驅動程序 進行通訊的時候 應用程序會發出 I/O 請求。操作系統會將請求轉為相應的IRP 數據。不同的IRP數據 會按照類型傳遞到不同的派遣函數中。
1.2 IRP的類型
當應用層調用 ReadFile WriteFile CreateFile CloseHandle 等WINAPI 函數 則會產生對應的IRP類型的的IRP 也就是 IRP_MJ_CREATE IRP_MJ_WRITE IRP_MJ_READ IRP_MJ_CLOSE 並且傳送到驅動的中的派遣函數中。
另外 內核中的 I/O 處理函數也會產生IRP,所以可見IRP並不完全是由應用層產生的。比如內核中的 Zw系列開頭的文件操作 一樣會產生IRP。
| IRP類型 | 來源 |
|---|---|
| IRP_MJ_CREATE | CreateFile/ZwCreateFile |
| IRP_MJ_READ | ReadFile/ZwReadFile |
| IRP_MJ_WRITE | WriteFile/ZwWriteFile |
| IRP_MJ_CLOSE | CloseHandle/ZwClose |
| ... | ... |
| ... | ... |
1.3 派遣函數
當我們知道IRP類型之后只需要給驅動設置派遣函數即可。 這樣當應用層調用對應的 Winapi發送IO請求數據包的時候我們的派遣函數則會獲取到。
代碼如下:
extern "C" NTSTATUS DriverEntry (
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
{
NTSTATUS status;
KdPrint(("Enter DriverEntry\n"));
//設置卸載函數
pDriverObject->DriverUnload = HelloDDKUnload;
//設置派遣函數
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = HelloDDKDispatchRoutin;
//創建驅動設備對象
status = CreateDevice(pDriverObject);
KdPrint(("Leave DriverEntry\n"));
return status;
}
在我們的DriverEntry中 有一個驅動對象參數 其中此參數的 MajorFunction是一個數組。數組里面存放着記錄着IRP類型的派遣函數的回調函數指針。所以我們根據如上設置之后。當winapi發送IO請求的時候對應的派遣函數則會調用。
1.4 設備對象 與符號鏈接
設備對象 也是驅動中的很重要的對象。 我們的IRP是要發送給設備的。所以需要創建設備對象。但是如果應用層想要發送IO請求(調用WINAPI) 那么內核驅動必須提供個符號鏈接給應用層使用。 內核層創建好設備之后還可以指定通訊方式。 也就是 應用-驅動 如何進行通信。數據如何傳輸。這個下面再說。
代碼如下:
NTSTATUS CreateDevice (
IN PDRIVER_OBJECT pDriverObject)
{
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//創建設備名稱
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
//創建設備
status = IoCreateDevice( pDriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj );
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO; //通信方式設置后面說明。
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
//創建符號鏈接
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink( &symLinkName,&devName );
if (!NT_SUCCESS(status))
{
IoDeleteDevice( pDevObj );
return status;
}
return STATUS_SUCCESS;
}
1.5 IRP堆棧介紹
IPR堆棧也是很重要的 IO數據包結構。因為IRP結構中記錄的數據不足與滿足我們的需求。所以提供了IRP堆棧。 比如 應用程序發出的I/O 請求是讀的請求,並且此請求會發送到內核的讀派遣函數中。 那么此時堆棧就是讀的堆棧。 所以類型的不同堆棧被填充的內容也會是不同的。
官方說法是 驅動程序會創建一個設備對象,並且將這些設備對象串聯到一起。形成了一個 設備棧 IRP會被操作系統發送到設備棧的頂層,如果頂層設備對象的派遣函數 結束了IRP請求,那么此次的IRP請求就會結束,不會往下發送了。否則操作系統就會將IRP再轉發到設備棧的下一層設備進行處理。如果設備依舊不能處理,那么繼續往下發。 因此IRP會被轉發多次。為了記錄IRP在每層設備中的操作,IRP會有一個堆棧數組。IRP的堆棧數組元素數應該大於IRP穿越過的設備數。每個 堆棧結構元素記錄着對應設備所作的操作。
上面所述的堆棧數組結構如下:
數組名結構為: IO_STACK_LOCATION
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
union {
//
// Parameters for IRP_MJ_CREATE
//
struct {
PIO_SECURITY_CONTEXT SecurityContext;
ULONG Options;
USHORT POINTER_ALIGNMENT FileAttributes;
USHORT ShareAccess;
ULONG POINTER_ALIGNMENT EaLength;
} Create;
//
// Parameters for IRP_MJ_READ
//
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Read;
//
// Parameters for IRP_MJ_WRITE
//
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Write;
//
// Parameters for IRP_MJ_QUERY_INFORMATION
//
struct {
ULONG Length;
FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
} QueryFile;
//
// Parameters for IRP_MJ_SET_INFORMATION
//
struct {
ULONG Length;
FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
PFILE_OBJECT FileObject;
union {
struct {
BOOLEAN ReplaceIfExists;
BOOLEAN AdvanceOnly;
};
ULONG ClusterCount;
HANDLE DeleteHandle;
};
} SetFile;
//
// Parameters for IRP_MJ_QUERY_VOLUME_INFORMATION
//
struct {
ULONG Length;
FS_INFORMATION_CLASS POINTER_ALIGNMENT FsInformationClass;
} QueryVolume;
//
// Parameters for IRP_MJ_DEVICE_CONTROL and IRP_MJ_INTERNAL_DEVICE_CONTROL
//
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
..............................
} Parameters;
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
.
.
.
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
在此結構中我們可以看到 IRP類型的記錄域
UCHAR MajorFunction;
UCHAR MinorFunction;
也記錄着 設備對象 文件對象
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
其中比較重要的就是 Parameters 參數。它里面記錄着 Read Write DeviceIoControl Create 等結構。 當我們 IRP類型為Read的時候。派遣函數 則可以從 Read域中獲取讀取的長度 偏移等信息。
調用本層堆棧信息 使用的API如下
PIO_STACK_LOCATION
IoGetCurrentIrpStackLocation(
IN PIRP Irp
);
1.6 派遣函數中的IRP處理
在派遣函數中我們可以使用如下API來完成IRP的操作
VOID
IoCompleteRequest(
IN PIRP Irp,
IN CCHAR PriorityBoost
);
此API第一個參數就是派遣函數中給定的IRP
第二個參數我們一般都是設置為 IO_NO_INCREMENT
第二個參數的意思如下:
第二個參數的意思代表優先級,指的是阻塞的線程以何種優先級恢復運行。
原因是如果是 鼠標 鍵盤等輸入設備他們需要更快的反應。所以需要指定優先級 以“優先”的身份運行。
我們常用的就是 IO_NO_INCREMENT
還有其他方式。請查詢WDK文檔。
二丶內核與應用層的通信方式 緩存方式(緩沖區方式)
2.1 緩存方式
緩存方式 就是 應用層發送數據到內核層中,內核層建立一個緩沖區來進行保存。 而我們操作這個緩沖區即可。 這樣的好處是安全 穩定。 缺點是效率慢。
緩存方式 在我們創建完設備對象之后。將設備對象的標志設置為 DO+_BUFFERD_IO
即可。
pDevObj->Flags |= DO_BUFFERED_IO;
如果設置為緩沖區模式。那么我們只需要在 IRP結構 中獲取AssociatedIrp.SystemBuffer 即可。
IRP結構如下
typedef struct _IRP {
.
.
PMDL MdlAddress; //直接IO會使用
ULONG Flags;
union {
struct _IRP *MasterIrp;
.
.
PVOID SystemBuffer; //緩沖區模式使用
} AssociatedIrp;
.
.
IO_STATUS_BLOCK IoStatus; //狀態
KPROCESSOR_MODE RequestorMode;
BOOLEAN PendingReturned;
.
.
BOOLEAN Cancel;
KIRQL CancelIrql;
PDRIVER_CANCEL CancelRoutine;
PVOID UserBuffer; //其它方式
。。。。。。。
} IRP, *PIRP;
2.2 讀取 寫入 控制等IRP的大小獲取
在我們的派遣函數中如果指定了緩沖區模式。那么我們從IRP中獲取 SystemBuffer 使用即可。
但是派遣函數 會根據 IRP不同的類型來分配不同的派遣函數調用。 其中就會有 IRP_MJ_READ IRP_MJ_WRITE IPR_MJ_DEVICECONTROL 根據派遣函數的不同我們獲取的用戶傳遞緩沖區方式的大小也是不同的。
比如IRP_MJ_READ
我們要在 IRP堆棧 中的 Parameters.Read.Length 來獲取長度
如果是 IRP_MJ_WRITE 那么相應的要在 Write.length 來獲取長度
如果是Control中 那么就是 DeviceIoControl中獲取。
其中他還比較特殊它的域如下:
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
記錄着應用層傳遞的輸出buffer的長度。 輸入buffer的長度。 控制碼。
以及 使用其它方式通訊類型的 用戶區的緩沖區。 后面會說。
2.3 緩存方式派遣函數中的使用例子
NTSTATUS DisPathchRead_SystemBuffer(PDEVICE_OBJECT pDeviceobj, PIRP pIrp)
{
KdBreakPoint();
PVOID pBuffer = NULL;
ULONG uReadLength = 0;
ULONG ustrLen = 0;
PIO_STACK_LOCATION pIrpStack = NULL;
pIrpStack = IoGetCurrentIrpStackLocation(pIrp); //獲取堆棧
pBuffer = pIrp->AssociatedIrp.SystemBuffer; //緩存方式獲取緩沖區
uReadLength = pIrpStack->Parameters.Read.Length;//根據不同類型在不同域中獲取長度
if (pBuffer != NULL && uReadLength > 0)
{
ustrLen = strlen("HelloWorld");
if (uReadLength < ustrLen)
goto END;
RtlCopyMemory(pBuffer, "HelloWorld", ustrLen);
}
END:
pIrp->IoStatus.Information = ustrLen;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
三丶MDL方式(直接IO方式)
3.1 直接IO方式
Mdl方式是將用戶傳遞的Buffer進行映射。在內核中映射了一份。這樣用戶模式和內核模式的緩沖區都是指向了同一塊物理內存,無論操作系統如何切換進程內核模式的地址都不會改變。
優點: 速度快 安全穩定。
使用MDL方式首先也是在創建設備之后設置設備通信方式為直接IO方式。
如下:
pDeviceObj->Flags |= DO_DIRECT_IO;
設置之后在IRP域中的 MdlAddress 則記錄着映射的地址
3.2Mdl結構
MDL是一個結構,記錄着這段虛擬內存(用戶的buffer) 。
因為內存是不連續的所以MDL會像鏈表一樣記錄
typedef struct _MDL {
struct _MDL *Next; //下一個MDL
CSHORT Size; //記錄本身MD
CSHORT MdlFlags;
struct _EPROCESS *Process; //記錄當前進程的EP
PVOID MappedSystemVa;//記錄內核中的地址
PVOID StartVa; //記錄第一個頁地址
ULONG ByteCount; //記錄虛擬機內存大小
ULONG ByteOffset;//記錄相對於頁的偏移
} MDL, *PMDL;
MmGetMdlVirtualAddress 返回 MDL 描述的 i/o 緩沖區的虛擬內存地址。
MmGetMdlByteCount 返回 i/o 緩沖區的大小(以字節為單位)。
MmGetMdlByteOffset 返回 i/o 緩沖區開始處的物理頁內的偏移量。
MmGetSystemAddressForMdlSafe例程將指定 MDL 描述的物理頁面映射到系統地址空間中的虛擬地址
MDL很多,需要詳細了解可以看一下微軟文檔。
這里只說明我們需要使用的。
其中虛擬內存首地址是我們計算出來的 VA = StartVa + ByteOffset
3.3 直接IO通信例子
NTSTATUS DisPathchRead_Mdl(PDEVICE_OBJECT pDeviceobj, PIRP pIrp)
{
KdBreakPoint();
PVOID pBuffer = NULL;
ULONG uReadLength = 0;
ULONG uOffset = 0;
ULONG ustrLen = 0;
PIO_STACK_LOCATION pIrpStack = NULL;
PVOID pKernelbase = NULL;
//獲取堆棧,例子中沒用。使用的時候需要根據IRP類型獲取操作的長度
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
uReadLength = MmGetMdlByteCount(pIrp->MdlAddress);//獲取IO長度(數組的)
uOffset = MmGetMdlByteOffset(pIrp->MdlAddress); //頁偏移
pBuffer = MmGetMdlVirtualAddress(pIrp->MdlAddress);//第一個頁
//獲取內核中映射的地址
pKernelbase = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
if (pKernelbase != NULL && uReadLength > 0)
{
ustrLen = strlen("HelloWorld");
if (uReadLength < ustrLen)
goto END;
RtlCopyMemory(pKernelbase, PsGetProcessImageFileName(pIrp->MdlAddress->Process), ustrLen);
}
END:
pIrp->IoStatus.Information = ustrLen;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
四丶其它方式讀寫
4.1 其它方式
其它方式讀寫則是直接將用戶的緩沖區傳遞給內核。但是這樣如果進程發生切換則會造成藍屏。所以需要我們去判斷是否可讀。有點效率是最快的。 缺點 不安全。
設置 設備對象中的標志為 0即可
pDeviceObj->Flags = 0;
讀寫的數據都在 IRP結構中的UserBuffer中。
示例如下:
NTSTATUS DisPathchRead_UserBuffer(PDEVICE_OBJECT pDeviceobj, PIRP pIrp)
{
KdBreakPoint();
PVOID pBuffer = NULL;
ULONG uReadLength = 0;
ULONG uOffset = 0;
ULONG ustrLen = 0;
PIO_STACK_LOCATION pIrpStack = NULL;
PVOID pKernelbase = NULL;
pIrpStack = IoGetCurrentIrpStackLocation(pIrp); //獲取堆棧
pBuffer = pIrp->UserBuffer;
uReadLength = pIrpStack->Parameters.Read.Length;//根據IRP類型不同獲取不同的長度
if (pBuffer != NULL && uReadLength > 0)
{
ustrLen = strlen("HelloWorld");
if (uReadLength < ustrLen)
goto END;
__try
{
ProbeForRead(pBuffer, 1, 1);//是否可讀寫。ProbeForWrite
RtlCopyMemory(pKernelbase, PsGetProcessImageFileName(pIrp->MdlAddress->Process), ustrLen);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
.....
}
}
END:
pIrp->IoStatus.Information = ustrLen;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
五丶IO控制設備通訊方式
5.1 DeviceIoControl通訊
在上面我們說了 ReadFile WriteFile 會分別產生 IRP_MJ_READ IRP_MJ_WRITE
且如果我們指定了通訊方式。那么分別就從不同的地方來獲取 ring3傳遞給內核的Buffer.
如下:
Irp->MdlAddress //直接IO DO_DIRECT_IO
Irp->AssociatedIrp.SystemBuffer; //緩沖區方式 DO_BUFFERED_IO
Irp->UserBuffer; //其它方式 0
在Ring3下面我們還可以通過 DeviceIoControl這個 WinApi來與內核進行通訊
通訊的的前提是我們需要使用 CreateFile 來打開我們內核提供的 符號鏈接
打開成功后返回 對象句柄 我們的 DeviceIoControl 就可以來通過這個對象句柄來與內核進行通訊了。
BOOL DeviceIoControl(
[in] HANDLE hDevice,
[in] DWORD dwIoControlCode,
[in, optional] LPVOID lpInBuffer,
[in] DWORD nInBufferSize,
[out, optional] LPVOID lpOutBuffer,
[in] DWORD nOutBufferSize,
[out, optional] LPDWORD lpBytesReturned,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
| 參數 | 說明 |
|---|---|
| hDevice | 句柄,由CreateFile打開符號鏈接后返回。 |
| dwIoControlCode | 控制碼,下面會詳解。 |
| lpInBuffer | 傳遞給內核層的輸入緩沖區。內核解析此緩沖區進行操作。 |
| nInBuffferSize | 輸入緩沖區的大小 |
| lpOutBuffer | 傳遞給內核層的輸出緩沖區,內核層將結果寫入此緩沖區。 |
| nOutBufferSize | 輸出緩沖區大小 |
| lpBytesReturned | 傳遞給內核層的4字節變量,來接受返回值的。內核層可設置返回值。一般都是記錄 讀取/寫入 多少字節的。內核層設置。 |
| lpOverLapped | 是否允許異步 |
觀看參數,其實除了控制碼其它都很好理解。 這里不再贅述。
5.2 控制碼詳解
控制碼是一個32位無符號整型 控制碼也稱為 IOCTL 它需要符合DDK的標准。
32位分別代表了不同信息。圖如下:

DDK還為我們提供了一個宏。 只需要使用這個宏進行填充即可。
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
我們只需要填充這個宏即可。
那么說一下參數含義
| 參數 | 說明 |
|---|---|
| DeviceType | 設備對象的類型,對應內核層中調用IoCreateDevice的時候傳遞的類型,一般是FILE_DEVICE_XXX |
| Function | 驅動程序自定義的IOCTL控制碼,0x0000-0x7FFF是微軟保留。程序員應該使用0x800-0xFFF |
| Method | 與驅動通信的時候操作模式也就是是緩沖區方式 還是MDL方式還是其它方式 |
| Access | 權限,一般都是設置位FILE_ANY_ACCESS |
通信操作模式:
-
METHOD_BUFFERED
使用緩沖區方式
-
METHOD_IN_DIRECT
使用直接寫模式
-
METHOD_OUT_DIRECT
使用直接讀模式
-
METHOD_NEITHER
使用其它方式
5.3 緩沖區方式
當使用 DeviceIoControl 函數,並且通信方式指明為 METHOD_BUFFERED 那么DeviceIoControl 傳遞的 InBuffer OutBuffer 都會轉化為IRP中的
SystemBuffer 我們直接從 IRP_MJ_DEVICE_CONTROL 指向的派遣函數中拿到
SystemBuffer即可,拿到后直接解析那么就是解析的 Inbuffer的內容 然后如果需要傳出的時候 直接寫入 SystemBuffer 即可。傳出寫入那么就是往 OutBuffer中寫。
總結來說,如果是緩沖區模式,那么輸入輸出緩沖區都是一個緩沖區,在內核中都會封裝到 Irp中的SystemBuffer中。
想要獲取讀取/寫入的字節 那么就要在 Irp堆棧中的Parameters中的 控制碼域來得到。
這點與上面幾個主題中所講的一樣。不同的控制碼要在不同的域中拿到讀取或者寫入的長度
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
如上 可以拿到 輸出緩沖區的長度 輸入緩沖區的長度 控制碼(派遣函數需要根據控制碼來執行不同的操作) Type3InputBuffer后面說。
5.4 直接內存模式
直接內存模式就是 METHOD_IN_DIRECT 和 METHOD_OUT_DIRECT
他們都是直接內存模式。唯一差別就是體現在打開設備的權限上。如果只讀方式打開設備(CreateFile) 那么METHOD_IN_DIRECT的 IOCTL則會成功,而METHOD_OUT_DIRECT則會失敗。如果讀寫權限成功 那么都會成功。
經過嘗試:
1.METHOD_IN_DIRECT METHOD_OUT_DIRECT 輸入緩沖區都是在 irp->AssociatedIrp.SystemBuffer,輸出緩沖區都是 irp->MdlAddress
必須使用API MmGetSystemAddressForMdlSafe 來獲取內核中映射的地址。 也就是操作MDL.
5.5 其它內存模式
其它內存模式就是 設置 IOCTL為 METHOD_NEITHER 這種方式很少用到。因為
他是直接訪問用戶的地址。使用的時候還必須要保證 線程上下文環境一致。
它的特點如下:
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl
DeviceIoControl->Type3InputBuffer 記錄着輸入緩沖區
Irp->UserBuffer 記錄着輸出緩沖區
其中輸入輸出緩沖區的長度還是在 DeviceIoControl 記錄着。
如果使用用戶模式的緩沖區 一定還是要使用 ProbeForRead ProbeForWirte 來校驗。
否則分分鍾藍屏。
這種方式優點就是最快 但也是最不安全。
