通常,高級語言都具有向程序員隱藏許多普通的和重複性細節這一非常好的優點,這樣程序員就可以專注於他們的目標。然而,有時程序員必須使用較低級語言,例如當編寫直接處理硬件的代碼或編寫對性能極其敏感的代碼的時候。彙編語言是最接近硬件的編程語言,這就很自然使它成爲上述那些情況下最終使用的一種語言。
本文假設您對計算機設計(例如,您應該知道處理器中有寄存器並能訪問內存)和操作系統(系統調用、異常和進程堆棧)有基本瞭解。本文對於不熟悉彙編的 PowerPC 程序員以及已知道 ia32 彙編並想擴展眼界的程序員都很有用。
PowerPC 簡介
PowerPC 體系結構規範(PowerPC Architecture Specification)發佈於 1993 年,它是一個 64 位規範 ( 也包含 32 位子集 )。幾乎所有常規可用的 PowerPC(除了新型號 IBM RS/6000 和所有 IBM pSeries 高端服務器)都是 32 位的。
PowerPC 處理器有廣泛的實現範圍,包括從諸如 Power4 那樣的高端服務器 CPU 到嵌入式 CPU 市場(任天堂 Gamecube 使用了 PowerPC)。PowerPC 處理器有非常強的嵌入式表現,因爲它具有優異的性能、較低的能量損耗以及較低的散熱量。除了象串行和以太網控制器那樣的集成 I/O,該嵌入式處理器與“臺式機”CPU 存在非常顯著的區別。例如,4xx 系列 PowerPC 處理器缺乏浮點運算,並且還使用一個受軟件控制的 TLB 進行內存管理,而不是象臺式機芯片中那樣採用反轉頁表。
PowerPC 處理器有 32 個(32 位或 64 位)GPR(通用寄存器)以及諸如 PC(程序計數器,也稱爲 IAR/指令地址寄存器或 NIP/下一指令指針)、LR(鏈接寄存器)、CR(條件寄存器)等各種其它寄存器。有些 PowerPC CPU 還有 32 個 64 位 FPR(浮點寄存器)。
RISC
PowerPC 體系結構是 RISC(精簡指令集計算)體系結構的一個示例。因此:
- 所有 PowerPC(包括 64 位實現)都使用定長的 32 位指令。
- PowerPC 處理模型要從內存檢索數據、在寄存器中對它進行操作,然後將它存儲回內存。幾乎沒有指令(除了裝入和存儲)是直接操作內存的。
應用程序二進制接口(ABI)
從技術而言,開發人員可以將任一 GPR 用於任何操作。例如,由於不存在“堆棧指針寄存器”;爲此程序員就可以使用任何寄存器。實際上,定義一組約定很有用,這樣二進制對象就可以與不同的編譯器和預先編寫好的彙編代碼進行互操作。
調用約定是由使用的 ABI(應用程序二進制接口)決定的。ppc32 Linux 和 NetBSD 實現使用 SVR4(System V R4)ABI,而 ppc64 Linux 仿效了 AIX,使用 PowerOpen ABI。ABI 還指定當調用子例程時哪些寄存器被認爲是易失型的(調用者保存(caller-save))以及哪些被認爲是非易失型的(被調用者保存(callee-save)),以及許多其它內容。
SVR4 ABI 指定了一些行爲的具體示例:
- 由於 PowerPC 擁有如此多的 GPR(32 個,而相比之下 ia32 只有 8 個),所以傳遞參數的寄存器從
gpr3
開始。 - 寄存器
gpr3
到gpr12
是易失型的(調用者保存)寄存器,如果需要的話,在調用子例程之前必須先保存它們並在返回之後恢復它們。 - 寄存器
gpr1
用來作爲棧幀指針。
SVR4 的許多特性與 PowerOpen ABI 的相同,這樣非常有助於互操作性。
何時使用匯編
在“Assembly HOWTO”(請參閱 參考資料獲取鏈接)中列出的所有優缺點 PowerPC 都有。
機器特定的寄存器
有時您必須接觸較高級的語言完全不瞭解的 CPU 寄存器。尤其在編寫操作系統的過程中會碰到這樣的情況。一個簡單示例是爲您的代碼分配它自己的堆棧 — 在 PowerPC 上,您必須設置
r1
。C 編譯器將只對 r1
遞增或遞減,所以如果您的應用程序直接在硬件上運行,那麼在調用 C 代碼之前您必須設置
r1
。另一個示例是操作系統的異常處理程序,它必須很仔細地保存和恢復狀態,每次只對一個寄存器進行操作,直到調用較高級代碼是安全的爲止。
但是,當您面臨必須使用低級硬件特性的情況時,您應該儘可能不使用匯編實現:
- C 代碼是可移植的併爲大量開發人員所瞭解;彙編代碼(尤其是 PowerPC 彙編)卻不是。
- 較高級代碼的調試常常比彙編容易得多。
- 較高級代碼在定義上比彙編更易於表達;換句話說,您可以使用較少代碼(在較短時間內)完成較多任務。
如果您發現您在用匯編編寫諸如循環或 C 結構那樣的高級構造,那麼請後退一步,先考慮使用其它語言是否會更容易完成。一般規則是使用足夠恰當的彙編就可以允許您使用較高級語言來完成。
優化
人們想要使用彙編語言的最普遍原因之一是爲了使慢程序運行得更快。但在這樣的情況中,彙編絕對應該是您最後的選擇。
對優化的一般建議已超出了本文的範圍,不過以下是一些着手點:
- 概要分析
在開始任何優化工作之前您 必須概要分析您的代碼。這不僅告訴您“熱點”在哪裏(它們常常不在您所期望的地方!),而且它還在您完成時向您證明您已對一切進行了優化。一旦您找到了熱點,您就可以開始優化高級代碼(而不是嘗試用匯編對它重寫)。 - 算法優化
不管您的彙編是如何緊湊,如果您使用 n 4算法,那麼您的代碼運行起來還會令人難以置信的慢。您應該先嚐試包括使用更合適的數據結構在內的一些其它技術。如果您通過鏈表進行重複迭代,那麼請考慮使用散列表、二叉樹或適合您應用程序的任何方法。
編譯器所能做的工作幾乎總是比您編寫彙編所能做的要好得多!不要嘗試用匯編重寫高級代碼,請明智地利用諸如 -O3
之類的優化選項和象
__inline__
那樣的 C 僞指令。編譯器知道象指令調度之類的訣竅,它考慮到處理器的內部結構並嘗試使所有流水線總是維持全滿。那樣可能涉及在指令流裏移動指令要比所要求的移動時間還要發生得早,這樣做可以使在 CPU 等待內存完成讀寫時避免流水線的延遲。除非您使用匯編編寫代碼已經有許多年了,否則大多數人都不能親手正確地執行這些任務。
Altivec(也稱爲 VMX)是 Motorola 的 74xx(“G4”)系列處理器中的一個 SIMD(單指令多數據(Single Instruction Multiple Data))128 位向量協處理器。因此,實際上它被認爲是一組機器特定的寄存器,但它只用於優化,所以將其包含在本節進行介紹。(Altivec 可以歸入到本文的兩節內容:“機器特定的寄存器”或“優化”。我選擇將它放在“優化”之中。)Altivec 可以非常有效地用於諸如科學計算或視頻計算那樣的應用程序中。
Altivec 能夠非常快速地執行某些操作。然而,它確實付出了一定代價:對 Altivec 寄存器的 128 位裝入和存儲在內存中需要 128 位對齊。更糟糕的是,如果執行了未對齊的訪問,Altivec 不會提出對齊異常;它只執行對齊訪問,以及裝入或存儲程序員無意觸及的內存。
目前的 GNU binutil 支持 Altivec 指令。然而,gcc 版本 3.1 之前的版本卻不支持,無論通過 C 代碼的自動向量化(很明顯,在象 Forth 那樣的語言中可能支持)還是通過顯式的 C 擴展都不支持。爲了通過 GUN toolchain 使用 Altivec,您必須用匯編編碼 — 所以,使用 Altivec 是用匯編編碼的一個很好的理由。
gcc 3.1 通過新的“向量擴展(Vector Extensions)”(請參閱 參考資料獲取有關該內容的更多信息的鏈接)具有對 Altivec 的支持。遺憾的是,目前幾乎很少有人使用 gcc 3.1,並且也沒有 PowerPC Linux 分發版提供該工具。
同樣遺憾的是,因爲作者沒有 G4,所以不能非常詳細地描述 Altivec 指令如何使用。有關 Altivec 的更多信息請參考 altivec.org(請參閱 參考資料)。
如何學習彙編
gcc 是開始學習彙編的最佳工具(適用於任何體系結構)。 gcc -O3 -S file.c
將以 gas 可編譯的格式生成
file.s
( gas 是 GNU 彙編程序)。在您喜愛的編輯器中打開 file.s
,您就會看見 C 代碼的彙編輸出。
您可能會看到您不理解的指令。可以在 The PowerPC Architecture: A Specification for a New Family of RISCProcessors, 2nd. Ed以及
PowerPC Microprocessor Family: The Programming Environments for32-bit Microprocessors(請參閱
參考資料獲取這些文檔的鏈接)中進行查閱。不過,就象學習任何(口語)語言一樣,某些單詞很重要,您應該知道這些單詞,而其它的可以被安全地忽略,直到您弄清了代碼更爲重要的特性。一個重要指令的典型示例就是分支系列的指令,例如
blr
。
彙編示例
Hello World — ia32 彙編
清單 1 直接複製自 Assembly HOWTO 中的 gas 示例,糟糕的是它完全特定於 ia32。它進行兩個直接的系統調用:第一個寫到標準輸出;第二個退出應用程序(包含返回代碼
0
)。直接進行系統調用非常少見;一般情況下,應用程序與一個封裝所有系統調用的 libc 庫相連。
清單 1. ia32 彙編( 下載此代碼樣本)
.data # section declaration msg: .string "Hello, world!\n" len = . - msg # length of our dear string .text # section declaration # we must export the entry point to the ELF linker or .global _start # loader. They conventionally recognize _start as their # entry point. Use ld -e foo to override the default. _start: # write our string to stdout movl $len,%edx # third argument: message length movl $msg,%ecx # second argument: pointer to message to write movl $1,%ebx # first argument: file handle (stdout) movl $4,%eax # system call number (sys_write) int $0x80 # call kernel # and exit movl $0,%ebx # first argument: exit code movl $1,%eax # system call number (sys_exit) int $0x80 # call kernel
Hello World — PPC32 彙編
清單 2 是將相同代碼直接轉換成 PowerPC 彙編代碼。
清單 2. PPC32 彙編( 下載此代碼樣本)
.data # section declaration - variables only msg: .string "Hello, world!\n" len = . - msg # length of our dear string .text # section declaration - begin code .global _start _start: # write our string to stdout li 0,4 # syscall number (sys_write) li 3,1 # first argument: file descriptor (stdout) # second argument: pointer to message to write lis 4,msg@ha # load top 16 bits of &msg addi 4,4,msg@l # load bottom 16 bits li 5,len # third argument: message length sc # call kernel # and exit li 0,1 # syscall number (sys_exit) li 3,1 # first argument: exit code sc # call kernel
有關清單 2 的一般說明
PowerPC 彙編需要一個目標寄存器用於所有寄存器到寄存器的操作(因爲它是 RISC 體系結構)。該寄存器總是位於參數列表的第一個。
在 PPC Linux 中,系統調用是通過 gpr0
中的系統調用(syscall)號和以 gpr3
開始的參數進行的。系統調用號、參數序列以及參數個數在其它 PowerPC 操作系統(NetBSD、Mac OS 等)中可能會有所不同,這是程序員通常利用 libc 庫(它處理特定於 OS 的細節)進行系統調用的一個原因。
寄存器表示法
PowerPC 寄存器有編號,而沒有名稱。對於初學者來說,有時這會使人混淆,因爲 tts 無法輕易地與寄存器區分開。“ 3
”可以表示值 3 或者寄存器
gpr3
,或者浮點 fpr3
,或者特殊用途的寄存器 spr3
。習慣了就好了。:)
立即指令li
表示“立即裝入”,它是表示“在編譯時獲取已知的常量值並將它存儲到寄存器中”的一種方法。立即指令的另一個示例是 addi
,例如,
addi 3,3,1
會按照 1 來遞增 gpr3
的內容,然後將結果存儲回 gpr3
。將之與
add 3,3,1
進行對照,後者將按照 gpr1
的內容 來遞增 gpr3
的內容,並將結果存儲回
gpr3
。
以“i”結束的指令通常是立即指令。
助記符li
實際上不是一條指令;它真正的含義是助記符。 助記符有點象預處理器宏:它是彙編程序接受的但祕密轉換成其它指令的一條指令。在這種情況中,
li 3,1
實際上被定義爲 addi 3,0,1
。
眼尖的讀者會注意到那些指令沒有必要完全相同: addi
實際上向 gpr0
的 內容加 1,將結果存儲到
gpr3
,是這樣嗎?的確是的,不過 PowerPC 規範指出 gpr0
有時具有值,而有時當作 0,這取決於環境。在這種情況中(而且
addi
描述顯式地聲明瞭這一點),0 表示值 0,而不是寄存器 gpr0
。
助記符對彙編程序開發人員以外的其它任何人根本不重要,但當您查看反彙編輸出時助記符會使人迷惑。不過,GNU objdump -d
可以非常有效地顯示原始的助記符,而不是實際出現在文件中的指令。例如,
objdump
將顯示助記符 nop
,而不是 ori 0,0,0
(真正使用的指令)。
裝入指針
Hello World 示例最有趣部分是我們如何裝入 msg
的地址。正如前面提到的,PowerPC 使用定長的 32 位指令(與 ia32 相反,後者使用可變長度的指令)。這個 32 位指令恰好是一個 32 位的整數。該整數被分成大小不同的字段:
清單 3. addi 機器代碼格式
-------------------------------------------------------------------------- | opcode | src register | dest register | immediate value | | 6 bits | 5 bits | 5 bits | 16 bits | --------------------------------------------------------------------------
字段的數量及其大小根據指令的不同而不同,但這裏的要點是這些字段會佔用指令空間。就 addi
而言,在將上述清單的三個字段放入指令之後,就只剩下 16 位供您添加即時值!
那意味着 li
只能裝入 16 位即時值。您不能只通過一條指令就將一個 32 位的指針裝入 GPR。您必須使用兩條指令,首先裝入高 16 位,然後是低 16 位。那恰恰就是
@ha
(“高”)和 @l
(“低”)後綴的用途。( @ha
的“a”部分處理符號擴展。)爲方便起見,
lis
(表示“裝入即時移位” )將直接裝入到 GPR 的高 16 位。然後餘下的所有操作是添加較低位。
每當您裝入一個絕對地址(或任何 32 位即時值)時,請務必使用這個訣竅。在引用全局地址時它是最常用的。
清單 4. Hello World — PPC64 彙編
清單 4 與上面的 32 位 PowerPC 示例(清單 2)幾乎相同。PowerPC 被設計成帶 32 位實現的 64 位規範,不僅如此,PowerPC 用戶級程序在那些實現上或多或少都與二進制兼容。在 Linux 下,ppc32 二進制在 64 位硬件上可以完美地運行(在各處做少許更改以使 32 位的用戶區和 64 位內核都能看到變量類型)。
清單 4. PPC64 彙編( 下載此代碼樣本)
.data # section declaration - variables only msg: .string "Hello, world!\n" len = . - msg # length of our dear string .text # section declaration - begin code .global _start .section ".opd","aw" .align 3 _start: .quad ._start,.TOC.@tocbase,0 .previous .global ._start ._start: # write our string to stdout li 0,4 # syscall number (sys_write) li 3,1 # first argument: file descriptor (stdout) # second argument: pointer to message to write # load the address of 'msg': # load high word into the low word of r4: lis 4,msg@highest # load msg bits 48-63 into r4 bits 16-31 ori 4,4,msg@higher # load msg bits 32-47 into r4 bits 0-15 rldicr 4,4,32,31 # rotate r4's low word into r4's high word # load low word into the low word of r4: oris 4,4,msg@h # load msg bits 16-31 into r4 bits 16-31 ori 4,4,msg@l # load msg bits 0-15 into r4 bits 0-15 # done loading the address of 'msg' li 5,len # third argument: message length sc # call kernel # and exit li 0,1 # syscall number (sys_exit) li 3,1 # first argument: exit code sc # call kernel
ppc32 代碼(清單 2)和 ppc64 代碼(清單 4)之間只有兩個區別。第一個是我們裝入指針的方法,第二個是那些有關 .opd section 的彙編程序僞指令。當將 ppc32 代碼編譯成 ppc32 二進制時,它在 ppc64 Linux 下工作得相當完美。
裝入指針
在 ppc32 上,將 32 位即時值裝入寄存器需要兩條指令。在 ppc64 上,需要 5 條!爲什麼?
我們還是使用 32 位固定長度的指令,它一次只能裝入 16 位即時值。這時,您至少需要四條指令(64 位/每條指令 16 位 = 4 條指令)。但沒有指令能直接裝入到 64 位 GPR 的高位字。所以我們必須先裝載低位字,將它移到高位字,然後再次裝入低位字。
旋轉指令(象這裏看到的 rlicr
)是臭名昭著地複雜,並被開玩笑地稱爲圖靈完成(Turing-complete)。如果您所需的全部就是裝入 64 位即時值,那麼不必擔心 — 只要將這五條指令轉換成宏,就不必再考慮這些指令了。
最後一個注意點:我們在這裏使用了 @h
來替代 ppc32 示例中的 @ha
,因爲我們後面提供低 16 位時使用了
ori
,而不是 addi
。在 RISC 機器上,經常可能用許多不同的方法來完成某項任務(例如,對於
nop
,就有許多可能的方法)。
函數描述符 — .opd 節
在 ppc64 Linux 下,當您定義並調用 C 函數 foo
時,那實際上不是該函數代碼的地址。在彙編中,如果您嘗試使用
bl foo
,那麼您很快會發現您的程序崩潰了。標號 foo
確實是 foo 函數描述符的地址。ppc64 ELF ABI(請參閱
參考資料)中詳細描述了函數描述符,但是如果從 C 代碼調用您的彙編,則您必須臨時使用一個函數描述符(它只是包含 3 個指針的結構),因爲編譯器希望使用它。
我們這裏沒有包含任何 C 代碼,但是 ELF ABI 還是顯示 ELF 文件的入口點(缺省情況下是 _start
)指向一個函數描述符。所以我們必須使用一個函數描述符,並且它應該在 .opd 節中。
那些彙編程序僞指令幾乎都從 gcc -S
的輸出中直接複製而來。這是您彙編代碼中用於預處理器宏的另一個極佳候選指令。
到哪裏瞭解更多
對於那些有興趣學習更多有關 PowerPC 的讀者而言,可以通過使用 gcc -S
(假如您手邊有 PowerPC 機器)編譯小型程序作爲開始。如果您手邊沒有 PowerPC 機器,則請查閱參考資料一節中列出的 PPC 交叉編譯 mini-howto,以及其它站點和文檔。還請嘗試使用 gdb 的 psim(PowerPC 模擬器)目標進行實驗。它比你想象的要容易!也希望您能從中獲得樂趣。
參考資料
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文.
- 下載本文中列出的 Hello World 代碼樣本:
- 在 The PowerPC Architecture: A Specification for a New Family of RISCProcessors, 2nd. Ed(Morgan Kaufmann,1994 年 5 月,ISBN 1-55860-316-6)以及 PowerPC Microprocessor Family: The Programming Environments for32-bit Microprocessors(IBM,2000 年 2 月)中獲取彙編指令的詳細信息。
- 有關彙編的概念, “Linux Assembly HOWTO”是開始學習的一本很好教程。(不幸的是,它幾乎不是真正的彙編,所以我不能將之推薦爲“非常棒”。)
- 在 Linux for PowerPC Embedded Systems HOWTO中學習嵌入式彙編。
- 還請參閱 Cross Development mini-howto for PPC Linux。(不要驚慌 — 它比您想象的要容易!)
- GNU gcc 3.1 包含 Altivec支持。
- 在 64 位 PowerPC ELF ABI中瞭解有關函數描述符的更多信息。
- 有關 IBM PowerPC 應用程序、特性彙總、技術文檔、新聞以及更多內容,請訪問 IBM PowerPC 網站。
- Aprogrammer's view of performance monitoring in the PowerPCmicroprocessor( IBM Systems Journal,1997)顯示了通過使用 Power PC 芯片上的性能監視器(PM),您可以如何分析處理器、軟件以及系統屬性的各種工作負載。
- Adecompression core for PowerPC( IBM Systems Journal,1998)向您顯示瞭如何提高 PowerPC 代碼的大小效率。
- 在“ Inline assembly for x86 in Linux”( developerWorks,2001 年 3 月)中瞭解 Linux 中內嵌彙編代碼的基本知識和用法。
- 要獲取在 Linux 上進行嵌入式開發的概述,請參閱“ Linux system development on an embedded device”( developerWorks,2002 年 3 月)。
- 在 developerWorksLinux 專區查找 更多 Linux 文章。
http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/#resources