PowerPC汇编学习

来源:百度文库 编辑:神马文学网 时间:2024/07/04 23:10:54
与 位置无关的代码是那些不管加载到哪部分内存中都能正常工作的代码。为什么我们需要与位置无关的代码呢?与位置无关的代码可以让库加载到地址空间中的任意位 置处。这就是允许库随机组合 —— 因为它们都没有被绑定到特定位置,所以就可以使用任意库来加载,而不用担心地址空间冲突的问题。链接器会负责确保每个库都被加载到自己的地址空间中。通过 使用与位置无关的代码,库就不用担心自己到底被加载到什么地方去了。
不过,最终与位置无关的代码需要有一种方法来定位全局变量。它可以通过维护一个全局偏移量表 来实现这种功能,这个表提供了函数或一组函数(在大部分情况中甚至是整个程序)访问的所有全局内容的地址。系统保留了一个寄存器来存放指向这个表的指针。 然后,所有访问都可以通过这个表中的一个偏移量来完成。偏移量是个常量。表本身是通过程序链接器/加载器来设置的,它还会初始化寄存器 2 来存放全局偏移量表的指针。使用这种方法,链接器/加载器就可以将认为适当的程序和数据放在一起,这只需要设置包含所有全局指针的一个全局偏移量表即可。
很容易陷于对这些问题的讨论细节当中。下面让我们来看一些代码,并分析一下这种方法的每个步骤都在做些什么。这是  中使用的 “加法” 程序,不过现在调整成了与位置无关的代码。
 
###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 $?
 
数据定义和入口点声明与之前的例子相同。不过,我们不用再使用 5 条指令将 first_value 的地址加载到寄存器 7 中了,现在只需要一条指令就可以了:ld 7, first_value@got(2)。正如前面介绍的一样,连接器/加载器会将寄存器 2 设置为全局偏移量表的地址。语法 first_value@got 会请求链接器不要使用 first_value 的地址,而是使用全局偏移量表中包含 first_value 地址的偏移量。
使用这种方法,大部分程序员都可以包含他们在一个全局偏移量表中使用的所有全局数据。DS-Form 从一个基址可以寻址多达 64K 的内存。注意为了获得 DS-Form 的整个范围,寄存器 2 指向了全局偏移量表的 中部, 这样我们就可以使用正数偏移量和负数偏移量了。由于我们正在定位的是指向数据的指针(而不是直接定位数据),因此我们可以访问大约 8,000 个全局变量(局部变量都保存在寄存器或堆栈中,这会在本系列的第三篇文章中进行讨论)。即使这还不够,我们还有多个全局偏移量表可以使用。这种机制也会在 下一篇文章中进行讨论。
尽管这比上一篇文章中所使用的 5 条指令的数据加载更加简洁,可读性也更好,但是我们仍然可以做得更好些。在 64 位 ELF ABI 中,全局偏移量表实际上是一个更大的部分 —— 称为内容表(table of contents) —— 的一个子集。除了创建全局偏移量表入口之外,内容表还包含变量,它没有包含全局数据的 地址,而是包含的数据本身。这些变量的大小和个数必须很小,因为内容表只有 64K。
要声明一个内容表的数据项,我们需要切换到 .toc 段,并显式地进行声明,如下所示:
.section .toc
name:
.tc unused_name[TC], initial_value
 
这会创建一个内容表入口。name 是在代码中引用它所使用的符号。initial_value 是初始化分配的一个 64 位的值。unused_name 是历史记录,现在在 ELF 系统上已经没有任何用处了。我们可以不再使用它了(此处包含进来只是为了帮助我们阅读遗留代码),不过 [TC] 是需要的。
要访问内容表中直接保存的数据,我们需要使用 @toc 来引用它,而不能使用 @got。@got 仍然可以工作,不过其功能也与以前一样 —— 返回一个指向值的指针,而不是返回值本身。下面看一下这段代码:
 
### 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
 
如您所见,如果查看在 .toc 段中所定义的符号(而不是大部分数据所在的 .data 段),使用 @toc 可以提供直接到值本身的偏移量,而使用 @got 只能提供一个该值地址的偏移量。
现在看一下使用 Toc 中的值来进行加法计算的例子:
 
### 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
 
可以看到,通过使用基于 .toc 的数据,我们可以显著减少代码所使用的指令数量。另外,由于这个内容表通常就在缓存中,它还可以显著减少内存的延时。我们只需要谨慎处理存储的数据量就可以了。
 

 
 
