與 位置無(wú)關(guān)的代碼是那些不管加載到哪部分內存中都能正常工作的代碼。為什么我們需要與位置無(wú)關(guān)的代碼呢?與位置無(wú)關(guān)的代碼可以讓庫加載到地址空間中的任意位 置處。這就是允許庫隨機組合 —— 因為它們都沒(méi)有被綁定到特定位置,所以就可以使用任意庫來(lái)加載,而不用擔心地址空間沖突的問(wèn)題。鏈接器會(huì )負責確保每個(gè)庫都被加載到自己的地址空間中。通過(guò) 使用與位置無(wú)關(guān)的代碼,庫就不用擔心自己到底被加載到什么地方去了。
不過(guò),最終與位置無(wú)關(guān)的代碼需要有一種方法來(lái)定位全局變量。它可以通過(guò)維護一個(gè)全局偏移量表 來(lái)實(shí)現這種功能,這個(gè)表提供了函數或一組函數(在大部分情況中甚至是整個(gè)程序)訪(fǎng)問(wèn)的所有全局內容的地址。系統保留了一個(gè)寄存器來(lái)存放指向這個(gè)表的指針。 然后,所有訪(fǎng)問(wèn)都可以通過(guò)這個(gè)表中的一個(gè)偏移量來(lái)完成。偏移量是個(gè)常量。表本身是通過(guò)程序鏈接器/加載器來(lái)設置的,它還會(huì )初始化寄存器 2 來(lái)存放全局偏移量表的指針。使用這種方法,鏈接器/加載器就可以將認為適當的程序和數據放在一起,這只需要設置包含所有全局指針的一個(gè)全局偏移量表即可。
很容易陷于對這些問(wèn)題的討論細節當中。下面讓我們來(lái)看一些代碼,并分析一下這種方法的每個(gè)步驟都在做些什么。這是 上一篇文章 中使用的 “加法” 程序,不過(guò)現在調整成了與位置無(wú)關(guān)的代碼。
清單 4. 通過(guò)全局偏移量表來(lái)訪(fǎng)問(wèn)數據
###DATA DEFINITIONS### .data .align 3 first_value: .quad 1 second_value: .quad 2 ###ENTRY POINT DECLARATION### .section .opd, "aw" .align 3 .globl _start _start: .quad ._start, .TOC.@tocbase, 0 ###CODE### .text ._start: ##Load values## #Load the address of first_value into register 7 from the global offset table ld 7, first_value@got(2) #Use the address to load the value of first_value into register 4 ld 4, 0(7) #Load the address of second_value into register 7 from the global offset table ld 7, second_value@got(2) #Use the address to load the value of second_value into register 5 ld 5, 0(7) ##Perform addition## add 3, 4, 5 ##Exit with status## li 0, 1 sc |
要匯編、連接并運行這段代碼,請按以下方法執行:
#Assemble as -a64 addnumbers.s -o addnumbers.o #Link ld -melf64ppc addnumbers.o -o addnumbers #Run ./addnumbers #View the result code (value returned from the program) echo $? |
數據定義和入口點(diǎn)聲明與之前的例子相同。不過(guò),我們不用再使用 5 條指令將 first_value 的地址加載到寄存器 7 中了,現在只需要一條指令就可以了:ld 7, first_value@got(2)。正如前面介紹的一樣,連接器/加載器會(huì )將寄存器 2 設置為全局偏移量表的地址。語(yǔ)法 first_value@got 會(huì )請求鏈接器不要使用 first_value 的地址,而是使用全局偏移量表中包含 first_value 地址的偏移量。
使用這種方法,大部分程序員都可以包含他們在一個(gè)全局偏移量表中使用的所有全局數據。DS-Form 從一個(gè)基址可以尋址多達 64K 的內存。注意為了獲得 DS-Form 的整個(gè)范圍,寄存器 2 指向了全局偏移量表的 中部, 這樣我們就可以使用正數偏移量和負數偏移量了。由于我們正在定位的是指向數據的指針(而不是直接定位數據),因此我們可以訪(fǎng)問(wèn)大約 8,000 個(gè)全局變量(局部變量都保存在寄存器或堆棧中,這會(huì )在本系列的第三篇文章中進(jìn)行討論)。即使這還不夠,我們還有多個(gè)全局偏移量表可以使用。這種機制也會(huì )在 下一篇文章中進(jìn)行討論。
盡管這比上一篇文章中所使用的 5 條指令的數據加載更加簡(jiǎn)潔,可讀性也更好,但是我們仍然可以做得更好些。在 64 位 ELF ABI 中,全局偏移量表實(shí)際上是一個(gè)更大的部分 —— 稱(chēng)為內容表(table of contents) —— 的一個(gè)子集。除了創(chuàng )建全局偏移量表入口之外,內容表還包含變量,它沒(méi)有包含全局數據的 地址,而是包含的數據本身。這些變量的大小和個(gè)數必須很小,因為內容表只有 64K。
要聲明一個(gè)內容表的數據項,我們需要切換到 .toc 段,并顯式地進(jìn)行聲明,如下所示:
.section .toc name: .tc unused_name[TC], initial_value |
這會(huì )創(chuàng )建一個(gè)內容表入口。name 是在代碼中引用它所使用的符號。initial_value 是初始化分配的一個(gè) 64 位的值。unused_name 是歷史記錄,現在在 ELF 系統上已經(jīng)沒(méi)有任何用處了。我們可以不再使用它了(此處包含進(jìn)來(lái)只是為了幫助我們閱讀遺留代碼),不過(guò) [TC] 是需要的。
要訪(fǎng)問(wèn)內容表中直接保存的數據,我們需要使用 @toc 來(lái)引用它,而不能使用 @got。@got 仍然可以工作,不過(guò)其功能也與以前一樣 —— 返回一個(gè)指向值的指針,而不是返回值本身。下面看一下這段代碼:
### DATA ### #Create the variable my_var in the table of contents .section .toc my_var: .tc [TC], 10 ### ENTRY POINT DECLARATION ### .section .opd, "aw" .align 3 .globl _start _start: .quad ._start, .TOC.@tocbase, 0 ### CODE ### .text ._start: #loads the number 10 (my_var contents) into register 3 ld 3, my_var@toc(2) #loads the address of my_var into register 4 ld 4, my_var@got(2) #loads the number 10 (my_var contents) into register 4 ld 3, 0(4) #load the number 15 into register 5 li 5, 15 #store 15 (register 5) into my_var via ToC std 5, my_var@toc(2) #store 15 (register 5) into my_var via GOT (offset already loaded into register 4) std 5, 0(4) #Exit with status 0 li 0, 1 li 3, 0 sc |
如您所見(jiàn),如果查看在 .toc 段中所定義的符號(而不是大部分數據所在的 .data 段),使用 @toc 可以提供直接到值本身的偏移量,而使用 @got 只能提供一個(gè)該值地址的偏移量。
現在看一下使用 Toc 中的值來(lái)進(jìn)行加法計算的例子:
### PROGRAM DATA ### #Create the values in the table of contents .section .toc first_value: .tc [TC], 1 second_value: .tc [TC], 2 ### ENTRY POINT DEFINITION ### .section .opd, "aw" .align 3 .globl _start _start: .quad ._start, .TOC.@tocbase, 0 .text ._start: ##Load values from the table of contents ## ld 4, first_value@toc(2) ld 5, second_value@toc(2) ##Perform addition## add 3, 4, 5 ##Exit with status## li 0, 1 sc |
可以看到,通過(guò)使用基于 .toc 的數據,我們可以顯著(zhù)減少代碼所使用的指令數量。另外,由于這個(gè)內容表通常就在緩存中,它還可以顯著(zhù)減少內存的延時(shí)。我們只需要謹慎處理存儲的數據量就可以了。
|
PowerPC 還可以在一條指令中執行多個(gè)加載和存儲操作。不幸的是,這限定于字大?。?span lang="EN-US" style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">32 位)的數據。這些都是非常簡(jiǎn)單的 D-Form 指令。我們指定了基址寄存器、偏移量和起始目標寄存器。處理器然后會(huì )將數據加載到通過(guò)寄存器 31 所列出的目標寄存器開(kāi)始的所有寄存器中,這會(huì )從指令所指定的地址開(kāi)始,一直往前進(jìn)行。此類(lèi)指令包括 lmw (加載多個(gè)字)和 stmw(存儲多個(gè)字)。下面是幾個(gè)例子:
#Starting at the address specified in register ten, load #the next 32 bytes into registers 24-31 lmw 24, 0(10) #Starting at the address specified in register 8, load #the next 8 bytes into registers 30-31 lmw 30, 0(8) #Starting at the address specified in register 5, store #the low-order 32-bits of registers 20-31 into the next #48 bytes stmw 20, 0(5) |
下面是使用多個(gè)值的加法程序:
### Data ### .data first_value: #using "long" instead of "double" because #the "multiple" instruction only operates #on 32-bits .long 1 second_value: .long 2 ### ENTRY POINT DECLARATION ### .section .opd, "aw" .align 3 .globl _start _start: .quad ._start, .TOC.@tocbase, 0 ### CODE ### .text ._start: #Load the address of our data from the GOT ld 7, first_value@got(2) #Load the values of the data into registers 30 and 31 lmw 30, 0(7) #add the values together add 3, 30, 31 #exit li 0, 1 sc |
大多數加載/存儲指令都可以使用加載/存儲指令最終使用的有效地址來(lái)更新主地址寄存器。例如,ldu 5, 4(8) 會(huì )將寄存器 8 中指定的地址加上 4 個(gè)字節加載到寄存器 5 中,然后將計算出來(lái)的地址存回 寄存器 8 中。這稱(chēng)為帶更新 的加載和存儲,這可以用來(lái)減少執行多個(gè)任務(wù)所需要的指令數。在下一篇文章中我們將更多地使用這種模式。
|
|
有 效地進(jìn)行加載和存儲對于編寫(xiě)高效代碼來(lái)說(shuō)至關(guān)重要。了解可用的指令格式和尋址模式可以幫助我們理解某種平臺的可能性和限制。PowerPC 上的 D-Form 和 DS-Form 指令格式對于與位置無(wú)關(guān)的代碼來(lái)說(shuō)非常重要。與位置無(wú)關(guān)的代碼允許我們創(chuàng )建共享庫,并使用較少的指令就可以完成加載全局地址的工作。
本系列的下一篇文章將介紹分支、函數調用以及與 C 代碼的集成問(wèn)題。
PowerPC 中的分支利用了 3 個(gè)特殊用途的寄存器:條件寄存器、計數寄存器 和鏈接寄存器。
條件寄存器從概念上來(lái)說(shuō)包含 7 個(gè)域(field)。域是一個(gè) 4 位長(cháng)的段,用來(lái)存儲指令結果狀態(tài)信息。其中有兩個(gè)域是專(zhuān)用的(稍后就會(huì )介紹),其余域是通用的。這些域的名字為 cr0 到 cr7。
第一個(gè)域 cr0 用來(lái)保存定點(diǎn)計算指令的結果,它使用了非立即操作(有幾個(gè)例外)。計算的結果會(huì )與 0 進(jìn)行比較,并根據結果設置適當的位(負數、零或正數)。要想在計算指令中設置 cr0,可以簡(jiǎn)單地在指令末尾添加一個(gè)句點(diǎn)(.)。例如,add 4, 5, 6這條指令是將寄存器 5 和寄存器 6 進(jìn)行加法操作,并將結果保存到寄存器 4 中,而不會(huì )在 cr0 中設置任何狀態(tài)位。add. 4, 5, 6 也可以進(jìn)行相同的加法操作,不過(guò)會(huì )根據所計算出來(lái)的值設置 cr0 中的位。cr0 也是比較指令上使用的默認域。
第二個(gè)域(稱(chēng)為 cr1)用于浮點(diǎn)指令,方法是在指令名后加上句點(diǎn)。浮點(diǎn)計算的內容超出了本文的討論范圍。
每個(gè)域都有 4 個(gè)位。這些位的用法根據所使用的指令的不同會(huì )有所不同。下面是可能的用法(下面也列出了浮點(diǎn)指令,不過(guò)沒(méi)有詳細介紹):
位 | 記憶法 | 定點(diǎn)比較 | 定點(diǎn)計算 | 浮點(diǎn)比較 | 浮點(diǎn)計算 |
0 | lt | 小于 | 負數 | 小于 | 異常摘要 |
1 | gt | 大于 | 正數 | 大于 | 啟用異常摘要 |
2 | eq | 等于 | 0 | 等于 | 無(wú)效操作異常摘要 |
3 | so | 摘要溢出 | 摘要溢出 | 無(wú)序 | 溢出異常 |
稍后您就會(huì )看到如何隱式或直接訪(fǎng)問(wèn)這些域。
條件寄存器可以使用 mtcr、mtcrf 和 mfcr 加載到通用寄存器中(或從通用寄存器中進(jìn)行加載)。mtcr 將一個(gè)特定的通用寄存器加載到條件寄存器中。mfcr 將條件寄存器移到通用寄存器中。mtcrf 從通用寄存器中加載條件寄存器,不過(guò)只會(huì )加載由 8 位掩碼所指定的域,即第一個(gè)操作數。
下面是幾個(gè)例子。
#Copy register 4 to the condition register mtcr 4 #Copy the condition register to register 28 mfcr 28 #Copy fields 0, 1, 2, and 7 from register 18 to the condition register mtcrf 0b11100001, 18 |
鏈接寄存器(名為 LR)是專(zhuān)用寄存器,其中保存了分支指令的返回地址。所有的分支指令都可以用來(lái)設置鏈接寄存器;如果進(jìn)行分支,就將鏈接寄存器設置成當前指令之后緊接的那條指令的地址。分支指令通過(guò)將字母 l 附加到指令末尾來(lái)設置鏈接寄存器。舉例來(lái)說(shuō),b 是無(wú)條件的分支指令,而 bl 則是設置鏈接寄存器的一條無(wú)條件分支指令。
計數寄存器(名為 CTR)是用來(lái)保存循環(huán)計數器的一個(gè)專(zhuān)用寄存器。專(zhuān)用分支指令可能會(huì )減少計數寄存器,并且(或者)會(huì )根據 CTR 是否達到 0 來(lái)進(jìn)行條件分支跳轉。
鏈接寄存器和計數寄存器都可以用作分支目的地。bctr 分支跳轉到計數寄存器中指定的地址,blr 分支跳轉到鏈接寄存器中指定的地址。
鏈接寄存器和計數寄存器的值也可以從通用寄存器中拷貝而來(lái),或者拷貝到通用寄存器中。對于鏈接寄存器來(lái)說(shuō),mtlr 會(huì )將給定的寄存器值拷貝到 鏈接寄存器中,mflr 則將值從 鏈接寄存器拷貝到通用寄存器中。mtctr 和 mfctr 也可以對計數寄存器實(shí)現相同的功能。
PowerPC 指令集中的無(wú)條件分支使用了 I-Form 指令格式:
I-Form 指令格式
0-5 位
操作碼
6-29 位
絕對或相對分支地址
30 位
絕對地址位 —— 如果這個(gè)域被置位了,那么指令就會(huì )被解釋成絕對地址,否則就被解釋成相對地址
31 位
鏈接位 —— 如果這個(gè)域被置位了,那么指令就會(huì )將鏈接寄存器設置為下一條指令的地址
正如前面介紹的一樣,將字母 l 添加到分支指令后面會(huì )導致鏈接位被置位,從而使 “返回地址”(分支跳轉后的指令)存儲在鏈接寄存器中。如果您在指令末尾再加上字母 a(位于 l 之后,如果l 也同時(shí)使用的話(huà)),那么所指定的地址就是絕對地址(通常在用戶(hù)級代碼中不會(huì )這樣用,因為這會(huì )過(guò)多地限制分支目的地)。
清單 2 闡述了無(wú)條件分支的用法,然后退出(您可以將下面的代碼輸入到 branch_example.s 文件中):
### ENTRY POINT DECLARATION ### .section .opd, "aw" .align 3 .globl _start _start: .quad ._start, .TOC.@tocbase, 0 ### PROGRAM CODE ### .text #branch to target t2 ._start: b t2 t1: #branch to target t3, setting the link register bl t3 #This is the instruction that it returns to b t4 t2: #branch to target t1 as an absolute address ba t1 t3: #branch to the address specified in the link register #(i.e. the return address) blr t4: li 0, 1 li 3, 0 sc |
對這個(gè)程序進(jìn)行匯編和鏈接,然后運行,方法如下:
as -a64 branch_example.s -o branch_example.o
ld -melf64ppc branch_example.o -o branch_example
./branch_example
請注意 b 和 ba 的目標在匯編語(yǔ)言中是以相同的方式來(lái)指定的,盡管二者在指令中的編碼方式大不相同。匯編器和鏈接器會(huì )負責為我們將目標地址轉換成相對地址或絕對地址。
cmp 指令用來(lái)將寄存器與其他寄存器或立即操作數進(jìn)行比較,并設置條件寄存器中適當狀態(tài)位。缺省情況下,定點(diǎn)比較指令使用 cr0 來(lái)存儲結果,但是這個(gè)域也可以作為一個(gè)可選的第一操作數來(lái)指定。比較指令的用法如清單 3 所示:
#Compare register 3 and register 4 as doublewords (64 bits) cmpd 3, 4 #Compare register 5 and register 10 as unsigned doublewords (64 bits) cmpld 5, 10 #Compare register 6 with the number 12 as words (32 bits) cmpwi 6, 12 #Compare register 30 and register 31 as doublewords (64 bits) #and store the result in cr4 cmpd cr4, 30, 31 |
正如您可以看到的一樣,d 指定操作數為雙字,而 w 則指定操作數為單字。i 說(shuō)明最后一個(gè)操作數是立即值,而不是寄存器,l 告訴處理器要進(jìn)行無(wú)符號(也稱(chēng)為邏輯)比較操作,而不是進(jìn)行有符號比較操作。
每條指令都會(huì )設置條件寄存器中的適當位(正如本文前面介紹的一樣),這些值然后會(huì )由條件分支指令來(lái)使用。
條件分支比無(wú)條件分支更加靈活,不過(guò)它的代價(jià)是可跳轉的距離不夠大。條件分支使用了 B-Form 指令格式:
B-Form 指令格式
0-5 位
操作碼
6-10 位
指定如何對位進(jìn)行測試、是否使用計數寄存器、如何使用計數寄存器,以及是否進(jìn)行分支預測(稱(chēng)為 BO 域)
11-15 位
指定條件寄存器中要測試的位(稱(chēng)為 BI 域)
16-29 位
絕對或相對地址
30 位
尋址模式 —— 該位設置為 0 時(shí),指定的地址就被認為是一個(gè)相對地址;當該位設置為 1 時(shí),指定的地址就被認為是一個(gè)絕對地址
31 位
鏈接位 —— 當該位設置為 1 時(shí),鏈接寄存器 被設置成當前指令的下一條指令的地址;當該位設置為 0 時(shí),鏈接寄存器沒(méi)有設置
正如您可以看到的一樣,我們可以使用完整的 10 位值來(lái)指定分支模式和條件,這會(huì )將地址大小限制為只有 14 位(范圍只有 16K)。這對于函數中的短跳轉非常有用,但是對其他跳轉指令來(lái)說(shuō)就沒(méi)多大用處了。要有條件地調用一個(gè) 16K 范圍之外的函數,代碼需要進(jìn)行一個(gè)條件分支,跳轉到一條包含無(wú)條件分支的指令,進(jìn)而跳轉到正確的位置。
條件分支的基本格式如下所示:
bc BO, BI, address
bcl BO, BI, address
bca BO, BI, address
bcla BO, BI, address
在這個(gè)基本格式中,BO 和 BI 都是數字。幸運的是,我們并不需要記住所有的數字及其意義。PowerPC 指令集的擴展記憶法(在第一篇中已經(jīng)介紹過(guò)了)在這里又可以再次派上用場(chǎng)了,這樣我們就不必非要記住所有的數字。與無(wú)條件分支類(lèi)似,在指令名后面添加一個(gè) l 就可以設置鏈接寄存器,在指令名后面添加一個(gè) a 會(huì )讓指令使用絕對尋址而不是相對尋址。
對于一個(gè)簡(jiǎn)單比較且在比較結果相等時(shí)發(fā)生跳轉的情況來(lái)說(shuō),基本格式(沒(méi)有使用擴展記憶法)如下所示:
#compare register 4 and 5 cmpd 4, 5 #branch if they are equal bc 12, 2 address |
bc 表示“條件分支(branch conditionally)”。12(BO 操作數)的意思是如果給定的條件寄存器域被置位了就跳轉,不采用分支預測,2(BI 操作數)是條件寄存器中要測試的位(是等于位)?,F在,很少有人(尤其是新手)能夠記住所有的分支編號和條件寄存器位的數字編號,這也沒(méi)太大用處。擴展記憶法可以讓代碼的閱讀、編寫(xiě)和調試變得更加清晰。
有幾種方法可以指定擴展記憶法。我們將著(zhù)重介紹指令名和指令的 BO 操作數(指定模式)的幾種組合。最簡(jiǎn)單的用法是 bt 和 bf。 如果條件寄存器中的給定位為真,bt 就會(huì )進(jìn)行分支跳轉;如果條件寄存器中給定位為假,bf 就會(huì )進(jìn)行分支跳轉。另外,條件寄存器位也可以使用這種記憶法來(lái)指定。如果您指定了 4*cr3+eq,這會(huì )測試 cr3 的位 2(之所以會(huì )用 4* 是因為每個(gè)域都是 4 位寬的)。位域中的每個(gè)位的可用記憶法已經(jīng)在前面對條件寄存器的介紹中給出了。如果您只指定了位,而沒(méi)有指定域,那么指令就會(huì )缺省為 cr0。
下面是幾個(gè)例子:
清單 5. 簡(jiǎn)單的條件分支
#Branch if the equal bit of cr0 is set bt eq, where_i_want_to_go #Branch if the equal bit of cr1 is not set bf 4*cr1+eq, where_i_want_to_go #Branch if the negative bit (mnemonic is "lt") of cr5 is set bt 4*cr5+lt, where_i_want_to_go |
另外一組擴展記憶法組合了指令、 BO 操作數和條件位(不過(guò)沒(méi)有域)。它們多少使用了“傳統”記憶方法來(lái)表示各種常見(jiàn)的條件分支。例如,bne my_destination(如果不等于 my_destination 就發(fā)生跳轉)與 bf eq, my_destination(如果 eq 位為假就跳轉到 my_destination)是等效的。要利用這種記憶法來(lái)使用不同的條件寄存器域,可以簡(jiǎn)單地在目標地址前面的操作數中指定域,例如 bne cr4, my_destination。這些分支記憶法遵循的模式是:blt(小于)、ble(小于或等于)、beq(等于)、 bge (大于或等于)、bgt(大于)、bnl(不小于)、bne(不等于)、bng(不大于)、 bso(溢出摘要)、 bns (無(wú)溢出摘要)、 bun(無(wú)序 —— 浮點(diǎn)運算專(zhuān)用) 和 bnu(非無(wú)序 —— 浮點(diǎn)運算專(zhuān)用)。
所有的記憶法和擴展記憶法可以在指令后面附加上 l 和/或 a 來(lái)分別啟用鏈接寄存器或絕對尋址。
使用擴展記憶法可以允許采用更容易讀取和編寫(xiě)的編程風(fēng)格。對于更高級的條件分支來(lái)說(shuō),擴展記憶法不僅非常有用,而且非常必要。
由于條件寄存器有多個(gè)域,不同的計算和比較可以使用不同的域,而邏輯操作可以用來(lái)將這些條件組合在一起。所有的邏輯操作都有如下格式:cr<opname> target_bit, operand_bit_1, operand_bit_2。例如,要對cr2 的 eq 位和 cr7 的 lt 位進(jìn)行一個(gè) and 邏輯操作,并將結果存儲到 cr0 的 eq 位中,就可以這樣編寫(xiě)代碼:crand 4*cr0+eq, 4*cr2+eq, 4*cr7+lt。
您可以使用 mcrf 來(lái)操作條件寄存器域。要將 cr4 拷貝到 cr1 中,可以這樣做:mcrf cr1, cr4。
分支指令也可以為分支處理器進(jìn)行的分支預測提供提示。在最常用的條件分支指令后面加上一個(gè) +,就可以向分支處理器發(fā)送一個(gè)信號,說(shuō)明可能會(huì )發(fā)生分支跳轉。在指令后面加上一個(gè) -,就可以向分支處理器發(fā)送一個(gè)信號,說(shuō)明不會(huì )發(fā)生分支跳轉。然而,這通常都是不必要的,因為 POWER5 CPU 中的分支處理器可以很好地處理分支預測。
計數寄存器是循環(huán)計數器使用的一個(gè)專(zhuān)用寄存器。條件分支的 BO 操作數(控制模式)也可以使用,用來(lái)指定如何測試條件寄存器位,減少并測試計數寄存器。下面是您可以對計數寄存器執行的兩個(gè)操作:
這些計數寄存器操作可以單獨使用,也可以與條件寄存器測試一起使用。
在擴展記憶法中,計數寄存器的語(yǔ)義可以通過(guò)在 b 后面立即添加 dz 或 dnz 來(lái)指定。任何其他條件或指令修改符也都可以添加到這后面。因此,要循環(huán) 100 次,您可以將 100 加載到計數寄存器中,并使用 bdnz 來(lái)控制循環(huán)。代碼如下所示:
#The count register has to be loaded through a general-purpose register #Load register 31 with the number 100 li 31, 100 #Move it to the count register mtctr 31 # loop_start: ###loop body goes here### #Decrement count register and branch if it becomes nonzero bdnz loop_start #Code after loop goes here |
您也可以將計數器測試與其他測試一起使用。舉例來(lái)說(shuō),循環(huán)可能需要有一個(gè)提前退出條件。下面的代碼展示了當寄存器 24 等于寄存器 28 時(shí)就會(huì )觸發(fā)的提前退出條件。
#The count register has to be loaded through a general-purpose register #Load register 31 with the number 100 li 31, 100 #Move it to the count register mtctr 31 # loop_start: ###loop body goes here### #Check for early exit condition (reg 24 == reg 28) cmpd 24, 28 #Decrement and branch if not zero, and also test for early exit condition bdnzf eq, loop_start #Code after loop goes here |
因此,我們并不需要再增加一條條件分支指令,所需要做的只是將比較指令和條件指令合并為一個(gè)循環(huán)計數器分支。
現在我們將在實(shí)踐中應用上面介紹的內容。
下面的程序利用了第一篇文章中所介紹的最大值 程序,并根據我們學(xué)習到的知識進(jìn)行了重新編寫(xiě)。該程序的第一個(gè)版本使用了一個(gè)寄存器來(lái)保存所讀取的當前地址,并通過(guò)間接尋址加載值。這個(gè)程序要做的是使用 索引間接尋址模式,使用一個(gè)寄存器作為基地址,使用另一個(gè)寄存器作為索引。另外,除了索引是從 0 開(kāi)始并簡(jiǎn)單增加之外,索引還會(huì )從尾到頭進(jìn)行計數,用來(lái)保存額外的比較指令。減量可以隱式地設置條件寄存器(這與和 0 顯式比較不同)以供條件分支指令隨后使用。下面是最大值程序的新版本(您可以將其輸入到 max_enhanced.s 文件中):
###PROGRAM DATA### .data .align 3 value_list: .quad 23, 50, 95, 96, 37, 85 value_list_end: #Compute a constant holding the size of the list .equ value_list_size, value_list_end - value_list ###ENTRY POINT DECLARATION### .section .opd, "aw" .global _start .align 3 _start: .quad ._start, .TOC.@tocbase, 0 ###CODE### ._start: .equ DATA_SIZE, 8 #REGISTER USAGE #Register 3 -- current maximum #Register 4 -- list address #Register 5 -- current index #Register 6 -- current value #Register 7 -- size of data (negative) #Load the address of the list ld 4, value_list@got(2) #Register 7 has data size (negative) li 7, -DATA_SIZE #Load the size of the list li 5, value_list_size #Set the "current maximum" to 0 li 3, 0 loop: #Decrement index to the next value; set status register (in cr0) add. 5, 5, 7 #Load value (X-Form - add register 4 + register 5 for final address) ldx 6, 4, 5 #Unsigned comparison of current value to current maximum (use cr2) cmpld cr2, 6, 3 #If the current one is greater, set it (sets the link register) btl 4*cr2+gt, set_new_maximum #Loop unless the last index decrement resulted in zero bf eq, loop #AFTER THE li 0, 1 sc set_new_maximum: mr 3, 6 blr (return using the link register) |
對這個(gè)程序進(jìn)行匯編、鏈接和執行,方法如下:
as -a64 max_enhanced.s -o max_enhanced.o ld -melf64ppc max_enhanced.o -o max_enhanced ./max_enhanced |
這個(gè)程序中的循環(huán)比第一篇文章中的循環(huán)大約會(huì )快 15%,原因有兩個(gè): (a) 主循環(huán)中減少了幾條指令,這是由于在我們減少寄存器 5 時(shí)使用了狀態(tài)寄存器來(lái)檢測列表的末尾; (b) 程序使用了不同的條件寄存器域來(lái)進(jìn)行比較(因此減量的結果可以保留下來(lái)供以后使用)。
請注意在對 set_new_maximum 的調用中使用鏈接寄存器并非十分必要。即使不使用鏈接寄存器,它也可以很好地設置返回地址。不過(guò),這個(gè)使用鏈接寄存器的例子會(huì )有助于說(shuō)明鏈接寄存器的用法。
PowerPC ABI 相當復雜,我們將在下一篇文章中繼續介紹。然而,對于那些不會(huì )調用任何其他函數并且遵循簡(jiǎn)單規則的函數來(lái)說(shuō),PowerPC ABI 提供了相當簡(jiǎn)單的函數調用機制。
為了能夠使用這個(gè)簡(jiǎn)化的 ABI,您的函數必須遵循以下規則:
當函數被調用時(shí),參數都是在寄存器中發(fā)送的,這些參數保存在寄存器 3 到寄存器 10,需要使用多少個(gè)寄存器取決于參數的個(gè)數。當函數返回時(shí),返回值必須保存到寄存器 3 中。
下面讓我們將原來(lái)的最大值程序作為一個(gè)函數進(jìn)行重寫(xiě),然后在 C 語(yǔ)言中調用這個(gè)函數。
我們應該傳遞的參數如下:指向數組的指針,這是第一個(gè)參數(寄存器 3);數組大小,這是第二個(gè)參數(寄存器 4)。之后,最大值就可以放入寄存器 3 中作為返回值。
下面就是我們將其作為函數改寫(xiě)后的程序(將其輸入到 max_function.s 文件中):
###ENTRY POINT DECLARATION### #Functions require entry point declarations as well .section .opd, "aw" .global find_maximum_value .align 3 find_maximum_value: .quad .find_maximum_value, .TOC.@tocbase, 0 ###CODE### .text .align 3 #size of array members .equ DATA_SIZE, 8 #function begin .find_maximum_value: #REGISTER USAGE #Register 3 -- list address #Register 4 -- list size (elements) #Register 5 -- current index in bytes (starts as list size in bytes) #Register 6 -- current value #Register 7 -- current maximum #Register 8 -- size of data #Register 3 and 4 are already loaded -- passed in from calling function li 8, -DATA_SIZE #Extend the number of elements to the size of the array #(shifting to multiply by 8) sldi 5, 4, 3 #Set current maximum to 0 li, 7, 0 loop: #Go to next value; set status register (in cr0) add. 5, 5, 8 #Load Value (X-Form - adds reg. 3 + reg. 5 to get the final address) ldx 6, 3, 5 #Unsigned comparison of current value to current maximum (use cr7) cmpld cr7, 6, 7 #if the current one is greater, set it bt 4*cr7+gt, set_new_maximum set_new_maximum_ret: #Loop unless the last index decrement resulted in zero bf eq, loop #AFTER THE #Move result to return value mr 3, 7 #return blr set_new_maximum: mr 7, 6 b set_new_maximum_ret |
這和前面的版本非常類(lèi)似,主要區別如下:
這個(gè)程序使用的 C 語(yǔ)言數據類(lèi)型是 unsigned long long。這編寫(xiě)起來(lái)非常麻煩,因此最好將其用 typedef 定義為另外一個(gè)類(lèi)型,例如 uint64。這樣一來(lái),此函數的原型就會(huì )如下所示:
uint64 find_maximum_value(uint64[] value_list, uint64 num_values); |
下面是測試新函數的一個(gè)簡(jiǎn)單驅動(dòng)程序(可以將其輸入到 use_max.c 中):
#include <stdio.h> typedef unsigned long long uint64; uint64 find_maximum_value(uint64[], uint64); int main() { uint64 my_values[] = {2364, 666, 7983, 456923, 555, 34}; uint64 max = find_maximum_value(my_values, 6); printf("The maximum value is: %llu\n", max); return 0; } |
要編譯并運行這個(gè)程序,可以簡(jiǎn)單地執行下面的操作:
gcc -m64 use_max.c max_function.s -o maximum ./maximum |
請注意由于我們實(shí)際上是在進(jìn)行格式化打印,而不是將值返回到 shell 中,因此可以使用 64 位大小的全部數組元素。
簡(jiǎn)單函數調用在性能方面的開(kāi)銷(xiāo)非常小。簡(jiǎn)化的函數調用 ABI 完全是標準的,更易于編寫(xiě)混合語(yǔ)言程序,這類(lèi)程序要求在其核心循環(huán)中具有定制匯編語(yǔ)言的速度,在其他地方具有高級語(yǔ)言的表述性和易用性。
了解分支處理器的詳細內容可以幫助我們編寫(xiě)更加有效的 PowerPC 代碼。使用不同的條件寄存器域可以讓程序員按照自己感興趣的方法來(lái)保存并組合條件。使用計數寄存器可以幫助實(shí)現更加有效的代碼循環(huán)。簡(jiǎn)單函數甚至讓新手程 序員也可以編寫(xiě)非常有用的匯編語(yǔ)言函數,并將其提供給高級語(yǔ)言程序使用。
在下一篇文章中,我將介紹 PowerPC ABI 函數調用方面的內容,還會(huì )討論堆棧在 PowerPC 平臺上是如何工作的。
聯(lián)系客服