自從操作系統升級到64位以后,就要不斷的需要面對32位、64位的問題。相信有很多人並不是很清楚32位程序與64位程序的區別,以及Program Files (x86),Program Files的區別。同時,對於程序的dll文件應該放到System32文件夾,還是SysWow64,大部分人做的決定是,32位程序放到System32,64位程序放到SysWow64。是不是這樣呢,那么今天就由我身邊發生的一個案例來詳細的說明一下。
dll文件不匹配導致數據庫無法啟動
前段時間,數據庫做了一些功能上的改進,於是用VS2010編譯檢出了一個版本,供測試部測試。測試部拿到數據庫后,通過批處理將數據庫程序,注冊為服務。雖然執行的是批處理,實際上注冊服務的過程,是通過運行數據庫程序,並給其傳入命令行參數來完成的,詳情請看這篇文章玩轉Windows服務系列——Debug、Release版本的注冊和卸載,及其原理。
通過批處理運行程序后,出現如下問題:
出現這種問題,測試部不淡定了,叫我去看。我又試着運行了一下程序,依然出現這個問題。“可是在我的機器上運行的挺好的啊”,這是我說的第一句話,相信很多人看了這句話就會心的笑了。
有問題就是有問題,既然我的機器上可以正常運行,那么測試機為什么不行呢,首先要查找原因。
數據庫是用VS2010編譯的,那么在其他機器上運行,就需要運行的操作系統中以及安裝了VS2010的運行時,否則就會因為缺少程序運行所必須的dll文件而無法正常運行。我想應該是這個原因,但又一想,如果沒有裝運行時的話,會提示缺少msvcr100.dll、msvcp100.dll等文件,上圖中的問題顯然不是缺少dll問題。問題有點復雜,為了簡單,先試着安裝運行時,看能不能解決吧。
將VS2010的x86和x64 Runtime安裝包全裝了一遍。再運行程序,依然是這個醒目的錯誤。
雖然安裝運行時沒有解決這個問題,但根據經驗判斷,要么是缺少dll文件,要么就是dll文件版本出了問題。那么,接下來就是想辦法證明這個猜想。
通過Dependency Walker檢測數據庫程序,所有依賴的dll文件都存在,沒有發現什么問題。然后通過Windows Sysinternals中的ListDLLs工具檢測當前運行的進程已經加載的dll文件,從列表中看到msvcr100.dll沒有加載,估計就是這個dll文件出了問題。從我的機器上找到這個文件,替換了測試機上的msvcr100.dll文件后,數據庫就正常運行了。
原來,剛剛啟動數據庫的時候,提示找不到msvcr100.dll文件,測試的同事就從其他的XP系統的機器上找了這個文件,並分別放入到System32和SysWow64中,於是就導致了上圖中的這個問題。
由於XP系統是32位的,所以找到的msvcr100.dll文件也是32位,當把這個32位程序放到System32文件夾后,啟動64位數據庫,就會加載這個32位dll,由於64位程序只能加載64位dll,所以當程序嘗試加載32位dll時,就會報錯了。
究竟是System32還是SysWow64
Win7、Server2008等64位系統出來以后,為了兼容32位程序,所以采用了Wow64方案,在系統文件夾中,可以看到一個System32文件夾,和一個SysWow64文件夾。雖然這個方案對於程序來說,可以很方便的兼容32位程序,但是對於一般用戶來說,想分辨System32和SysWow64那就有點困難了,因為名字太有迷惑性了。
至於微軟為什么采用Wow64方案,我就不細說了,感興趣的朋友可以看這篇文章:什么是SysWow64。這篇文章詳細的介紹了Wow64技術,以及64位系統兼容32位程序的情況。
最后,我們可以知道:
- SysWow64文件夾,是64位Windows,用來存放32位Windows系統文件的地方,而System32文件夾,是用來存放64位程序文件的地方。
- 當32位程序加載System32文件夾中的dll時,操作系統會自動映射到SysWow64文件夾中的對應的文件。
看到這些,你一定會認為你真正的明白了System32和SysWow64的區別,我也一樣,我以為我真的懂了,但是真的懂了嗎,是真懂了嗎?
無論怎樣還是請你堅持看完。
區分dll文件32位64位的程序讓我倍感迷惑
上面說到的數據庫無法啟動的這種情況,已經遇到了不止一次了。每次遇到這種問題,我都想能不能有個工具可以檢查System32和SysWow64文件夾中的dll程序是不是對應的64位和32位程序。據我所知只有dumpbin可以查看一個dll文件是32位還是64位,但它明顯不是我想要的工具,因為每次只能查看一個文件。
好吧,自己動手,豐衣足食,既然沒有這種工具,那就來寫一個吧,好在判斷dll文件是32位還是64位也不是很難。
Windows系統下,exe、dll文件都可以稱為PE文件,他們有相同的文件格式,稱為PE文件格式。
PE文件的第一個部分是IMAGE_DOS_HEADER,大小為64B,對於檢查32位64位來說,有一個重要的成員e_lfanew,這個成員的值為IMAGE_NT_HEADERS的偏移。
IMAGE_DOS_HEADER的定義如下:
typedef struct _IMAGE_DOS_HEADER {//(注:最左邊是文件頭的偏移量。) +0h WORD e_magic //Magic DOS signature MZ(4Dh 5Ah) DOS可執行文件標記 +2h WORD e_cblp //Bytes on last page of file +4h WORD e_cp //Pages in file +6h WORD e_crlc //Relocations +8h WORD e_cparhdr //Size of header in paragraphs +0ah WORD e_minalloc //Minimun extra paragraphs needs +0ch WORD e_maxalloc //Maximun extra paragraphs needs +0eh WORD e_ss //intial(relative)SS value DOS代碼的初始化堆棧SS +10h WORD e_sp //intial SP value DOS代碼的初始化堆棧指針SP +12h WORD e_csum //Checksum +14h WORD e_ip //intial IP value DOS代碼的初始化指令入口[指針IP] +16h WORD e_cs //intial(relative)CS value DOS代碼的初始堆棧入口 +18h WORD e_lfarlc //File Address of relocation table +1ah WORD e_ovno //Overlay number +1ch WORD e_res[4] //Reserved words +24h WORD e_oemid //OEM identifier(for e_oeminfo) +26h WORD e_oeminfo //OEM information;e_oemid specific +29h WORD e_res2[10] //Reserved words +3ch DWORD e_lfanew //Offset to start of PE header 指向PE文件頭 } IMAGE_DOS_HEADER;
IMAGE_NT_HEADERS的定義如下:
typedef struct _IMAGE_NT_HEADERS { +0h DWORD Signature; +4h IMAGE_FILE_HEADER FileHeader; +18h IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS;
Signature 字段:在一個有效的 PE 文件里,Signature 字段被設置為00004550h,ASCII 碼字符是“PE00”。標志這 PE 文件頭的開始。“PE00” 字符串是 PE 文件頭的開始,DOS 頭部的 e_lfanew 字段正是指向這里。
IMAGE_FILE_HEADER 結構定義:
typedef struct _IMAGE_FILE_HEADER { +04h WORD Machine; // 運行平台 +06h WORD NumberOfSections; // 文件的區塊數目 +08h DWORD TimeDateStamp; // 文件創建日期和時間 +0Ch DWORD PointerToSymbolTable; // 指向符號表(主要用於調試) +10h DWORD NumberOfSymbols; // 符號表中符號個數(同上) +14h WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32 結構大小 +16h WORD Characteristics; // 文件屬性 } IMAGE_FILE_HEADER;
其中Machine字段表示可執行文件的目標CPU類型:
- IMAGE_FILE_MACHINE_I386 0x014c x86
- IMAGE_FILE_MACHINE_IA64 0x0200 Intel Itanium
- IMAGE_FILE_MACHINE_AMD64 0x8664 x64
這樣不是很直觀,上張圖來看一下:
有了這些,我們就可以通過程序來判斷32位、64位了,代碼如下:
public static bool IsPE32(string path) { FileStream file = File.OpenRead(path); //移動到e_lfanew的位置處 stream.Seek(0x40 - 4, SeekOrigin.Begin); byte[] buf = new byte[4]; stream.Read(buf, 0, buf.Length); //根據e_lfanew的值計算出Machine的位置 int pos = BitConverter.ToInt32(buf,0) + 4; stream.Seek(pos, SeekOrigin.Begin); buf = new byte[2]; stream.Read(buf, 0, buf.Length); //得到Machine的值,0x14C為32位,0x8664為64位 Int16 machine = BitConverter.ToInt16(buf, 0); if (machine == 0x14C) { return true; } else { return false; } }
最核心的功能完成了,剩下的就是界面和遍歷文件夾了,效果圖:
根據檢測結果和實際情況判斷,檢測結果沒問題。那么就開始真正的檢測吧,System32和SysWow64。檢測結果如下圖:
從圖中看出,System32、SysWow64中檢測出的所有的文件均為32位程序,根據常識也可以判斷出,實際肯定不是這樣的。一定是程序出了什么問題,那么直接用十六進制編輯器看一下兩個文件是否一致吧。
再次判斷究竟是System32還是SysWow64——意想不到的坑
通過UE查看兩個文件夾中的msvcr110d.dll確實都是32位程序,而且用Beyond Compare進行比較,兩個文件也沒有差異。用工具查看兩個文件的MD5也是完全一致:
難道兩個文件真的都是32位嗎,我還是覺得不太可能。
接下來將System32和SysWow64中的msvcr110d.dll分別移動到其他文件夾,這樣System32和SysWow64就沒有這個dll文件了,然后運行一個32位的需要這個dll文件的程序PeTest,提示找不到這個dll文件。分別將原來System32和SysWow64中的msvcr110.dll拷貝到這個PeTest所在的目錄,運行程序。當使用SysWow64中的msvcr110d.dll時,程序可以正常運行,說明這個文件確實是32位。當使用System32中的msvcr110d.dll時,程序無法正常運行,出現文章開始時提到的錯誤。為什么通過Beyond Compare、UE、MD5檢測為同樣的dll文件,一個可以正常運行,另外一個就不可以呢。
再次思考這兩句話:
- SysWow64文件夾,是64位Windows,用來存放32位Windows系統文件的地方,而System32文件夾,是用來存放64位程序文件的地方。
- 當32位程序加載System32文件夾中的dll時,操作系統會自動映射到SysWow64文件夾中的對應的文件。
32位程序加載System32文件夾中的dll文件,操作系統會自動映射到SysWow64文件夾,也就是說64位程序,系統不會再做映射。
通過任務管理器查看UE、Beyond Compare和MD5三個進程全部為32位進程,即三個程序全部是32位。
至此我們可以重新理解“32位程序加載System32文件夾中的dll文件,操作系統會自動映射到SysWow64文件夾”這句話,應該是“只要32位程序訪問System32文件夾,無論是加載dll,還是讀取文本信息,都會被映射到SysWow64文件夾”。
這個理解對嗎,我們來做一個實驗驗證一下。
找一個32位的文本編輯器Notepad++(Win7系統自帶的是64位),在SysWow64文件夾中新建一個1.txt的文件,打開編輯此文件,內容為SysWow64。然后在打開文件對話框中的輸入框中輸入“C:\Windows\System32\1.txt”,打開文件,看到內容為SysWow64,如圖所示:
System32中沒有創建1.txt文件,32位程序訪問System32中的1.txt文件,被自動映射到SysWow64文件夾中的1.txt文件,而如果用64位的Notepad編輯器打開System32中的1.txt文件,就會提示找不到文件:
由此就可以驗證猜想“只要32位程序訪問System32文件夾,無論是加載dll,還是讀取文本信息,都會被映射到SysWow64文件夾”是正確的。
Program Files (x86)與Program Files
由System32與SysWow64的情況,考慮到Program Files (x86)與Program Files是不是也是這種情況。當32位程序訪問Program Files目錄時,會被自動映射到Program Files (x86)目錄?
還是通過1.txt的方式來驗證,發現當32位程序訪問Program Files目錄時,並沒有被映射到Program Files (x86)目錄。
32位程序真的需要訪問System32嗎
經過了這么多驗證,總算是知道32位程序無法訪問System32,只有64位程序才能訪問,由此認為,這是Windows的一個非常大的坑。但是仔細想想,32位程序真的需要訪問System32嗎。就用這個dll檢測工具來說吧。
如果在64位系統上,32位程序無法訪問System32,為了訪問它,就需要編譯為64位。而如果程序編譯為64位,就無法在32位系統上運行,同時由於在32位系統上不需要檢測32位、64位,所以只需要32位程序即可。這可真是一個矛盾的事情,難道必須編譯兩個程序,一個32位,一個64位,來適應不同的操作系統嗎。如果是C++的話,那么答案是這樣的,必須編譯一個32位一個64位。而DotNet就不一樣了,編譯的時候選擇“AnyCPU”,並且不選擇“首選32位”(VS2012中默認選中),編譯后的程序,可以同時在32位和64位系統上運行,32位系統上是32位進程,64位系統上是64位進程,是不是很方便呢,這正是DotNet和AnyCPU的魅力所在。
至此,dll檢測程序,不需要做任何代碼修改,只需在編譯的時候選擇AnyCPU,並去掉“首選32位”選項,即可正常檢測System32、SysWow64文件夾中的dll文件。
32位程序與64位程序的區別總結
至此,我想應該是真的明白了System32與SysWow64的區別了吧,這個不大不小的坑,算是邁過去了,那么就來總結一下32位程序與64位程序的區別:
- SysWow64文件夾,是64位Windows,用來存放32位Windows系統文件的地方,而System32文件夾,是用來存放64位程序文件的地方。
- .Net程序以AnyCPU配置,並選擇“首選32位”編譯,會以32位的進程運行,此時就無法訪問System32文件夾中的文件;如果沒有選擇“首選32位”,則會以64位的進程運行,這樣就可以訪問System32文件夾了。(VS2012中,“首選32位”默認是選中的)。
- 32位程序的尋址空間有限,最多達到4G,而64位程序的尋址空間可以達到TB級,想要使用大內存的話,就升級到64位吧,好在DotNet程序從32位升級到64位比較簡單,不像C++那么麻煩。
- 32位程序訪問System32目錄,會自動被映射到SysWOW64目錄,而64位程序可以訪問System32目錄和SysWOW64目錄。
- 32位程序與64位程序有各自的注冊表。
- 32位與64位程序都可以訪問Program Files (x86)與Program Files目錄。
- 更多區別可以參考:32-bit and 64-bit Windows: frequently asked questions。
工作與學習過程中會遇到很多坑,一不小心就會跌倒,但是從哪里跌倒的就從哪里爬起來,總結經驗教訓,以飽滿的熱情再次起航,勝利就在不遠的前方。
由於本人水平有限,文中如有不對之處,還請批評指正,本人不勝感激!