PowerPC汇编指令
高級編程與低級編程的對比
大多數編程語言都與處理器保持著相當程度的獨立性。但都有一些特殊特性依賴于處理器的某些功能,它們更有可能是特定于操作系統的,而不是特定于處理器的。構建高級編程語言的目的是在程序員和硬件體系結構間搭建起一座橋梁。這樣做有多方面的原因。盡管可移植性是原因之一,但更重要的一點或許是提供一種更友好的模型,這種模型的建立方式更接近程序員的思考方式,而不是芯片的連線方式。
然而,在匯編語言編程中,您要直接應對處理器的指令集。這意味著您看系統的方式與硬件相同。這也有可能使匯編語言編程變得更為困難,因為編程模型的建立更傾向于使硬件工作,而不是密切反映問題域。這樣做的好處在于您可以更輕松地完成系統級任務、執行那些與處理器相關性很強的優化任務。而缺點是您必須在那個級別上進行思考,依賴于一種特定的處理器系列,往往還必須完成許多額外的工作以準確地建模問題域。
關于匯編語言,很多人未想到的一個好處就是它非常具體。在高級語言中,對每個表達式都要進行許多處理。您有時不得不擔憂幕后到底發生了哪些事情。在匯編語言編程中,您可以完全精確地掌控硬件的行為。您可以逐步處理硬件級更改。
匯編語言基礎
在了解指令集本身之前,有兩項關于匯編語言的關鍵內容需要理解,也就是內存模型和獲取-執行周期。
內存模型非常簡單。內存只存儲一種東西 —— 固定范圍內的數字,也稱為字節(在大多數計算機上,這是一個 0 到 255 之間的數字)。每個存儲單元都使用一個有序地址定位。設想一個龐大的空間,其中有許多信箱。每個信箱都有編號,且大小相同。這是計算機能夠存儲的惟一 內容。因此,所有一切最終都必須存儲為固定范圍內的數字。幸運的是,大多數處理器都能夠將多個字節結合成一個單元來處理大數和具有不同取值范圍的數字(例如浮點數)。但特定指令處理一塊內存的方式與這樣一個事實無關:每個存儲單元都以完全相同的方式存儲。除了內存按有序地址定位之外,處理器還維護著一組寄存器,這是容納被操縱的數據或配置開關的臨時位置。
控制處理器的基本過程就上獲取-執行周期。處理器有一個稱為程序計數器的寄存器,容納要執行的下一條指令的地址。獲取-執行的工作方式如下:
讀程序計數器,從其中列出的地址處讀取指令
更新程序計數器,使之指向下一條指令
解碼指令
加載處理該指令所需的全部內存項
處理計算
儲存結果
完成這一切的實際原理極其復雜,特別是 POWER5 處理器可同步處理多達 5 條的指令。但上述介紹對于構思模型來說已足夠。
PowerPC 體系結構按特征可表述為加載/存儲 體系結構。這也就意味著,所有的計算都是在寄存器中完成的,而不是主存儲器中。在將數據載入寄存器以及將寄存器中的數據存入內存時的內存訪問非常簡單。這與 x86 體系結構(比如說)不同,其中幾乎每條指令都可對內存、寄存器或兩者同時進行操作。加載/存儲體系結構通常具有許多通用的寄存器。PowerPC 具有 32 個通用寄存器和 32 個浮點寄存器,每個寄存器都有編號(與 x86 完全不同,x86 為寄存器命名而不是編號)。操作系統的 ABI(應用程序二進制接口)可能主要使用通用寄存器。還有一些專用寄存器用于容納狀態信息并返回地址。管理級應用程序還可使用其他一些專用寄存器,但這些內容不在本文討論之列。通用寄存器在 32 位體系結構中是 32 位的,在 64 位體系結構中則是 64 位的。本文主要關注 64 位體系結構。
匯編語言中的指令非常低級 —— 它們一次只能執行一項(有時可能是為數不多的幾項)操作。例如,在 C 語言中可以寫 d = a + b + c - d + some_function(e, f - g),但在匯編語言中,每一次加、減和函數調用操作都必須使用自己的指令,實際上函數調用可能需要使用幾條指令。有時這看上去冗長麻煩。但有三個重要的優點。第一,簡單了解匯編語言能夠幫助您編寫出更好的高級代碼,因為這樣您就可以了解較低的級別上究竟發生了什么。第二,能夠處理匯編語言中的所有細節這一事實意味著您能夠優化速度關鍵型循環,而且比編譯器做得更出色。編譯器十分擅長代碼優化。但了解匯編語言可幫助您理解編譯器進行的優化(在 gcc 中使用 -S 開關將使編譯器生成匯編代碼而不是對象代碼),并且還能幫您找到編譯器遺漏的地方。第三,您能夠充分利用 PowerPC 芯片的強大力量,實際上這往往會使您的代碼比高級語言中的代碼更為簡潔。
這里不再進一步解釋,接下來讓我們開始研究 PowerPC 指令集。下面給出了一些對新手很有幫助的 PowerPC 指令:
li REG, VALUE
加載寄存器 REG,數字為 VALUE
add REGA, REGB, REGC
將 REGB 與 REGC 相加,并將結果存儲在 REGA 中
addi REGA, REGB, VALUE
將數字 VALUE 與 REGB 相加,并將結果存儲在 REGA 中
mr REGA, REGB
將 REGB 中的值復制到 REGA 中
or REGA, REGB, REGC
對 REGB 和 REGC 執行邏輯 “或” 運算,并將結果存儲在 REGA 中
ori REGA, REGB, VALUE
對 REGB 和 VALUE 執行邏輯 “或” 運算,并將結果存儲在 REGA 中
and, andi, xor, xori, nand, nand, and nor
其他所有此類邏輯運算都遵循與 “or” 或 “ori” 相同的模式
ld REGA, 0(REGB)
使用 REGB 的內容作為要載入 REGA 的值的內存地址
lbz, lhz, and lwz
它們均采用相同的格式,但分別操作字節、半字和字(“z” 表示它們還會清除該寄存器中的其他內容)
b ADDRESS
跳轉(或轉移)到地址 ADDRESS 處的指令
bl ADDRESS
對地址 ADDRESS 的子例程調用
cmpd REGA, REGB
比較 REGA 和 REGB 的內容,并恰當地設置狀態寄存器的各位
beq ADDRESS
若之前比較過的寄存器內容等同,則跳轉到 ADDRESS
bne, blt, bgt, ble, and bge
它們均采用相同的形式,但分別檢查不等、小于、大于、小于等于和大于等于
std REGA, 0(REGB)
使用 REGB 的地址作為保存 REGA 的值的內存地址
stb, sth, and stw
它們均采用相同的格式,但分別操作字節、半字和字
sc
對內核進行系統調用
注意到,所有計算值的指令均以第一個操作數作為目標寄存器。在所有這些指令中,寄存器都僅用數字指定。例如,將數字 12 載入寄存器 5 的指令是 li 5, 12。我們知道,5 表示一個寄存器,12 表示數字 12,原因在于指令格式 —— 沒有其他指示符。
每條 PowerPC 指令的長度都是 32 位。前 6 位確定具體指令,其他各位根據指令的不同而具有不同功能。指令長度固定這一事實使處理器更夠更有效地處理指令。但 32 位這一限制可能會帶來一些麻煩,后文中您將會看到。大多數此類麻煩的解決方法將在本系列的第 2 部分中討論。
上述指令中有許多都利用了 PowerPC 的擴展記憶法。也就是說,它們實際上是一條更為通用的指令的特殊形式。例如,上述所有條件跳轉指令實際上都是 bc(branch conditional)指令的特殊形式。bc 指令的形式是 bc MODE, CBIT, ADDRESS。CBIT 是條件寄存器要測試的位。MODE 有許多有趣的用途,但為簡化使用,若您希望在條件位得到設置時跳轉,則將其設置為 12;若希望在條件位未得到設置時跳轉,則將其設置為 4。部分重要的條件寄存器位包括:表示小于的 8、表示大于的 9、表示相等的 10。因此,指令 beq ADDRESS 實際上就是 bc 12, 10 ADDRESS。類似地,li 是 addi 的特殊形式,mr 是 or 的特殊形式。這些擴展的記憶法有助于使 PowerPC 匯編語言程序更具可讀性,并且能夠編寫出更簡單的程序,同時也不會抵消更高級的程序和程序員可以利用的強大能力。
您的第一個 POWER5 程序
現在我們來看實際代碼。我們編寫的第一個程序僅僅載入兩個值、將其相加并退出,將結果作為狀態代碼,除此之外沒有其他功能。將一個文件命名為 sum.s,在其中輸入如下代碼:
清單 1. 您的第一個 POWER5 程序
#Data sections holds writable memory declarations
.data
.align 3 #align to 8-byte boundary
#This is where we will load our first value from
first_value:
??????? #"quad" actually emits 8-byte entities
??????? .quad 1
second_value:
??????? .quad 2
#Write the "official procedure descriptor" in its own section
.section ".opd","aw"
.align 3 #align to 8-byte boundary
#procedure description for ._start
.global _start
#Note that the description is named _start,
# and the beginning of the code is labeled ._start
_start:
??????? .quad ._start, .TOC.@tocbase, 0
#Switch to ".text" section for program code
.text
._start:
??????? #Use register 7 to load in an address
??????? #64-bit addresses must be loaded in 16-bit pieces
??????? #Load in the high-order pieces of the address
??????? lis 7, first_value@highest
??????? ori?? 7, 7, first_value@higher
??????? #Shift these up to the high-order bits
??????? rldicr 7, 7, 32, 31
??????? #Load in the low-order pieces of the address
??????? oris 7, 7, first_value@h
??????? ori 7, 7, first_value@l
??????? #Load in first value to register 4, from the address we just loaded
??????? ld 4, 0(7)
??????? #Load in the address of the second value
??????? lis 7, second_value@highest
??????? ori 7, 7, second_value@higher
??????? rldicr 7, 7, 32, 31
??????? oris 7, 7, second_value@h
??????? ori 7, 7, second_value@l
??????? #Load in the second value to register 5, from the address we just loaded
??????? ld 5, 0(7)
??????? #Calculate the value and store into register 6
??????? add 6, 4, 5
??????? #Exit with the status
??????? li 0, 1??? #system call is in register 0
??????? mr 3, 6??? #Move result into register 3 for the system call
??????? sc
討論程序本身之前,先構建并運行它。構建此程序的第一步是匯編 它:
as -m64 sum.s -o sum.o
這會生成一個名為 sum.o 的文件,其中包含對象代碼,這是匯編代碼的機器語言版,還為連接器增加了一些附加信息。“-m64” 開關告訴匯編程序您正在使用 64 位 ABI 和 64 位指令。所生成的對象代碼是此代碼的機器語言形式,但無法直接按原樣運行,還需要進行連接,之后操作系統才能加載并運行它。連接的方法如下:
ld -melf64ppc sum.o -o sum
這將生成可執行的 sum。要運行此程序,按如下方法操作:
./sum
echo $?
這將輸入 “3”,也就是最終結果。現在我們來看看這段代碼的實際工作方式。
由于匯編語言代碼的工作方式非常接近操作系統的級別,因此組織方式與它將生成的對象和可執行文件也很相近。那么,為了理解代碼,我們首先需要理解對象文件。
對象和可執行文件劃分為 “節”。程序執行時,每一節都會載入地址空間內的不同位置。它們都具有不同的保護和目的。我們需要關注的主要幾節包括:
.data
包含用于該程序的預初始化數據
.text
包含實際代碼(過去稱為程序文本)
.opd
包含 “正式過程聲明”,它用于輔助連接函數和指定程序的入口點(入口點就是要執行的代碼中的第一條指令)
我們的程序做的第一件事就是切換到 .data 節,并將對齊量設置為 8 字節的邊界(.align 3 會將匯編程序的內部地址計數器對齊為 2^3 的倍數)。
first_value: 這一行是一個符號聲明。它將創建一個稱為 first_value 的符號,與匯編程序中列出的下一條聲明或指令的地址同義。請注意,first_value 本身是一個常量 而不是變量,盡管它所引用的存儲地址可能是可更新的。first_value 只是引用內存中特定地址的一種簡化方法。
下一條偽指令 .quad 1 創建一個 8 字節的數據值,容納值 1。
之后,我們使用類似的一組偽指令定義地址 second_value,容納 8 字節數據項,值為 2。
.section ".opd", "aw" 為我們的過程描述符創建一個 “.opd” 節。強制這一節對齊到 8 字節邊界。然后將符號 _start 聲明為全局符號,也就是說它在連接后不會被丟棄。然后聲明 _start 腹稿本身( .globl 匯編程序未定義 _start,它只是使其在定義后成為全局符號)。接下來生成的三個數據項是過程描述符,本系列后續文章中將討論相關內容。
現在轉到實際程序代碼。.text 偽指令告訴匯編程序我們將切換到 “text” 一節。之后就是 ._start 的定義。
第一組指令載入第一個值的地址,而非值本身。由于 PowerPC 指令僅有 32 位長,指令內僅有 16 位可用于加載常量值(切記,address of first_value 是常量)。由于地址最多可達到 64 位,因此我們必須采用每次一段的方式載入地址(本系列的第 2 部分將介紹如何避免這樣做)。匯編程序中的 @ 符號指示匯編程序給出一個符號值的特殊處理形式。這里使用了以下幾項:
@highest
表示一個常量的第 48-63 位
@higher
表示一個常量的第 32-47 位
@h
表示一個常量的第 16-31 位
@l
表示一個常量的第 0-15 位
所用的第一條指令表示 “載入即時移位(load immediate shifted)”。這會在最右端(first_value 的第 48-63 位)載入值,將數字移位到左邊的 16 位,然后將結果存儲到寄存器 7 中。寄存器 7 的第 16-31 位現包含地址的第 48-63 位。接下來我們使用 “or immediate” 指令對寄存器 7 和右端的值(first_value 的第 32-47 位)執行邏輯或運算,將結果存儲到寄存器 7 中。現在地址的第 32-47 位存儲到了寄存器的第 0-15 位中。寄存器 7 現左移 32 位,0-31 位將清空,結果存儲在寄存器 7 中。現在寄存器 7 的第 32-63 位包含我們所載入的地址的第 32-63 位。下兩條指令使用了 “or immediate” 和 “or immediate shifted” 指令,以類似的方式載入第 0-31 位。
僅僅是要載入一個 64 位值就要做許多工作。這也就是為什么 PowerPC 芯片上的大多數操作都通過寄存器完成,而不通過立即值 —— 寄存器操作可一次使用全部 64 位,而不僅限于指令的長度。下一期文章將介紹簡化這一任務的尋址模式。
現在只要記住,這只會載入我們想載入的值的地址。現在我們希望將值本身載入寄存器。為此,將使用寄存器 7 去告訴處理器希望從哪個地址處載入值。在圓括號中填入 “7” 即可指出這一點。指令 ld 4, 0(7) 將寄存器 7 中地址處的值載入寄存器 4(0 表示向該地址加零)。現在寄存器 4 是第一個值。
使用類似的過程將第二個值載入寄存器 5。
加載寄存器之后,即可將數字相加了。指令 add 6, 4, 5 將寄存器 4 的內容與寄存器 5 的內容相加,并將結果存儲在寄存器 6(寄存器 4 和寄存器 5 不受影響)。
既然已經計算出了所需值,接下來就要將這個值作為程序的返回/退出值了。在匯編語言中退出一個程序的方法就是發起一次系統調用(使用 exit 系統調用退出)。每個系統調用都有一個相關聯的數字。這個數字會在實現調用前存儲在寄存器 0 中。從寄存器 3 開始存儲其余參數,系統調用需要多少參數就使用多少寄存器。然后 sc 指令使內核接收并響應請求。exit 的系統調用號是 1。因此,我們需要首先將數字 1 移動到寄存器 0 中。
在 PowerPC 機器上,這是通過加法完成的。addi 指令將一個寄存器與一個數字相加,并將結果存儲在一個寄存器中。在某些指令中(包括 addi),如果指定的寄存器是寄存器 0,則根本不會加上寄存器,而是使用數字 0。這看上去有些令人糊涂,但這樣做的原因在于使 PowerPC 能夠為相加和加載使用相同的指令。
退出系統調用接收一個參數 —— 退出值。它存儲在寄存器 3 中。因此,我們需要將我們的應答從寄存器 6 移動到寄存器 3 中。“register move” 指令 rm 3, 6 執行所需的移動操作。現在我們就可以告訴操作系統已經準備好接受它的處理了。
調用操作系統的指令就是 sc,表示 “system call”。這將調用操作系統,操作系統將讀取我們置于寄存器 0 和寄存器 3 中的內容,然后退出,以寄存器 3 的內容作為返回值。在命令行中可使用命令 echo $? 檢索該值。
需要指出,這些指令中許多都是多余的,目的僅在于教學。例如,first_value 和 second_value 實際上是常量,因此我們完全可以直接載入它們,跳過數據節。同樣,我們也能一開始就將結果存儲在寄存器 3 中(而不是寄存器 6),這樣就可以免除一次寄存器移動操作。實際上,可以將寄存器同時 作為源寄存器和目標寄存器。所以,如果想使其盡可能地簡潔,可將其寫為如下形式:
清單 2. 第一個程序的簡化版本
.section ".opd", "aw"
.align 3
.global _start
_start:
.quad ._start, .TOC.@tocbase, 0
.text
li 3, 1?? #load "1" into register 3
li 4, 2?? #load "2" into register 4
add 3, 3, 4??? #add register 3 to register 4 and store the result in register 3
li 0, 1?? #load "1" into register 0 for the system call
sc
回頁首
查找最大值
我們的下一個程序將提供更多一點的功能 —— 查找一組值中的最大值,退出并返回結果。
在名為 max.s 的文件中鍵入如下代碼:
清單 3. 查找最大值
###PROGRAM DATA###
.data
.align 3
#value_list is the address of the beginning of the list
value_list:
??????? .quad 23, 50, 95, 96, 37, 85
#value_list_end is the address immediately after the list
value_list_end:
###STANDARD ENTRY POINT DECLARATION###
.section "opd", "aw"
.global _start
.align 3
_start:
??????? .quad ._start, .TOC.@tocbase, 0
###ACTUAL CODE###
.text
._start:
??????? #REGISTER USE DOCUMENTATION
??????? #register 3 -- current maximum
??????? #register 4 -- current value address
??????? #register 5 -- stop value address
??????? #register 6 -- current value
??????? #load the address of value_list into register 4
??????? lis 4, value_list@highest
??????? ori 4, 4, value_list@higher
??????? rldicr 4, 4, 32, 31
??????? oris 4, 4, value_list@h
??????? ori 4, 4, value_list@l
??????? #load the address of value_list_end into register 5
??????? lis 5, value_list_end@highest
??????? ori 5, 5, value_list_end@higher
??????? rldicr 5, 5, 32, 31
??????? oris 5, 5, value_list_end@h
??????? ori 5, 5, value_list_end@l
??????? #initialize register 3 to 0
??????? li 3, 0
??????? #MAIN LOOP
loop:
??????? #compare register 4 to 5
??????? cmpd 4, 5
??????? #if equal branch to end
??????? beq end
??????? #load the next value
??????? ld 6, 0(4)
??????? #compare register 6 (current value) to register 3 (current maximum)
??????? cmpd 6, 3
??????? #if reg. 6 is not greater than reg. 3 then branch to loop_end
??????? ble loop_end
??????? #otherwise, move register 6 (current) to register 3 (current max)
??????? mr 3, 6
loop_end:
??????? #advance pointer to next value (advances by 8-bytes)
??????? addi 4, 4, 8
??????? #go back to beginning of loop
??????? b loop
end:
??????? #set the system call number
??????? li 0, 1
??????? #register 3 already has the value to exit with
??????? #signal the system call
??????? sc
為匯編、連接和運行程序,執行:
as -a64 max.s -o max.o
ld -melf64ppc max.o -o max
./max
echo $?
您之前已體驗了一個 PowerPC 程序,也了解了一些指令,那么應該可以看懂部分代碼。數據節與上一個程序基本相同,差別只是在 value_list 聲明后有幾個值。注意,這不會改變 value_list —— 它依然是指向緊接其后的第一個數據項地址的常量。對于之后的數據,每個值使用 64 位(通過 .quad 表示)。入口點聲明與前一程序相同。
對于程序本身,需要注意的一點就是我們記錄了各寄存器的用途。這一實踐將很好地幫助您跟蹤代碼。寄存器 3 存儲當前最大值,初始設置為 0。寄存器 4 包含要載入的下個值的地址。最初是 value_list,每次遍歷前進 8 位。寄存器 5 包含緊接 value_list 中數據之后的地址。這使您可以輕松比較寄存器 4 和寄存器 5,以便了解是否到達了列表末端,并了解何時需要跳轉到 end。寄存器 6 包含從寄存器 4 指向的位置處載入的當前值。每次遍歷時,它都會與寄存器 3(當前最大值)比較,如果寄存器 6 較大,則用它取代寄存器 3。
注意,我們為每個跳轉點標記了其自己的符號化標簽,這使我們能夠將這些標簽作為跳轉指令的目標。例如,beq end 跳轉到這段代碼中緊接 end 符號定義之后的代碼處。
要注意的另外一條指令是 ld 6, 0(4)。它使用寄存器 4 中的內容作為存儲地址來檢索一個值,此值隨后存儲到寄存器 6 中。
總結
以上是生活随笔為你收集整理的PowerPC汇编指令的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 嵌入式xworks系统初始化(Power
- 下一篇: 地址映射原理和实现