, ,

32-bit dll with 64-bit driver

在64-bit作業系統上所使用的驅動程式,一定是64-bit,然而應用程式卻可能是32或64-bit。當32-bit應用程式傳值給驅動程式時是有可能會發生溢位的。主要原因是32與64-bit指標所佔用記憶體長度的不同。接下來我將透過Reference 1中的程式PhyMem,來告訴大家問題在哪與如何修改。(最後改完的程式碼恕我不提供)

這是一個中國人寫的程式。作用與WinIO相同,可以存取windows的io port與physical memory。這個程式包含pmdll、driver與test三個專案,分別產生dll、sys與exe。作者僅提供32-bit的版本,但只要修改編譯設定就可以讓它產生出64-bit的artifact。但如同我前言所說,如果你是32-bit的dll要存取64的sys該怎麼辦? 可以規定User在64-bit的OS用64-bit的應用程式就好了阿! 但是將一個32-bit應用程式改為64-bit的有這麼簡單嗎? 除此之外,中間傳遞的資料型態也會影響到正常功能。

我開發環境是VC2008與DDK6000,下載Reference1的專案是無法直接編譯的,可以參考我的設定去修改編譯、連結參數。輸出檔可以看個人需求,根據32或64命名,或者是用同一個檔案名稱。

32-bit

  • C/C++ > 一般 > 其它Include目錄($(DDKROOT)為設定於環境變數的DDK安裝目錄):
$(DDKROOT)\inc\ddk";"$(DDKROOT)\inc\api";"$(DDKROOT)\inc\crt";"$(DDKROOT)\inc\crt\gl";"$(DDKROOT)\inc\crt\sys"
  • 連結器 > 一般 > 其它程式庫目錄: $(DDKROOT)\lib\wnet\i386
  • 連結器 > 資訊清單檔: 將產生資訊清單與UAC選擇否,因為這不適用於driver。
  • 連結器 > 進階 > 隨機化的基底位置: 選擇預設。

64-bit

Reference1僅提供32-bit設定。64-bit可在建置>組態管理員中,新增x64平台,而設定檔可從win32複製過來修改。修改內容如下:

  • C/C++ > 前置處理器: 將_X86_替換為_AMD64_,這會影響到使用的資料結構。
  • 連結器 > 一般 > 其它程式庫目錄: $(DDKROOT)\lib\wnet\amd64

首先看pmdll這個專案中讀physical memory的程式碼,它做的事情就是將address與size,透過driver mapping出來。driver會assign到pVirAddr上,最後回傳給callee。

PVOID MapPhyMem(DWORD phyAddr, DWORD memSize)
{
	PVOID pVirAddr=NULL;	//mapped virtual addr
	PHYMEM_MEM pm;
	DWORD dwBytes=0;
	BOOL bRet=FALSE;
 
	pm.pvAddr=(PVOID)phyAddr;	//physical address
	pm.dwSize=memSize;	//memory size
 
	if (hDriver!=INVALID_HANDLE_VALUE)
	{
		bRet=DeviceIoControl(hDriver, IOCTL_PHYMEM_MAP, &pm,
			sizeof(PHYMEM_MEM), &pVirAddr, sizeof(PVOID), &dwBytes, NULL);
	}
 
	if (bRet && dwBytes==sizeof(PVOID))
		return pVirAddr;
	else
		return NULL;
}

