驅動開發:通過Async反向與內核通信

来源:https://www.cnblogs.com/LyShark/archive/2022/10/03/16750551.html
-Advertisement-
Play Games

在前幾篇文章中給大傢具體解釋了驅動與應用層之間正向通信的一些經典案例,本章將繼續學習驅動通信,不過這次我們學習的是通過運用`Async`非同步模式實現的反向通信,反向通信機制在開發中時常被用到,例如一個殺毒軟體如果監控到有異常進程運行或有異常註冊表被改寫後,該驅動需要主動的通知應用層進程讓其知道,這就... ...


在前幾篇文章中給大傢具體解釋了驅動與應用層之間正向通信的一些經典案例,本章將繼續學習驅動通信,不過這次我們學習的是通過運用Async非同步模式實現的反向通信,反向通信機制在開發中時常被用到,例如一個殺毒軟體如果監控到有異常進程運行或有異常註冊表被改寫後,該驅動需要主動的通知應用層進程讓其知道,這就需要用到驅動反向通信的相關知識點,如下將循序漸進的實現一個反向通信案例。

在開始學習Async反向通信之前先來研究一個Sync正向通信案例,不論是正向反向通信其在通信模式上與《驅動開發:通過ReadFile與內核層通信》所介紹的通信模式基本一致,都是通過ReadFile觸發驅動中的IRP_MJ_READ讀取派遣,唯一的區別是在傳輸數據時使用了MmGetSystemAddressForMdl方式,它將給定MDL描述的物理頁面映射到系統空間,並調用RtlCopyMemory()將全局字元串複製到這個空間內,這樣客戶端就可以迴圈讀取內核傳出的數據。

我們來看驅動端代碼是如何實現的這個功能,代碼並沒有什麼特殊的無法理解的點,只是需要註意我們在驅動入口調用IoCreateDevice()時傳入了第二個參數FILE_DEVICE_EXTENSION,該參數的作用是,創建設備時,指定設備擴展記憶體的大小,傳一個值進去,就會給設備分配一塊非頁面記憶體。

#include <ntddk.h>
#include <stdio.h>

// 保存一段非分頁記憶體,用於給全局變數使用
#define FILE_DEVICE_EXTENSION 4096

// 定義全局字元串
static int global_count = 0;
static char global_char[5][128] = { 0 };

// 驅動綁定預設派遣函數
NTSTATUS _DefaultDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_NOT_SUPPORTED;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return _pIrp->IoStatus.Status;
}

// 驅動創建後觸發
NTSTATUS _SyncCreateCloseDispatch(PDEVICE_OBJECT _pDevcieObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_SUCCESS;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return _pIrp->IoStatus.Status;
}

// 應用層讀數據後觸發
NTSTATUS _SyncReadDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	NTSTATUS status = STATUS_SUCCESS;
	PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(_pIrp);
	PVOID pBuffer = NULL;
	ULONG uBufferLen = 0;

	do
	{
		// 讀寫請求使用的是直接I/O方式
		pBuffer = MmGetSystemAddressForMdl(_pIrp->MdlAddress);
		if (pBuffer == NULL)
		{
			status = STATUS_UNSUCCESSFUL;
			break;
		}
		uBufferLen = pIrpStack->Parameters.Read.Length;
		DbgPrint("讀位元組長度: %d \n", uBufferLen);

		// 最大支持20位元組讀請求
		uBufferLen = uBufferLen >= 20 ? 20 : uBufferLen;

		// 輸出五次字元串
		if (global_count < 5)
		{
			RtlCopyMemory(pBuffer, global_char[global_count], uBufferLen);
			global_count = global_count + 1;
		}

	} while (FALSE);

	// 填寫返回狀態及返回大小
	_pIrp->IoStatus.Status = status;
	_pIrp->IoStatus.Information = uBufferLen;

	// 完成IRP
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return status;
}

// 卸載驅動
VOID _UnloadDispatch(PDRIVER_OBJECT _pDriverObject)
{
	// 刪除創建的設備
	UNICODE_STRING  Win32DeviceName;
	RtlInitUnicodeString(&Win32DeviceName, L"\\DosDevices\\LySharkSync");
	IoDeleteDevice(_pDriverObject->DeviceObject);
}

