NO IMAGE

目前組合語言在程式設計領域並未廣為人知,而 PowerPC 彙編更是異乎尋常的陌生。Hollis Blanchard 從 PowerPC 的角度對組合語言作了概述並對比了三種體系結構 ia32、ppc 和 ppc64 的示例。

通常,高階語言都具有向程式設計師隱藏許多普通的和重複性細節這一非常好的優點,這樣程式設計師就可以專注於他們的目標。然而,有時程式設計師必須使用較低階語言,例如當編寫直接處理硬體的程式碼或編寫對效能極其敏感的程式碼的時候。組合語言是最接近硬體的程式語言,這就很自然使它成為上述那些情況下最終使用的一種語言。

本文假設您對計算機設計(例如,您應該知道處理器中有暫存器並能訪問記憶體)和作業系統(系統呼叫、異常和程序堆疊)有基本瞭解。本文對於不熟悉彙編的 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 開始。
  • 暫存器 gpr3gpr12 是易失型的(呼叫者儲存)暫存器,如果需要的話,在呼叫子例程之前必須先儲存它們並在返回之後恢復它們。
  • 暫存器 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.sgas 是 GNU 彙編程式)。在您喜愛的編輯器中開啟 file.s ,您就會看見 C 程式碼的彙編輸出。

您可能會看到您不理解的指令。可以在 The PowerPC Architecture: A Specification for a New Family of RISC Processors, 2nd. Ed以及 PowerPC Microprocessor Family: The Programming Environments for 32-bit Microprocessors(請參閱 參考資料獲取這些文件的連結)中進行查閱。不過,就象學習任何(口語)語言一樣,某些單詞很重要,您應該知道這些單詞,而其它的可以被安全地忽略,直到您弄清了程式碼更為重要的特性。一個重要指令的典型示例就是分支系列的指令,例如 blr

彙編示例

Hello World — ia32 彙編

清單 1 直接複製自 Assembly HOWTO 中的 gas 示例,糟糕的是它完全特定於 ia32。它進行兩個直接的系統呼叫:第一個寫到標準輸出;第二個退出應用程式(包含返回程式碼 0 )。直接進行系統呼叫非常少見;一般情況下,應用程式與一個封裝所有系統呼叫的 libc 庫相連。

.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 彙編程式碼。

.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,[email protected]    # load top 16 bits of &msg
addi    4,4,[email protected]   # 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 位的整數。該整數被分成大小不同的欄位:

--------------------------------------------------------------------------

|    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 位核心都能看到變數型別)。

.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,[email protected],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,[email protected]   # load msg bits 48-63 into r4 bits 16-31
ori 4,4,[email protected]  # 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,[email protected]   # load msg bits 16-31 into r4 bits 16-31
ori     4,4,[email protected]   # 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 模擬器)目標進行實驗。它比你想象的要容易!也希望您能從中獲得樂趣。

 

本文摘抄自

Hollis Blanchard (mailto:[email protected]?subject=PowerPC 彙編 — PowerPC 彙編簡介), 軟體開發人員, IBM