PowerPC 还可以在一条指令中执行多个加载和存储操作。不幸的是,这限定于字大小(32 位)的数据。这些都是非常简单的 D-Form 指令。我们指定了基址寄存器、偏移量和起始目标寄存器。处理器然后会将数据加载到通过寄存器 31 所列出的目标寄存器开始的所有寄存器中,这会从指令所指定的地址开始,一直往前进行。此类指令包括 lmw (加载多个字)和 stmw(存储多个字)。下面是几个例子:
 
#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)
 
下面是使用多个值的加法程序:
 
### 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
 
 
大多数加载/存储指令都可以使用加载/存储指令最终使用的有效地址来更新主地址寄存器。例如,ldu 5, 4(8) 会将寄存器 8 中指定的地址加上 4 个字节加载到寄存器 5 中,然后将计算出来的地址存回 寄存器 8 中。这称为带更新 的加载和存储,这可以用来减少执行多个任务所需要的指令数。在下一篇文章中我们将更多地使用这种模式。

 
 

 
有 效地进行加载和存储对于编写高效代码来说至关重要。了解可用的指令格式和寻址模式可以帮助我们理解某种平台的可能性和限制。PowerPC 上的 D-Form 和 DS-Form 指令格式对于与位置无关的代码来说非常重要。与位置无关的代码允许我们创建共享库,并使用较少的指令就可以完成加载全局地址的工作。
本系列的下一篇文章将介绍分支、函数调用以及与 C 代码的集成问题。
 
第 3 部分: 使用 PowerPC 分支处理器进行编程
分支寄存器
PowerPC 中的分支利用了 3 个特殊用途的寄存器:条件寄存器、计数寄存器 和链接寄存器。
条件寄存器从概念上来说包含 7 个域(field)。域是一个 4 位长的段,用来存储指令结果状态信息。其中有两个域是专用的(稍后就会介绍),其余域是通用的。这些域的名字为 cr0 到 cr7。
第一个域 cr0 用来保存定点计算指令的结果,它使用了非立即操作(有几个例外)。计算的结果会与 0 进行比较,并根据结果设置适当的位(负数、零或正数)。要想在计算指令中设置 cr0,可以简单地在指令末尾添加一个句点(.)。例如,add 4, 5, 6这条指令是将寄存器 5 和寄存器 6 进行加法操作,并将结果保存到寄存器 4 中,而不会在 cr0 中设置任何状态位。add. 4, 5, 6 也可以进行相同的加法操作,不过会根据所计算出来的值设置 cr0 中的位。cr0 也是比较指令上使用的默认域。
第二个域(称为 cr1)用于浮点指令,方法是在指令名后加上句点。浮点计算的内容超出了本文的讨论范围。
每个域都有 4 个位。这些位的用法根据所使用的指令的不同会有所不同。下面是可能的用法(下面也列出了浮点指令,不过没有详细介绍):

记忆法
定点比较
定点计算
浮点比较
浮点计算
0
lt
小于
负数
小于
异常摘要
1
gt
大于
正数
大于
启用异常摘要
2
eq
等于
0
等于
无效操作异常摘要
3
so
摘要溢出
摘要溢出
无序
溢出异常
稍后您就会看到如何隐式或直接访问这些域。
条件寄存器可以使用 mtcr、mtcrf 和 mfcr 加载到通用寄存器中(或从通用寄存器中进行加载)。mtcr 将一个特定的通用寄存器加载到条件寄存器中。mfcr 将条件寄存器移到通用寄存器中。mtcrf 从通用寄存器中加载条件寄存器,不过只会加载由 8 位掩码所指定的域,即第一个操作数。
下面是几个例子。
 