// 驅動入口
NTSTATUS DriverEntry(PDRIVER_OBJECT _pDriverObject, PUNICODE_STRING _pRegistryPath)
{
	UNICODE_STRING DeviceName, Win32DeivceName;
	PDEVICE_OBJECT pDeviceObject = NULL;
	NTSTATUS status;
	HANDLE hThread;
	OBJECT_ATTRIBUTES ObjectAttributes;

	// 設置符號名
	RtlInitUnicodeString(&DeviceName, L"\\Device\\LySharkSync");
	RtlInitUnicodeString(&Win32DeivceName, L"\\DosDevices\\LySharkSync");

	// 迴圈初始化IRP函數
	for (ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		_pDriverObject->MajorFunction[i] = _DefaultDispatch;
	}

	// 再次覆蓋派遣函數
	_pDriverObject->MajorFunction[IRP_MJ_CREATE] = _SyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_CLOSE] = _SyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_READ] = _SyncReadDispatch;
	_pDriverObject->DriverUnload = _UnloadDispatch;

	// 分配一個自定義擴展 大小為sizeof(DEVEXT)
	// By: LyShark.com
	status = IoCreateDevice(_pDriverObject, sizeof(FILE_DEVICE_EXTENSION), &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject);
	if (!NT_SUCCESS(status))
		return status;
	
	if (!pDeviceObject)
		return STATUS_UNEXPECTED_IO_ERROR;

	// 為全局變數賦值
	strcpy(global_char[0], "hi,lyshark A");
	strcpy(global_char[1], "hi,lyshark B");
	strcpy(global_char[2], "hi,lyshark C");
	strcpy(global_char[3], "hi,lyshark D");
	strcpy(global_char[4], "hi,lyshark E");

	// 指定讀寫方式為 直接I/O MDL模式
	pDeviceObject->Flags |= DO_DIRECT_IO;

	// 數據傳輸時地址校驗大小
	pDeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
	status = IoCreateSymbolicLink(&Win32DeivceName, &DeviceName);

	pDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
	return STATUS_SUCCESS;
}

對於應用層來說並沒有什麼特別的,同樣調用ReadFile讀取內核中的參數,同樣for迴圈讀取五次,代碼如下:

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

int main(int argc, char *argv[])
{
	HANDLE hFile;
	char Buffer[10] = { 0 };
	DWORD dwRet = 0;
	BOOL bRet;

	hFile = CreateFileA("\\\\.\\LySharkSync", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return 0;

	for (int x = 0; x < 5; x++)
	{
		bRet = ReadFile(hFile, Buffer, 20, &dwRet, NULL);
		if (!bRet)
		{
			CloseHandle(hFile);
			return 0;
		}
		printf("讀入數據: %s -> 讀取長度: %d \n", Buffer, dwRet);
	}
	return 0;
}

這段代碼運行效果如下:

與同步模式不同,非同步模式雖然同樣使用ReadFile實現通信,但在通信中引入了Event事件通知機制,這也是非同步與同步最大的區別所在,用戶層可以分別創建多個Event事件,等待內核依次做出相應並最終一併返回。

首先驅動內定義了_DeviceExtension自定義介面,該介面用於保存此次事件所對應的Irp以及其所對應的DPC時間等。

非同步分發函數_AsyncReadDispatch同樣是被IRP_MJ_READ派遣函數觸發的,觸發後其內部會首先IoGetCurrentIrpStackLocation得到當前IRP的堆棧信息,然後設置IoMarkIrpPending()並最終將該IRP通過InsertTailList()插入到IRP鏈表內等待被處理。

  • IoMarkIrpPending
    • 用於標記指定的IRP,標志著某個驅動的分發常式(分發函數)因需要被其他的驅動程式進一步處理最終返回STATUS_PENDING狀態。

函數_CustomDpc則是定時器內部要執行的具體操作,在DriverEntry驅動入口處做瞭如下初始化,初始化了鏈表,並初始化了一個定時器,最後啟動這個定時器每隔1秒都會執行一次_CustomDpc如果我們的IRP鏈表內IsListEmpty() 檢測存在數據,則會主動拷貝記憶體RtlCopyMemory並推送到應用層。

// 初始化IRP鏈表
InitializeListHead(&pDevExt->IrpList);
// 初始化定時器
KeInitializeTimer(&(pDevExt->timer));
// 初始化DPC pDevExt是傳給_CustomDpc函數的參數
KeInitializeDpc(&pDevExt->dpc, (PKDEFERRED_ROUTINE)_CustomDpc, pDevExt);

// 設置定時時間位1s
pDevExt->liDueTime = RtlConvertLongToLargeInteger(-10000000);
// 啟動定時器
KeSetTimer(&pDevExt->timer, pDevExt->liDueTime, &pDevExt->dpc);

驅動層完成代碼如下所示:

#include <ntddk.h>

// 自定義介面擴展
typedef struct _DeviceExtension
{
	LIST_ENTRY IrpList;
	KTIMER timer;
	LARGE_INTEGER liDueTime;
	KDPC dpc;
}DEV_EXT, *PDEV_EXT;

// 預設派遣函數
NTSTATUS _DefaultDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_NOT_SUPPORTED;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return _pIrp->IoStatus.Status;
}