發現問題了嗎? 還沒有沒關係,接著來看看driver主要的程式碼。它會把dll的input assign到local variable,並透過sizeof去確認PHYMEM_MEM與PVOID是否相同,決定要不要做我看不懂的事mapping memory。

	//Get the pointer to the input/output buffer and it's length
	pSysBuf=(PVOID)irp->AssociatedIrp.SystemBuffer;
	pMem=(PPHYMEM_MEM)pSysBuf;
	pPort=(PPHYMEM_PORT)pSysBuf;
	pPci=(PPHYMEM_PCI)pSysBuf;
	dwInBufLen=irpStack->Parameters.DeviceIoControl.InputBufferLength;
	dwOutBufLen=irpStack->Parameters.DeviceIoControl.OutputBufferLength;
 
	switch (irpStack->MajorFunction)
	{
	case IRP_MJ_DEVICE_CONTROL:
 
		dwIoCtlCode=irpStack->Parameters.DeviceIoControl.IoControlCode;
 
		switch (dwIoCtlCode)
		{
		case IOCTL_PHYMEM_MAP:
 
			if (dwInBufLen==sizeof(PHYMEM_MEM) && dwOutBufLen==sizeof(PVOID))
			{
				PHYSICAL_ADDRESS phyAddr;
				PVOID pvk, pvu;
 
...以下省略

主要的問題是: PPHYMEM_MEM這個結構的大小,在32-bit dll與64-bit sys中是一樣的嗎? 如果不一樣,勢必造成轉型sizeof check問題。我們可以透過下面的程式碼去確認:

typedef struct tagPHYMEM_MEM
{
	PVOID pvAddr;	//physical addr when mapping, virtual addr when unmapping
	ULONG dwSize;	//memory size to map or unmap
} PHYMEM_MEM, *PPHYMEM_MEM;
 
int _tmain(int argc, _TCHAR* argv[])
{
	cout << "PVOID: " << sizeof(PVOID) << endl;
	cout << "ULONG: " << sizeof(ULONG) << endl;
	cout << "PHYMEM_MEM: " << sizeof(PHYMEM_MEM) << endl;
	return 0;
}

32-bit執行檔 vs 64-bit執行檔執行結果:

從執行結果可以發現32與64-bit在data type與structure size的不同。參考Reference 2~4介紹,32-bit and 64-bit data models至少有四種model,Windows使用的是LLP64,只有pointer會從32被擴展到64-bit。而structure的部分,在32-bit如果是1個byte,會被擴展而4個byte,64位元則會被擴展為8個byte,目的是欄位的對齊。因此把32-bit的dll資料結構assign給64-bit的driver,是對不起來的! 這就像是審預算,基層只A一點錢,越上層需要越多的filler,最後無法對齊造成分贓不均,才導致海角7億ㄅㄧㄚㄎㄤ。

針對這個問題,我的想法就是: A多少錢就要平分給基層人員。用在32-bit與64-bit會一樣大小的data type不就好了嗎? 我第一個修改的是資料結構

data structure

PVOID在32-bit是4 bytes而64-bit是8 bytes,我們可以改用PVOID64確保在32與64-bit都是 8bytes

typedef struct tagPHYMEM_MEM
{
	PVOID pvAddr;	//physical addr when mapping, virtual addr when unmapping
	ULONG dwSize;	//memory size to map or unmap
} PHYMEM_MEM, *PPHYMEM_MEM;

to

typedef struct tagPHYMEM_MEM
{
	PVOID64 pvAddr;	//physical addr when mapping, virtual addr when unmapping
	ULONGLONG dwSize;	//memory size to map or unmap
} PHYMEM_MEM, *PPHYMEM_MEM;

你也許會問: 不是有問題的只有PVOID的sizeof,為什麼我連dwSize都一起改了呢? 一個原因是希望讓在64-bit也是使用8 bytes的型態。也許做assign時,會因為剛好它在最後一個field而不會有問題,但如果它是一段連續的記憶體位置呢? 另外一個原因是32-bit與64-bit的定址不同。64-bit的記憶體位置比32-bit大了一倍,用ULONG無法描述0x0000000000000000~0xFFFFFFFFFFFFFFFF。

dll

dll的部分就是將原本PVOID的部分改為使用PVOID64。function的參數僅使用DWORD,如果要支援mapping更大的記憶體位置,可以改用DWORD64,但值傳到driver層要小心溢位問題。

PVOID MapPhyMem(DWORD phyAddr, DWORD memSize)
{
	PVOID pVirAddr=NULL;	//mapped virtual addr
	PHYMEM_MEM pm;
	DWORD dwBytes=0;
	BOOL bRet=FALSE;
 
	pm.pvAddr=(PVOID)phyAddr;	//physical address
	pm.dwSize=memSize;	//memory size
 
	if (hDriver!=INVALID_HANDLE_VALUE)
	{
		bRet=DeviceIoControl(hDriver, IOCTL_PHYMEM_MAP, &pm,
			sizeof(PHYMEM_MEM), &pVirAddr, sizeof(PVOID), &dwBytes, NULL);
	}
 
	if (bRet && dwBytes==sizeof(PVOID))
		return pVirAddr;
	else
		return NULL;
}

to

PVOID MapPhyMem(DWORD phyAddr, DWORD memSize)
{
	PVOID64 pVirAddr=NULL;	//mapped virtual addr
	PHYMEM_MEM pm;
	DWORD dwBytes=0;
	BOOL bRet=FALSE;
 
	pm.pvAddr=(PVOID64)phyAddr;	//physical address
	pm.dwSize=memSize;	//memory size
 
	if (hDriver!=INVALID_HANDLE_VALUE)
	{
		bRet=DeviceIoControl(hDriver, IOCTL_PHYMEM_MAP, &pm,
			sizeof(PHYMEM_MEM) , &pVirAddr, sizeof(PVOID64), &dwBytes, NULL);
	}
 
	if (bRet && dwBytes==sizeof(PVOID64))
		return (PVOID)pVirAddr;
	else
		return NULL;
}

driver

修改的內容,除了sizeof(PVOID)的判斷改為dwOutBufLen==sizeof(PVOID64),還有宣告的型態。這裡會有個問題是: phyAddr.QuadPart所使用的是LONGLONG,當要存取0x8000000000000000以上的記憶體時,我想應該會出問題。目前我不曉得該如何解決,而且還沒有人使用到這麼大的記憶體吧? Windows所允許的記憶體大小也不到這。

	switch (irpStack->MajorFunction)
	{
	case IRP_MJ_DEVICE_CONTROL:
 
		dwIoCtlCode=irpStack->Parameters.DeviceIoControl.IoControlCode;
 
		switch (dwIoCtlCode)
		{
		case IOCTL_PHYMEM_MAP:
 
			if (dwInBufLen==sizeof(PHYMEM_MEM) && dwOutBufLen==sizeof(PVOID))
			{
				PHYSICAL_ADDRESS phyAddr;
				PVOID pvk, pvu;
 
				phyAddr.QuadPart=(ULONGLONG)pMem->pvAddr;
 
...以下省略

to

	switch (irpStack->MajorFunction)
	{
	case IRP_MJ_DEVICE_CONTROL:
 
		dwIoCtlCode=irpStack->Parameters.DeviceIoControl.IoControlCode;
 
		switch (dwIoCtlCode)
		{
		case IOCTL_PHYMEM_MAP:
			DebugPrint("dwInBufLen=%d,sizeof(PHYMEM_MEM)=%d", dwInBufLen, sizeof(PHYMEM_MEM));
			DebugPrint("dwOutBufLen=%d,sizeof(PVOID)=%d", dwOutBufLen, sizeof(PVOID64));
 
			if ( dwInBufLen==sizeof(PHYMEM_MEM) && dwOutBufLen==sizeof(PVOID64) )
			{
				PHYSICAL_ADDRESS phyAddr;
				PVOID64 pvk, pvu;
 
				phyAddr.QuadPart=(ULONGLONG)pMem->pvAddr;
...以下省略

不要以為把程式拿來編譯成64-bit就會動了,不同的component溝通、相依於作業系統的功能、作業系統特性等,都有可能出現難以解決的bug。另外,你們可能會有疑問的是: 我是如何發現這個問題的? 答案是透過Kernel Driver Debug工具: DebugPrint。等我有時間會在教大家如何使用,因為我花在編譯與安裝成64-bit也花了不少時間。

阿兩內心的獨白: 因為這個玩意兒,讓我第一次去改windows driver的code。