跳到主要內容

Windows DLL - 32-bit dll with 64-bit driver

前言

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

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的有這麼簡單嗎? 除此之外,中間傳遞的資料型態也會影響到正常功能。

設定driver專案

我開發環境是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。

Reference

留言

這個網誌中的熱門文章

Show NIC selection when setting the network command with the device option

 Problem  在answer file中設定網卡名稱後,安裝時會停在以下畫面: 所使用的command參數如下: network --onboot = yes --bootproto =dhcp --ipv6 =auto --device =eth1 Diagnostic Result 這樣的參數,以前試驗過是可以安裝完成的。因此在發生這個問題後,我檢查了它的debug console: 從console得知,eth1可能是沒有連接網路線或者是網路太慢而導致的問題。後來和Ivy再三確認,有問題的是有接網路線的網卡,且問題是發生在activate階段: Solution 我想既然有retry應該就有次數或者timeout限制,因此發現在Anaconda的說明文件中( link ),有提到dhcptimeout這個boot參數。看了一些人的使用範例,應該是可以直接串在isolinux.cfg中,如下: default linux ksdevice = link ip =dhcp ks =cdrom: / ks.cfg dhcptimeout = 90 然而我在RHEL/CentOS 6.7與6.8試驗後都無效。 因此我就拿了顯示的錯誤字串,問問Google大師,想找一下Anaconda source code來看一下。最後找到別人根據Anaconda code修改的版本: link ,關鍵在於setupIfaceStruct函式中的setupIfaceStruct與readNetConfig: setupIfaceStruct: 會在dhcp時設定dhcptimeout。 readNetConfig: 在writeEnabledNetInfo將timeout寫入dhclient config中;在wait_for_iface_activation內會根據timeout做retry。 再來從log與code可以得知,它讀取的檔案是answer file而不是boot command line。因此我接下來的測試,就是在answer file的network command上加入dhcptimeout: network --onboot = yes --bootproto =dhcp --ipv6 =auto --device =eth1 --dhcptimeo

解決RobotFramework從3.1.2升級到3.2.2之後,Choose File突然會整個Hand住的問題

考慮到自動測試環境的維護,我們很久以前就使用java去執行robot framework。前陣子開始處理從3.1.2升級到3.2.2的事情,主要先把明確的runtime語法錯誤與deprecate item處理好,這部分內容可以參考: link 。 直到最近才發現,透過SeleniumLibrary執行Choose File去上傳檔案的動作,會導致測試案例timeout。本篇文章主要分享心路歷程與解決方法,我也送了一條issue給robot framework: link 。 我的環境如下: RobotFramework: 3.2.2 Selenium: 3.141.0 SeleniumLibrary: 3.3.1 Remote Selenium Version: selenium-server-standalone-3.141.59 首先並非所有Choose File的動作都會hang住,有些測試案例是可以執行的,但是上傳一個作業系統ISO檔案一定會發生問題。後來我透過wireshark去比對新舊版本的上傳動作,因為我使用 Remote Selenium ,所以Selenium會先把檔案透過REST API發送到Remote Selenium Server上。從下圖我們可以發現,在3.2.2的最後一個TCP封包,比3.1.2大概少了500個bytes。 於是就開始了我trace code之路。包含SeleniumLibrary產生要送給Remote Selenium Server的request內容,還有HTTP Content-Length的計算,我都確認過沒有問題。 最後發現問題是出在socket API的使用上,就是下圖的這支code: 最後發現可能因為開始使用nio的方式送資料,但沒處理到尚未送完的資料內容,而導致發生問題。加一個loop去做計算就可以解決了。 最後我有把解法提供給robot framework官方,在他們出新的版本之前,我是將改完的_socket.py放在我們自己的Lib底下,好讓我們測試可以正常進行。(shutil.py應該也是為了解某個bug而產生的樣子..)

PostgreSQL - Unattended installation on windows

Introduction 要將別人軟體包裝到自己軟體中,不可或缺的東西就是Unattended installation。以Unattended installation來說,我們可以選擇透過Installer的silent mode安裝,也可以透過把目標軟體做成portable的版本。本篇文章分享這兩種方法,教導大家如何將PostgreSQL透過Unattended installation方式安裝到目標系統成為service。 Note. 本篇以PostgreSQL 10.7為例。 Install with installer Tips 安裝程式或反安裝程式的參數,除了可以直接上官網搜尋Installation User Guide以外,也可以直接使用help參數查詢: postgresql- 10.7 - 2 -windows-x64.exe --help Windows安裝程式主要有EnterpriseDB與BigSQL兩種。BigSQL版本安裝元件是透過網路下載且支援參數不如EnterpriseDB版本多,以我們需求來說,我們傾向於使用EnterpriseDB版本。接下來分享給大家安裝與反安裝方法。 Installation @ echo off set INSTALL_DIR =C:\postgres10 set INSTALLER =postgresql- 10.7 - 2 -windows-x64.exe   rem options for installation set SSMDB_SERVICE =postgresql- 10 set MODE =--unattendedmodeui none --mode unattended   set DB_PASSWD =--superpassword postgres set DB_PORT =--serverport 5432   set SERVICE_NAME =--servicename % SSMDB_SERVICE %   set PREFIX =--prefix "%INSTALL_DIR%" set DATA_DIR =--datadir "%INSTALL_DIR%\data"   set OPTIONS =