// 創建派遣函數
NTSTATUS _AsyncCreateCloseDispatch(PDEVICE_OBJECT _pDevcieObject, PIRP _pIrp)
{
	_pIrp->IoStatus.Status = STATUS_SUCCESS;
	_pIrp->IoStatus.Information = 0;
	IoCompleteRequest(_pIrp, IO_NO_INCREMENT);
	return  _pIrp->IoStatus.Status;
}

// 讀取派遣函數
NTSTATUS _AsyncReadDispatch(PDEVICE_OBJECT _pDeviceObject, PIRP _pIrp)
{
	NTSTATUS status;
	PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(_pIrp);
	PDEV_EXT pDevExt = (PDEV_EXT)_pDeviceObject->DeviceExtension;

	IoMarkIrpPending(_pIrp);

	// 將IRP插入自定義鏈表中插入的是ListEntry
	InsertTailList(&pDevExt->IrpList, &_pIrp->Tail.Overlay.ListEntry);

	// 返回pending 主要返回給I/O管理器的值必須和IRP的Pending標誌位一致
	// By: LyShark.com
	// 即調用iomarkirppending和返回值要一致 
	return STATUS_PENDING;
}

// DPC線程
VOID _CustomDpc(PKDPC Dpc, PVOID DeferredContext, PVOID SystemArgument1, PVOID SystemArgument2)
{
	PIRP pIrp;
	PDEV_EXT pDevExt = (PDEV_EXT)DeferredContext;
	PVOID pBuffer = NULL;
	ULONG uBufferLen = 0;
	PIO_STACK_LOCATION pIrpStack = NULL;

	do
	{
		if (!pDevExt)
		{
			break;
		}

		// 檢查尾端IRP鏈表是否為空 為空則跳出
		if (IsListEmpty(&pDevExt->IrpList))
		{
			break;
		}

		// 從IRP鏈表中取出一個IRP並完成該IRP 取出的是ListEntry的地址
		PLIST_ENTRY pListEntry = (PLIST_ENTRY)RemoveHeadList(&pDevExt->IrpList);
		if (!pListEntry)
			break;

		pIrp = (PIRP)CONTAINING_RECORD(pListEntry, IRP, Tail.Overlay.ListEntry);
		pIrpStack = IoGetCurrentIrpStackLocation(pIrp);

		DbgPrint("當前DPC Irp: 0x%x\n", pIrp);

		// 驅動程式的讀寫方式位直接I/O
		pBuffer = MmGetSystemAddressForMdl(pIrp->MdlAddress);
		if (pBuffer == NULL)
		{
			pIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
			pIrp->IoStatus.Information = 0;
			IoCompleteRequest(pIrp, IO_NO_INCREMENT);

			break;
		}
		uBufferLen = pIrpStack->Parameters.Read.Length;
		DbgPrint("讀取DPC長度: %d\n", uBufferLen);

		// 支持5位元組以下的讀請求
		uBufferLen = uBufferLen > 13 ? 13 : uBufferLen;

		// 複製請求內容
		RtlCopyMemory(pBuffer, "hello lyshark", uBufferLen);

		pIrp->IoStatus.Status = STATUS_SUCCESS;
		pIrp->IoStatus.Information = uBufferLen;

		// 完成該IRP
		IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	} while (FALSE);

	// 重新設置定時器
	KeSetTimer(&pDevExt->timer, pDevExt->liDueTime, &pDevExt->dpc);
}