#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)是专用寄存器,其中保存了分支指令的返回地址。所有的分支指令都可以用来设置链接寄存器;如果进行分支,就将链接寄存器设置成当前指令之后紧接的那条指令的地址。分支指令通过将字母 l 附加到指令末尾来设置链接寄存器。举例来说,b 是无条件的分支指令,而 bl 则是设置链接寄存器的一条无条件分支指令。
计数寄存器(名为 CTR)是用来保存循环计数器的一个专用寄存器。专用分支指令可能会减少计数寄存器,并且(或者)会根据 CTR 是否达到 0 来进行条件分支跳转。
链接寄存器和计数寄存器都可以用作分支目的地。bctr 分支跳转到计数寄存器中指定的地址,blr 分支跳转到链接寄存器中指定的地址。
链接寄存器和计数寄存器的值也可以从通用寄存器中拷贝而来,或者拷贝到通用寄存器中。对于链接寄存器来说,mtlr 会将给定的寄存器值拷贝到 链接寄存器中,mflr 则将值从 链接寄存器拷贝到通用寄存器中。mtctr 和 mfctr 也可以对计数寄存器实现相同的功能。
 
 
PowerPC 指令集中的无条件分支使用了 I-Form 指令格式:
指令格式
0-5 位
操作码
6-29 位
绝对或相对分支地址
30 位
绝对地址位 —— 如果这个域被置位了,那么指令就会被解释成绝对地址,否则就被解释成相对地址
31 位
链接位 —— 如果这个域被置位了,那么指令就会将链接寄存器设置为下一条指令的地址
正如前面介绍的一样,将字母 l 添加到分支指令后面会导致链接位被置位,从而使 “返回地址”(分支跳转后的指令)存储在链接寄存器中。如果您在指令末尾再加上字母 a(位于 l 之后,如果l 也同时使用的话),那么所指定的地址就是绝对地址(通常在用户级代码中不会这样用,因为这会过多地限制分支目的地)。
清单 2 阐述了无条件分支的用法,然后退出(您可以将下面的代码输入到 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
 
对这个程序进行汇编和链接,然后运行,方法如下:
as -a64 branch_example.s -o branch_example.o
ld -melf64ppc branch_example.o -o branch_example
./branch_example
请注意 b 和 ba 的目标在汇编语言中是以相同的方式来指定的,尽管二者在指令中的编码方式大不相同。汇编器和链接器会负责为我们将目标地址转换成相对地址或绝对地址。
 
 
cmp 指令用来将寄存器与其他寄存器或立即操作数进行比较,并设置条件寄存器中适当状态位。缺省情况下,定点比较指令使用 cr0 来存储结果,但是这个域也可以作为一个可选的第一操作数来指定。比较指令的用法如清单 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 说明最后一个操作数是立即值,而不是寄存器,l 告诉处理器要进行无符号(也称为逻辑)比较操作,而不是进行有符号比较操作。
每条指令都会设置条件寄存器中的适当位(正如本文前面介绍的一样),这些值然后会由条件分支指令来使用。
条件分支比无条件分支更加灵活,不过它的代价是可跳转的距离不够大。条件分支使用了 B-Form 指令格式:
指令格式
0-5 位
操作码
6-10 位
指定如何对位进行测试、是否使用计数寄存器、如何使用计数寄存器,以及是否进行分支预测(称为 BO 域)
11-15 位
指定条件寄存器中要测试的位(称为 BI 域)
16-29 位
绝对或相对地址
30 位
寻址模式 —— 该位设置为 0 时,指定的地址就被认为是一个相对地址;当该位设置为 1 时,指定的地址就被认为是一个绝对地址
31 位
链接位 —— 当该位设置为 1 时,链接寄存器 被设置成当前指令的下一条指令的地址;当该位设置为 0 时,链接寄存器没有设置
正如您可以看到的一样,我们可以使用完整的 10 位值来指定分支模式和条件,这会将地址大小限制为只有 14 位(范围只有 16K)。这对于函数中的短跳转非常有用,但是对其他跳转指令来说就没多大用处了。要有条件地调用一个 16K 范围之外的函数,代码需要进行一个条件分支,跳转到一条包含无条件分支的指令,进而跳转到正确的位置。
条件分支的基本格式如下所示:
bc BO, BI, address
bcl BO, BI, address
bca BO, BI, address
bcla BO, BI, address
在这个基本格式中,BO 和 BI 都是数字。幸运的是,我们并不需要记住所有的数字及其意义。PowerPC 指令集的扩展记忆法(在第一篇中已经介绍过了)在这里又可以再次派上用场了,这样我们就不必非要记住所有的数字。与无条件分支类似,在指令名后面添加一个 l 就可以设置链接寄存器,在指令名后面添加一个 a 会让指令使用绝对寻址而不是相对寻址。
对于一个简单比较且在比较结果相等时发生跳转的情况来说,基本格式(没有使用扩展记忆法)如下所示:
 
#compare register 4 and 5
cmpd 4, 5
#branch if they are equal
bc 12, 2 address
 
bc 表示“条件分支(branch conditionally)”。12(BO 操作数)的意思是如果给定的条件寄存器域被置位了就跳转,不采用分支预测,2(BI 操作数)是条件寄存器中要测试的位(是等于位)。现在,很少有人(尤其是新手)能够记住所有的分支编号和条件寄存器位的数字编号,这也没太大用处。扩展记忆法可以让代码的阅读、编写和调试变得更加清晰。
有几种方法可以指定扩展记忆法。我们将着重介绍指令名和指令的 BO 操作数(指定模式)的几种组合。最简单的用法是 bt 和 bf。 如果条件寄存器中的给定位为真,bt 就会进行分支跳转;如果条件寄存器中给定位为假,bf 就会进行分支跳转。另外,条件寄存器位也可以使用这种记忆法来指定。如果您指定了 4*cr3+eq,这会测试 cr3 的位 2(之所以会用 4* 是因为每个域都是 4 位宽的)。位域中的每个位的可用记忆法已经在前面对条件寄存器的介绍中给出了。如果您只指定了位,而没有指定域,那么指令就会缺省为 cr0。
下面是几个例子:
清单 5. 简单的条件分支
 
#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 操作数和条件位(不过没有域)。它们多少使用了“传统”记忆方法来表示各种常见的条件分支。例如,bne my_destination(如果不等于 my_destination 就发生跳转)与 bf eq, my_destination(如果 eq 位为假就跳转到 my_destination)是等效的。要利用这种记忆法来使用不同的条件寄存器域,可以简单地在目标地址前面的操作数中指定域,例如 bne cr4, my_destination。这些分支记忆法遵循的模式是:blt(小于)、ble(小于或等于)、beq(等于)、 bge (大于或等于)、bgt(大于)、bnl(不小于)、bne(不等于)、bng(不大于)、 bso(溢出摘要)、 bns (无溢出摘要)、 bun(无序 —— 浮点运算专用) 和 bnu(非无序 —— 浮点运算专用)。
所有的记忆法和扩展记忆法可以在指令后面附加上 l 和/或 a 来分别启用链接寄存器或绝对寻址。
使用扩展记忆法可以允许采用更容易读取和编写的编程风格。对于更高级的条件分支来说,扩展记忆法不仅非常有用,而且非常必要。
由于条件寄存器有多个域,不同的计算和比较可以使用不同的域,而逻辑操作可以用来将这些条件组合在一起。所有的逻辑操作都有如下格式:cr target_bit, operand_bit_1, operand_bit_2。例如,要对cr2 的 eq 位和 cr7 的 lt 位进行一个 and 逻辑操作,并将结果存储到 cr0 的 eq 位中,就可以这样编写代码:crand 4*cr0+eq, 4*cr2+eq, 4*cr7+lt。
您可以使用 mcrf 来操作条件寄存器域。要将 cr4 拷贝到 cr1 中,可以这样做:mcrf cr1, cr4。
分支指令也可以为分支处理器进行的分支预测提供提示。在最常用的条件分支指令后面加上一个 +,就可以向分支处理器发送一个信号,说明可能会发生分支跳转。在指令后面加上一个 -,就可以向分支处理器发送一个信号,说明不会发生分支跳转。然而,这通常都是不必要的,因为 POWER5 CPU 中的分支处理器可以很好地处理分支预测。
 
 
计数寄存器是循环计数器使用的一个专用寄存器。条件分支的 BO 操作数(控制模式)也可以使用,用来指定如何测试条件寄存器位,减少并测试计数寄存器。下面是您可以对计数寄存器执行的两个操作:
减少计数寄存器,如果为 0 就分支跳转
减少计数寄存器,如果非 0 就分支跳转
这些计数寄存器操作可以单独使用,也可以与条件寄存器测试一起使用。
在扩展记忆法中,计数寄存器的语义可以通过在 b 后面立即添加 dz 或 dnz 来指定。任何其他条件或指令修改符也都可以添加到这后面。因此,要循环 100 次,您可以将 100 加载到计数寄存器中,并使用 bdnz 来控制循环。代码如下所示:
 
#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 address
loop_start:
 
###loop body goes here###
 
#Decrement count register and branch if it becomes nonzero
bdnz loop_start
 
#Code after loop goes here
 
您也可以将计数器测试与其他测试一起使用。举例来说,循环可能需要有一个提前退出条件。下面的代码展示了当寄存器 24 等于寄存器 28 时就会触发的提前退出条件。
 
#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 address
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
 
因此,我们并不需要再增加一条条件分支指令,所需要做的只是将比较指令和条件指令合并为一个循环计数器分支。
 
 
现在我们将在实践中应用上面介绍的内容。
下面的程序利用了第一篇文章中所介绍的最大值 程序,并根据我们学习到的知识进行了重新编写。该程序的第一个版本使用了一个寄存器来保存所读取的当前地址,并通过间接寻址加载值。这个程序要做的是使用 索引间接寻址模式,使用一个寄存器作为基地址,使用另一个寄存器作为索引。另外,除了索引是从 0 开始并简单增加之外,索引还会从尾到头进行计数,用来保存额外的比较指令。减量可以隐式地设置条件寄存器(这与和 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 LOOP -- exit
   li 0, 1
   sc
 
set_new_maximum:
   mr 3, 6
   blr (return using the link register)
 
对这个程序进行汇编、链接和执行,方法如下:
as -a64 max_enhanced.s -o max_enhanced.o
ld -melf64ppc max_enhanced.o -o max_enhanced
./max_enhanced
 
这个程序中的循环比第一篇文章中的循环大约会快 15%,原因有两个: (a) 主循环中减少了几条指令,这是由于在我们减少寄存器 5 时使用了状态寄存器来检测列表的末尾; (b) 程序使用了不同的条件寄存器域来进行比较(因此减量的结果可以保留下来供以后使用)。
请注意在对 set_new_maximum 的调用中使用链接寄存器并非十分必要。即使不使用链接寄存器,它也可以很好地设置返回地址。不过,这个使用链接寄存器的例子会有助于说明链接寄存器的用法。
 
 
PowerPC ABI 相当复杂,我们将在下一篇文章中继续介绍。然而,对于那些不会调用任何其他函数并且遵循简单规则的函数来说,PowerPC ABI 提供了相当简单的函数调用机制。
为了能够使用这个简化的 ABI,您的函数必须遵循以下规则:
不能调用任何其他函数。
只能修改寄存器 3 到 12。
只能修改条件寄存器域 cr0、cr1、cr5、cr6 和 cr7。
不能修改链接寄存器,除非在调用 blr 返回之前已经复原了链接寄存器。
当函数被调用时,参数都是在寄存器中发送的,这些参数保存在寄存器 3 到寄存器 10,需要使用多少个寄存器取决于参数的个数。当函数返回时,返回值必须保存到寄存器 3 中。
下面让我们将原来的最大值程序作为一个函数进行重写,然后在 C 语言中调用这个函数。
我们应该传递的参数如下:指向数组的指针,这是第一个参数(寄存器 3);数组大小,这是第二个参数(寄存器 4)。之后,最大值就可以放入寄存器 3 中作为返回值。
下面就是我们将其作为函数改写后的程序(将其输入到 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 LOOP
   #Move result to return value
   mr 3, 7
  
   #return
   blr
 
set_new_maximum:
   mr 7, 6
   b set_new_maximum_ret
 
这和前面的版本非常类似,主要区别如下:
初始条件都是通过参数传递的,而不是写死的。
函数中寄存器的使用都为匹配所传递的参数的布局进行了修改。
删除了 set_new_maximum 对链接寄存器不必要的使用以保护链接寄存器的内容。
这个程序使用的 C 语言数据类型是 unsigned long long。这编写起来非常麻烦,因此最好将其用 typedef 定义为另外一个类型,例如 uint64。这样一来,此函数的原型就会如下所示:
uint64 find_maximum_value(uint64[] value_list, uint64 num_values);
 
下面是测试新函数的一个简单驱动程序(可以将其输入到 use_max.c 中):
 
#include
 
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;
}
 
要编译并运行这个程序,可以简单地执行下面的操作:
gcc -m64 use_max.c max_function.s -o maximum
./maximum
 
请注意由于我们实际上是在进行格式化打印,而不是将值返回到 shell 中,因此可以使用 64 位大小的全部数组元素。
简单函数调用在性能方面的开销非常小。简化的函数调用 ABI 完全是标准的,更易于编写混合语言程序,这类程序要求在其核心循环中具有定制汇编语言的速度,在其他地方具有高级语言的表述性和易用性。
 
 
了解分支处理器的详细内容可以帮助我们编写更加有效的 PowerPC 代码。使用不同的条件寄存器域可以让程序员按照自己感兴趣的方法来保存并组合条件。使用计数寄存器可以帮助实现更加有效的代码循环。简单函数甚至让新手程 序员也可以编写非常有用的汇编语言函数,并将其提供给高级语言程序使用。
在下一篇文章中,我将介绍 PowerPC ABI 函数调用方面的内容,还会讨论堆栈在 PowerPC 平台上是如何工作的。