最近斷斷續續接觸了些64位彙編的知識,這裡小結一下,一是階段學習的回顧,二是希望對64位彙編新手有所協助。我也是剛接觸這方面知識,文中肯定有錯誤之處,大家多指正。
文章的標題包含了本文的四方面主要內容:
(1)Windows:本文是在windows環境下的組譯工具設計,調試環境為Windows Vista 64位版,調用的均為windows API。
(2)X64:本文討論的是x64彙編,這裡的x64表示AMD64和Intel的EM64T,而不包括IA64。至於三者間的區別,可自行搜尋。
(3)彙編:顧名思義,本文討論的程式設計語言是彙編,其它進階語言的64位編程均不屬於討論範疇。
(4)入門:既是入門,便不會很全。其一,文中有很多知識僅僅點到為止,更深入的學習留待日後努力。其二,便於類似我這樣剛接觸x64彙編的新手入門。
本文所有代碼的調試環境:Windows Vista x64,Intel Core 2 Duo。
1. 建立開發環境
1.1 編譯器的選擇
對應於不同的x64彙編工具,開發環境也有所不同。最普遍的要算微軟的MASM,在x64環境中,相應的編譯器已經更名為ml64.exe,隨Visual Studio 2005一起發布。因此,如果你是微軟的忠實fans,直接安裝VS2005既可。運行時,只需開啟相應的64位命令列視窗(圖1),便可以用ml64進行編譯了。第二個推薦的編譯器是GoASM,共包含三個檔案:GoASM編譯器、GoLINK連結器和GoRC資源編譯器,且內建了Include目錄。它的最大好外是小,不用為了學習64位彙編安裝幾個G 的VS。因此,本文的代碼就在GoASM下編譯。
第三個Yasm,因為不熟,所以不再贅述,感興趣的朋友自行測試吧。
不同的編譯器,文法會有一定差別,這在下面再說。
1.2 IDE的選擇
搜遍了Internet也沒有找到支援asm64的IDE,甚至連個Editor都沒有。因此,最簡單的方法是自行修改EditPlus的masm文法檔案,這也是我採用的方法,至少可以得到文法高亮。當然,如果你懶得動手,那就用notepad吧。
沒有IDE,每次編譯時間都要手動輸入不少參數和選項,做個批處理就行了。
1.3 硬體與作業系統
硬體要求就是64位的CPU。作業系統也必須是64位的,如果在64位的CPU上安裝了32位的作業系統,就算編譯成功也無法運行程式。2. 寄存器的改變
彙編是直接與寄存器打交道的語言,因此硬體對語言影響很大。先來看看x64與x32相比在硬體上多了什麼,變了什麼(圖2)。
X64多了8個通用寄存器:R8、R9、R10、R11、R12、R13、R14、R15,當然,它們都是64位的。另外還增加了8個128位XMM寄存器,不過通常用不著。
X32中原有的寄存器在X64中均為擴充為64位,且名稱的第一個字母從E改為R。不過我們還是可以在64位程式中調用32位的寄存器,如RAX(64位)、EAX(低32)、AX(低16位)、AL(低8位)、AH(8到15位),相應的有R8、R8D、R8W和R8B。不過不要在程式中使用如AH之類的寄存器,因為在AMD的CPU上這種用法會與某些指令產生衝突。第一個x64組譯工具
本節,我們開始編寫自己的第一個x64組譯工具。在這之前,先講一下calling convention的改變。
3.1 API調用方式
把Calling convention放在第一個講,代表它的重要性。在32位彙編中,我們調用一個API時,採用的是stdcall,它有兩個特點:一是所有參數入棧,通過椎棧傳遞;二是被調用的API負責棧指標(ESP)的恢複,我們在調用MessageBox後不用add esp,14h,因為MessageBox已經恢複過了。
而在x64彙編中,兩方面都發生了變化。一是前四個參數分析通過四個寄存器傳遞:RCX、RDX、R8、R9,如果還有更多的參數,才通過椎棧傳遞。二是調用者負責椎棧空間的分配與回收。
下面給出一段代碼,功能是顯示一個簡單的MessageBox,注意對RSP的操作:
代碼:程式碼:;範例程式碼1.asm
;文法:GoASM
DATA SECTION
text db 'Hello x64!', 0
caption db 'My First x64 Application', 0
CODE SECTION
START:
sub rsp,28h
xor r9d,r9d
lea r8, caption
lea rdx, text
xor rcx,rcx
call MessageBoxA
add rsp,28h
ret這段代碼是在GoASM中編譯,指令部分GoASM與ML64差不多,關鍵是一些宏的定義有差別。比如masm中的.code,在這裡就成了CODE SECTION。下面再說區別,先編譯。GoASM中編譯分兩步:
(1) 編譯:goasm /x64 1.asm
(2) 連結:golink 1.obj user32.dll
如果一些正常,命令列中應顯示圖3的內容。這段代碼是在GoASM中編譯,指令部分GoASM與ML64差不多,關鍵是一些宏的定義有差別。比如masm中的.code,在這裡就成了CODE SECTION。下面再說區別,先編譯。GoASM中編譯分兩步:
(1) 編譯:goasm /x64 1.asm
(2) 連結:golink 1.obj user32.dll
如果一些正常,命令列中應顯示圖3的內容。程式碼:;範例程式碼2.asm
;文法:ML64
extrn MessageBoxA: proc
.data
text db 'Hello x64!', 0
caption db 'My First x64 Application', 0
.code
Main proc
sub rsp,28h
xor r9d,r9d
lea r8, caption
lea rdx, text
xor rcx,rcx
call MessageBoxA
add rsp,28h
ret
Main ENDP
endml64 2.asm /link /subsystem:windows /entry:Main user32.lib。如果正常,應該很有意思吧,在64位系統下,我們仍然調用user32的API。可能是名稱用習慣了,微軟自己都懶得改了吧。
3.2 64位的椎棧
代碼中還有一處值得注意,那就是sub rsp,28h和add rsp,28h。28h這個數值是怎麼來的呢?
首先,x64中椎棧被擴充為64位;其次,我們在調用MessageBoxA時,要給四個參數外加一個返回地址留空間,因此8(位)*5=40=28h。
另外一些小問題要注意,AMD64不支援push 32bit寄存器的指令,最好的方法就是push和pop都用64位寄存器。EM64T如何?看了下Intel的開發手冊,各個指令都分三種情況:純32位、純64位和32與64位混合。下面是手冊的片段:
Opcode* Instruction 64-Bit Mode Compat/Leg Mode Description
FF /6 PUSH r/m16 Valid Valid Push r/m16.
FF /6 PUSH r/m32 N.E. Valid Push r/m32.
FF /6 PUSH r/m64 Valid N.E. Push r/m64.
Default operand size 64-bits.
沒別的好方法,使用中多注意,盡量在64位程式中保用64位寄存器。
4. 一些參考資料
寫完了第一個hello world,本文就此打住。本還想寫一些內容,但掌握不深,留待下回吧。感覺有些資料不得不在第一篇文章中放出來,因為它們是現有學習x64彙編的最好教材了,文中很多代碼和知識點也來自於這些資料。
(1)《Moving to Windows x64》,出自:http://www.ntcore.com/Files/vista_x64.htm
(2)GoASM的協助文檔,目前最好的64位彙編教程。出自:www.jorgon.freeserve.co.uk
(3)《開始進行 64 位元 Windows 系統編程之前需要瞭解的所有資訊》,出自:http://www.microsoft.com/china/MSDN/library/Windev/64bit/issuesx64.mspx
(4)來自CodeGurus的兩篇文章
《Assembler & Win64》,
http://www.codegurus.be/codegurus/Programming/assembler&win64_en.htm
《bout RIP relative addressing》
http://www.codegurus.be/codegurus/Programming/riprelativeaddressing_en.htm
(5)AMD開發手冊
(6)Intel開發手冊,注意是新的《ntel 64 and IA-32 Architectures software Developer’s Manual》
64位技術現在還不成熟,沒有好調試器,但是我們搞技術的總是對新東西充滿了好奇和熱情。這個理由就足夠我們現在開始學習64位彙編了!OK,Let’s go on。
1. 再說Calling convention
關於API的調用方式,在入門(1)中說了一些,不過感覺有必要再講兩點。一是在調用API時椎棧的架構,也就是Stack Frame,二是利用反組譯碼64位C/C++程式來研究calling convention。
先說Stack Frame。圖1是一個通用的椎棧架構。
在一個使用STDCALL的32位程式中,stack frame的四項工作:
(1) 傳入參數的調用;
(2) 在返回caller時,callee要負責平衡椎棧;
(3) 給局部變數提供空間;
(4) 保證ebx、esi、edi和ebp四個寄存器的值不變(這種寄存器被稱為non-volatile)。
在64位環境中,少了一個平衡椎棧的任務,因為平衡椎棧的工作由caller負責了,因此callee的stack frame只剩下三項工作:
(1) 將寄存器傳入的參數和其它超過4個以上的參數在椎棧上儲存(入棧);
(2) 給局部變數提供空間;
(3) 保證non-volatile寄存器的值不變,包括ebp、ebx、rdi、rsi、r12到r15,xmm6到xmm15。
所以,在一個函數的開始往往有如下代碼:
MOV [RSP+8h],RCX
MOV [RSP+10h],RDX
MOV [RSP+18h],R8
MOV [RSP+20h],R9
PUSH RBP
MOV RBP,RSP
而在返回時會有如下代碼:
LEA RSP,[RBP]
POP RBP
RET