用于 Power 体系结构的汇编语言,第 3 部分: 使用 PowerPC 分支处理器进行编程

深入了解分支指令和寄存器

前两篇文章 中,您看到了 POWER5 处理器上的程序是如何使用 64 位 PowerPC® 指令集工作的、PowerPC 指令集是如何对内存进行寻址的,以及如何编写与位置无关的代码。在本文中,您将学习如何使用 PowerPC 指令集中功能强大的条件和分支指令。

Jonathan Bartlett (johnnyb@eskimo.com), 技术总监, New Medio

Jonathan Bartlett 是 Programming from the Ground Up 一书的作者,这本书对使用 Linux 汇编语言的编程进行了简介。他是 New Medio 的开发技术总监,为客户开发 Web 应用程序、视频应用程序、kiosk 应用程序和桌面应用程序。



2007 年 2 月 13 日

分支寄存器

PowerPC 中的分支利用了 3 个特殊用途的寄存器:条件寄存器计数寄存器链接寄存器

条件寄存器

条件寄存器从概念上来说包含 7 个域(field)。域是一个 4 位长的段,用来存储指令结果状态信息。其中有两个域是专用的(稍后就会介绍),其余域是通用的。这些域的名字为 cr0cr7

第一个域 cr0 用来保存定点计算指令的结果,它使用了非立即操作(有几个例外)。计算的结果会与 0 进行比较,并根据结果设置适当的位(负数、零或正数)。要想在计算指令中设置 cr0,可以简单地在指令末尾添加一个句点(.)。例如,add 4, 5, 6 这条指令是将寄存器 5 和寄存器 6 进行加法操作,并将结果保存到寄存器 4 中,而不会在 cr0 中设置任何状态位。add. 4, 5, 6 也可以进行相同的加法操作,不过会根据所计算出来的值设置 cr0 中的位。cr0 也是比较指令上使用的默认域。

第二个域(称为 cr1)用于浮点指令,方法是在指令名后加上句点。浮点计算的内容超出了本文的讨论范围。

每个域都有 4 个位。这些位的用法根据所使用的指令的不同会有所不同。下面是可能的用法(下面也列出了浮点指令,不过没有详细介绍):

条件寄存器域位
记忆法定点比较定点计算浮点比较浮点计算
0lt小于负数小于异常摘要
1gt大于正数大于启用异常摘要
2eq等于0等于无效操作异常摘要
3so摘要溢出摘要溢出无序溢出异常

稍后您就会看到如何隐式或直接访问这些域。

条件寄存器可以使用 mtcrmtcrfmfcr 加载到通用寄存器中(或从通用寄存器中进行加载)。mtcr 将一个特定的通用寄存器加载到条件寄存器中。mfcr 将条件寄存器移到通用寄存器中。mtcrf 从通用寄存器中加载条件寄存器,不过只会加载由 8 位掩码所指定的域,即第一个操作数。

下面是几个例子。

清单 1. 条件寄存器转换的例子
#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 则将值 链接寄存器拷贝到通用寄存器中。mtctrmfctr 也可以对计数寄存器实现相同的功能。


无条件分支

PowerPC 指令集中的无条件分支使用了 I-Form 指令格式:

I-Form 指令格式

0-5 位

操作码

6-29 位

绝对或相对分支地址

30 位

绝对地址位 —— 如果这个域被置位了,那么指令就会被解释成绝对地址,否则就被解释成相对地址

31 位

链接位 —— 如果这个域被置位了,那么指令就会将链接寄存器设置为下一条指令的地址

正如前面介绍的一样,将字母 l 添加到分支指令后面会导致链接位被置位,从而使 “返回地址”(分支跳转后的指令)存储在链接寄存器中。如果您在指令末尾再加上字母 a(位于 l 之后,如果l 也同时使用的话),那么所指定的地址就是绝对地址(通常在用户级代码中不会这样用,因为这会过多地限制分支目的地)。

清单 2 阐述了无条件分支的用法,然后退出(您可以将下面的代码输入到 branch_example.s 文件中):

清单 2. 无条件分支的例子
### 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

请注意 bba 的目标在汇编语言中是以相同的方式来指定的,尽管二者在指令中的编码方式大不相同。汇编器和链接器会负责为我们将目标地址转换成相对地址或绝对地址。


条件分支

比较寄存器

cmp 指令用来将寄存器与其他寄存器或立即操作数进行比较,并设置条件寄存器中适当状态位。缺省情况下,定点比较指令使用 cr0 来存储结果,但是这个域也可以作为一个可选的第一操作数来指定。比较指令的用法如清单 3 所示:

清单 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 指令格式:

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

在这个基本格式中,BOBI 都是数字。幸运的是,我们并不需要记住所有的数字及其意义。PowerPC 指令集的扩展记忆法(在第一篇中已经介绍过了)在这里又可以再次派上用场了,这样我们就不必非要记住所有的数字。与无条件分支类似,在指令名后面添加一个 l 就可以设置链接寄存器,在指令名后面添加一个 a 会让指令使用绝对寻址而不是相对寻址。

对于一个简单比较且在比较结果相等时发生跳转的情况来说,基本格式(没有使用扩展记忆法)如下所示:

清单 4. 条件分支的基本格式
#compare register 4 and 5
cmpd 4, 5
#branch if they are equal
bc 12, 2 address

bc 表示“条件分支(branch conditionally)”。12BO 操作数)的意思是如果给定的条件寄存器域被置位了就跳转,不采用分支预测,2BI 操作数)是条件寄存器中要测试的位(是等于位)。现在,很少有人(尤其是新手)能够记住所有的分支编号和条件寄存器位的数字编号,这也没太大用处。扩展记忆法可以让代码的阅读、编写和调试变得更加清晰。

有几种方法可以指定扩展记忆法。我们将着重介绍指令名和指令的 BO 操作数(指定模式)的几种组合。最简单的用法是 btbf。 如果条件寄存器中的给定位为真,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<opname> target_bit, operand_bit_1, operand_bit_2。例如,要对 cr2eq 位和 cr7lt 位进行一个 and 逻辑操作,并将结果存储到 cr0eq 位中,就可以这样编写代码:crand 4*cr0+eq, 4*cr2+eq, 4*cr7+lt

您可以使用 mcrf 来操作条件寄存器域。要将 cr4 拷贝到 cr1 中,可以这样做:mcrf cr1, cr4

分支指令也可以为分支处理器进行的分支预测提供提示。在最常用的条件分支指令后面加上一个 +,就可以向分支处理器发送一个信号,说明可能会发生分支跳转。在指令后面加上一个 -,就可以向分支处理器发送一个信号,说明不会发生分支跳转。然而,这通常都是不必要的,因为 POWER5 CPU 中的分支处理器可以很好地处理分支预测。


使用计数寄存器

计数寄存器是循环计数器使用的一个专用寄存器。条件分支的 BO 操作数(控制模式)也可以使用,用来指定如何测试条件寄存器位,减少并测试计数寄存器。下面是您可以对计数寄存器执行的两个操作:

  • 减少计数寄存器,如果为 0 就分支跳转
  • 减少计数寄存器,如果非 0 就分支跳转

这些计数寄存器操作可以单独使用,也可以与条件寄存器测试一起使用。

在扩展记忆法中,计数寄存器的语义可以通过在 b 后面立即添加 dzdnz 来指定。任何其他条件或指令修改符也都可以添加到这后面。因此,要循环 100 次,您可以将 100 加载到计数寄存器中,并使用 bdnz 来控制循环。代码如下所示:

清单 6. 使用计数器控制循环的例子
#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 时就会触发的提前退出条件。

清单 7. 计数寄存器组合分支的例子
#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 文件中):

清单 8. 最大值程序的增强版本
###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。
  • 只能修改条件寄存器域 cr0cr1cr5cr6cr7
  • 不能修改链接寄存器,除非在调用 blr 返回之前已经复原了链接寄存器。

当函数被调用时,参数都是在寄存器中发送的,这些参数保存在寄存器 3 到寄存器 10,需要使用多少个寄存器取决于参数的个数。当函数返回时,返回值必须保存到寄存器 3 中。

下面让我们将原来的最大值程序作为一个函数进行重写,然后在 C 语言中调用这个函数。

我们应该传递的参数如下:指向数组的指针,这是第一个参数(寄存器 3);数组大小,这是第二个参数(寄存器 4)。之后,最大值就可以放入寄存器 3 中作为返回值。

下面就是我们将其作为函数改写后的程序(将其输入到 max_function.s 文件中):

清单 9. 函数形式的最大值程序
###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 中):

清单 10. 使用最大值函数的 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;
}

要编译并运行这个程序,可以简单地执行下面的操作:

gcc -m64 use_max.c max_function.s -o maximum
./maximum

请注意由于我们实际上是在进行格式化打印,而不是将值返回到 shell 中,因此可以使用 64 位大小的全部数组元素。

简单函数调用在性能方面的开销非常小。简化的函数调用 ABI 完全是标准的,更易于编写混合语言程序,这类程序要求在其核心循环中具有定制汇编语言的速度,在其他地方具有高级语言的表述性和易用性。


结束语

了解分支处理器的详细内容可以帮助我们编写更加有效的 PowerPC 代码。使用不同的条件寄存器域可以让程序员按照自己感兴趣的方法来保存并组合条件。使用计数寄存器可以帮助实现更加有效的代码循环。简单函数甚至让新手程序员也可以编写非常有用的汇编语言函数,并将其提供给高级语言程序使用。

在下一篇文章中,我将介绍 PowerPC ABI 函数调用方面的内容,还会讨论堆栈在 PowerPC 平台上是如何工作的。

参考资料

学习

获得产品和技术

  • 订购 SEK for Linux,共包含两张 DVD,其中有用于 Linux 的最新 IBM 试用软件,包括 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。
  • 利用可直接从 developerWorks 下载的 IBM 试用版软件 在 Linux 上构建您的下一个开发项目。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux, Multicore acceleration
ArticleID=195701
ArticleTitle=用于 Power 体系结构的汇编语言,第 3 部分: 使用 PowerPC 分支处理器进行编程
publish-date=02132007