// 卸載驅動
VOID _UnloadDispatch(PDRIVER_OBJECT _pDriverObject)
{
	UNICODE_STRING Win32DeviceName;
	PDEV_EXT pDevExt = (PDEV_EXT)_pDriverObject->DeviceObject->DeviceExtension;

	RtlInitUnicodeString(&Win32DeviceName, L"\\DosDevices\\LySharkAsync");

	// 刪除定時器
	// LyShark
	KeCancelTimer(&pDevExt->timer);
	// 刪除創建的設備
	IoDeleteDevice(_pDriverObject->DeviceObject);
}

// 驅動入口
NTSTATUS DriverEntry(PDRIVER_OBJECT _pDriverObject, PUNICODE_STRING _pRegistryPath)
{
	UNICODE_STRING DeviceName, Win32DeivceName;
	PDEVICE_OBJECT pDeviceObject = NULL;
	NTSTATUS status;
	PDEV_EXT pDevExt = NULL;
	HANDLE hThread;
	OBJECT_ATTRIBUTES ObjectAttributes;
	CLIENT_ID CID;

	RtlInitUnicodeString(&DeviceName, L"\\Device\\LySharkAsync");
	RtlInitUnicodeString(&Win32DeivceName, L"\\DosDevices\\LySharkAsync");

	for (ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		_pDriverObject->MajorFunction[i] = _DefaultDispatch;
	}

	_pDriverObject->MajorFunction[IRP_MJ_CREATE] = _AsyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_CLOSE] = _AsyncCreateCloseDispatch;
	_pDriverObject->MajorFunction[IRP_MJ_READ] = _AsyncReadDispatch;
	_pDriverObject->DriverUnload = _UnloadDispatch;

	// 分配自定義擴展
	status = IoCreateDevice(_pDriverObject, sizeof(DEV_EXT), &DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject);
	if (!NT_SUCCESS(status))
		return status;
	if (!pDeviceObject)
		return STATUS_UNEXPECTED_IO_ERROR;

	pDeviceObject->Flags |= DO_DIRECT_IO;
	pDeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
	status = IoCreateSymbolicLink(&Win32DeivceName, &DeviceName);

	pDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
	pDevExt = (PDEV_EXT)pDeviceObject->DeviceExtension;

	// 初始化IRP鏈表
	InitializeListHead(&pDevExt->IrpList);
	// 初始化定時器
	KeInitializeTimer(&(pDevExt->timer));
	// 初始化DPC pDevExt是傳給_CustomDpc函數的參數
	KeInitializeDpc(&pDevExt->dpc, (PKDEFERRED_ROUTINE)_CustomDpc, pDevExt);

	// 設置定時時間位1s
	pDevExt->liDueTime = RtlConvertLongToLargeInteger(-10000000);
	// 啟動定時器
	KeSetTimer(&pDevExt->timer, pDevExt->liDueTime, &pDevExt->dpc);

	return STATUS_SUCCESS;
}

驅動層說完了,接下來是應用層,對於應用層來說,需要使用CreateEvent打開通知事件,或者叫做事件對象,然後當有通知時,則直接使用ReadFile讀取對應的緩衝區,當所有讀取全部結束WaitForMultipleObjects等待結束即輸出結果。

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

int main(int argc, char *argv[])
{
	HANDLE hFile;
	char Buffer[3][32] = { 0 };
	DWORD dwRet[3] = { 0 };
	OVERLAPPED ol[3] = { 0 };
	HANDLE hEvent[3] = { 0 };

	// By:LyShark
	hFile = CreateFileA("\\\\.\\LySharkAsync", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
		NULL);
	if (INVALID_HANDLE_VALUE == hFile)
		return  0;

	// event用來通知請求完成
	hEvent[0] = CreateEvent(NULL, TRUE, FALSE, NULL);
	ol[0].hEvent = hEvent[0];

	hEvent[1] = CreateEvent(NULL, TRUE, FALSE, NULL);
	ol[1].hEvent = hEvent[1];

	hEvent[2] = CreateEvent(NULL, TRUE, FALSE, NULL);
	ol[2].hEvent = hEvent[2];

	// 讀取事件內容到緩存
	ReadFile(hFile, Buffer[0], 13, &dwRet[0], &ol[0]);
	ReadFile(hFile, Buffer[1], 13, &dwRet[1], &ol[1]);
	ReadFile(hFile, Buffer[2], 13, &dwRet[2], &ol[2]);

	// 等待三個事件執行完畢
	WaitForMultipleObjects(3, hEvent, TRUE, INFINITE);

	// 輸出結果
	printf("緩存LyShark A: %s \n", Buffer[0]);
	printf("緩存LyShark B: %s \n", Buffer[1]);
	printf("緩存LyShark C: %s \n", Buffer[2]);

	CloseHandle(hFile);
	return  0;
}

這段代碼最終運行效果如下:

文章作者:lyshark (王瑞)
文章出處:https://www.cnblogs.com/LyShark/p/16750551.html
版權聲明:本博客文章與代碼均為學習時整理的筆記,文章 [均為原創] 作品,轉載請 [添加出處] ,您添加出處是我創作的動力!
轉載文章請遵守《中華人民共和國著作權法》相關規定或遵守《署名CC BY-ND 4.0國際》禁止演繹規範,合理合規攜帶原創出處轉載。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家好,我是沙漠盡頭的狼。 最近幾天在看一本Go的書籍,看了100來頁,感覺不錯,分享給大家​。​ 書籍基本信息 書籍信息: 書名:Go Web編程 作 者:(新加坡)鄭兆雄(Sau Sheong Chang) 著;黃健巨集 譯 著作 定 價:79 出 版 社:人民郵電出版社 出版日期:2017年12 ...
  • 簡述 類型:結構型 目的:解決介面不相容問題。 話不多說,看個案例吧。 優化案例 最初版v0 在真實的開發場景中,系統的每個模塊都是分配給不同的團隊或個人來開發的。這使得事前溝通變得尤為重要,且溝通問題也時有發生。現在公司有兩個模塊無法相容,難道只能重寫其中的一個嗎? class User { St ...
  • 編碼 ASCII:用八位二進位的低七位,一共規定了128個字元的編碼,一個位元組表示一個字元, 擴展ASCII:第八位為1,規定了以1開頭的128個字元 Unicode:固定大小的編碼,通常兩個位元組表示一個字元,字母和漢字統一用兩個位元組,浪費空間 UTF-8:是一種變長的編碼方式。字母用一個位元組,漢字 ...
  • 一、前期準備 1.新建一個django項目 2.連接mysql資料庫(註意需要在init文件裡面書寫import pymysql),告訴django使用pymysql連接資料庫 3.靜態文件路徑在settings里配置一下,並且在項目文件夾下新建一個靜態文件夾 二、bbs項目表設計 做一個項目最首要 ...
  • 2022-10-03 url中的位置參數 位置參數存放的位置 是子應用中的自定義的“urls.py”文件中的路由中。 位置參數的設置: 如果位置參數很多,那麼在自定義中的路由文件中可以使用正則表達式。位置參數就是在瀏覽器搜索中對應的關鍵詞時,能夠跳轉到相應的界面。界面內容的設置是在子應用的“view ...
  • Git操作入門 Git 是一個快速、可擴展的 分散式版本控制系統 ,它具有極為豐富的命令集,對內部系統提供了高級操作和完全訪問.Git與你熟悉的大部分版本控制系統的差別是很大的。相似的還有Subversion、 CVS 、Perforce、Mercurial 等等,他們 使用“增量文件系統” (De ...
  • SpringBoot HelloWorld 1.創建Meven工程 2.引入依賴 pom.xml <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifact ...
  • 背景介紹 最近把dubbo的版本從2.7.3升級到2.7.15時,遇到一個報錯 No application config found or it's not a valid config! ,對應的異常棧為: Caused by: java.lang.IllegalStateException: ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...