用于 Power 体系结构的汇编语言

1 部分: 编程概念和基本 PowerPC 指令

POWER5 和其他 PowerPC 处理器系列产品

POWER5™ 处理器是支持 PowerPC® 指令集的高性能处理器系列产品中的最新产品。此系列处理器中的第一款 64 位处理器就是 POWER3Macintosh G5 处理器是 POWER4 处理器的扩展,增加了额外的向量处理单元。POWER5 处理器是最新一代的 POWER 处理器,同时具备双核和对称多线程功能。这使单独一个芯片能够同步处理 4 个线程!不仅如此,各线程在每个时钟周期内还可执行一组指令(最多可达到 5 条)。

PowerPC 指令集广泛应用于 IBM 和其他厂商提供的多种芯片,而不仅仅是 POWER 系列。它用在服务器、工作站和高端嵌入式环境之中(设想数字摄像机和路由器,而不是移动电话)。Gekko 芯片用在了任天堂的 GameCube 中,Xenon 则用在了 Microsoft Xbox 360 中。Cell Broadband Engine 是近来崭露头角的一种体系结构,使用 PowerPC 指令,并且具有八个向量处理器。Sony PlayStation 3 将使用 Cell,考虑到 PlayStation 3 将用于广泛的多媒体应用程序,因此还使用为数众多的其他向量。

如您所见,PowerPC 指令集比 POWER 处理器系列更加有用。指令集本身可以 64 位模式操作,也可以简化的 32 位模式操作。POWER5 处理器支持这两种模式,POWER5 上的 Linux 发布版支持为 32 位和 64 PowerPC 指令集而编译的应用程序。

访问 POWER5 处理器

目前所有的 IBM iSeries pSeries 服务器都使用 POWER5 处理器,并可运行 Linux。此外,开源开发人员还可请求访问 POWER5 机器,以便通过 IBM OpenPower 项目移植应用程序(相关链接请参见 参考资料 部分)。在一台 G5 Power Macintosh 上运行 PowerPC 发布版即可访问略加修改的 POWER4 处理器,它也是 64 位的。G4 和更早的版本则仅有 32 位。

DebianRed HatSUSE Gentoo 均有一个或多个发布版支持 POWER5 处理器,只有 Red Hat Enterprise Linux ASSUSE Linux Enterprise Server OpenSUSE 支持 IBM iSeries 服务器(其余均支持 IBM pSeries 服务器)。

高级编程与低级编程的对比

大多数编程语言都与处理器保持着相当程度的独立性。但都有一些特殊特性依赖于处理器的某些功能,它们更有可能是特定于操作系统的,而不是特定于处理器的构建高级编程语言的目的是在程序员和硬件体系结构间搭建起一座桥梁。这样做有多方面的原因。尽管可移植性是原因之一,但更重要的一点或许是提供一种更友好 的模型,这种模型的建立方式更接近程序员的思考方式,而不是芯片的连线方式。

然而,在汇编语言编程中,您要直接应对处理器的指令集。这意味着您看系统的方式与硬件相同。这也有可能使汇编语言编程变得更为困难,因为编程模型的建立更 倾向于使硬件工作,而不是密切反映问题域。这样做的好处在于您可以更轻松地完成系统级任务、执行那些与处理器相关性很强的优化任务。而缺点是您必须在那个 级别上进行思考,依赖于一种特定的处理器系列,往往还必须完成许多额外的工作以准确地建模问题域。

关于汇编语言,很多人未想到的一个好处就是它非常具体。在高级语言中,对每个表达式都要进行许多处理。您有时不得不担忧幕后到底发生了哪些事情。在汇编语言编程中,您可以完全精确地掌控硬件的行为。您可以逐步处理硬件级更改。

汇编语言基础

在了解指令集本身之前,有两项关于汇编语言的关键内容需要理解,也就是内存模型和获取-执行周期

内存模型非常简单。内存只存储一种东西 —— 固定范围内的数字,也称为字节(在大多数计算机上,这是一个 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 扩展记忆法。也就是说,它们实际上是一条更为通用的指令的特殊形式。例如,上述所有条件跳转指令实际上都是 bcbranch conditional)指令的特殊形式。bc 指令的形式是 bc MODE, CBIT, ADDRESSCBIT 是条件寄存器要测试的位。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 中地址处的值载入寄存器 40 表示向该地址加零)。现在寄存器 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 的汇编语言编程应有了基本的了解。指令最初看上去可能有点麻烦,但习惯总会成自然。在下一期文章中,我们将介绍 PowerPC 处理器的各种寻址模式,说明如何更有效地将它们用于 64 位编程。第 3 篇文章将更全面地介绍 ABI,讨论有哪些寄存器、分别有哪些用途、如何调用函数并从函数返回,以及关于 ABI 的其他有趣内容。

2 部分: PowerPC 上加载和存储的艺术

寻址模式以及寻址模式之所以重要的原因

在开始讨论寻址模式之前,让我们首先来回顾一下计算机内存的概念。您可能已经了解了关于内存和编程的一些事实,但是由于现代编程语言正试图淡化计算机中的一些物理概念,因此复习一下相关内容是很有用的:

  • 主存中的每个位置都使用连续的数字地址 编号,内存位置就使用这个地址来引用。
  • 每个主存位置的长度都是一个字节
  • 较大的数据类型可以通过简单地将多个字节当作一个单位实现(例如,将两个内存位置放到一起作为一个 16 位的数字)。
  • 寄存器的长度在 32 位平台上是 4 个字节,在 64 位平台上是 8 个字节
  • 每次可以将 124 8 个字节的内存加载到寄存器中
  • 非数字数据可以作为数字数据进行存储 —— 惟一的区别在于可以对这些数据执行哪些操作,以及如何使用这些数据。

新接触汇编语言的程序员有时可能会对我们有多少访问内存的方法感到惊奇。这些不同的方法就称为寻址模式。 有些模式逻辑上是等价的,但是用途却不同。它们之所以被视为不同的寻址模式,原因在于它们可能根据处理器采用了不同的实现。

有两种寻址模式实际上根本就不会访问内存。在立即寻址模式 中,要使用的数据是指令的一部分(例如 li 指令就表示立即加载,这是因为要加载的数字就是这条指令本身 的一部分)。在寄存器寻址模式 中,我们也不会访问主存的内容,而是访问寄存器。

访问主存最显而易见的寻址模式称为直接寻址模式。在这种模式中,指令本身就包含了数据加载的源地址。这种模式通常用于全局变量访问、分支以及子程序调用。稍微简单的一种模式是相对寻址模式,它会根据当前程序计数器来计算地址。这通常用于短程分支,其中目标地址距当前位置很近,因此指定一个偏移量(而不是绝对地址)会更有意义。这就像是直接寻址模式的最终地址在汇编或链接时就知道了一样。

索引寻址模式 对于全局变量访问数组元素来说是最为有效的一种方式。它包括两个部分:一个内存地址以及一个索引寄存器。索引寄存器会与某个指定的地址相加,结果用作访问内存时使用的地址。有些平台(非 PowerPC)允许程序员为索引寄存器指定一个倍数。因此,如果每个数组元素的长度都是 8 个字节,那么我们就可以使用 8 作为倍数。这样就可以将索引寄存器当作数组索引来使用。否则,就必须按照数据大小来增加或减少索引寄存器了。

寄存器间接寻址模式 使用一个寄存器来指定内存访问的整个地址。这种模式在很多情况中都会使用,包括(但不限于):

  • 解除指针变量的引用
  • 使用其他模式无法进行的内存访问(地址可以通过其他方式进行计算,并存储到寄存器中,然后就使用这个值来访问内存)

基指针寻址模式 的工作方式与索引寻址模式非常类似(指定的数字和寄存器被加在一起得出最终地址),不过两个元素的作用交换了。在基指针寻址模式中,寄存器中保存的是基 址,数字是偏移量。这对于访问结构中的成员是非常有用的。寄存器可以存放整个结构的地址,数字部分可以根据所访问的结构成员进行修改。

最 后,假设我们有一个包括 3 个域的结构体:第一个域是 8 个字节,第二个域是 4 个字节,最后一个域是 8 个字节。然后,假设这个结构体本身的地址在一个名为 X 的寄存器中。如果我们希望访问这个结构体的第二个元素,就需要在寄存器中的值上加上 8。因此,使用基指针寻址模式,我们可以指定寄存器 X 作为基指针,8 作为偏移量。要访问第三个域,我们需要指定寄存器 X 作为指针,12 作为偏移量。要访问第一个域,我们实际上可以使用间接寻址模式,而不用使用基指针寻址模式,因为这里没有偏移量(这就是为什么在很多平台上第一个结构体成 员都是访问最快的一个成员;我们可以使用更加简单的寻址模式 —— PowerPC 上这并不重要)。

最后,索引寄存器间接寻址模式 中,基址和索引都保存在寄存器中。所使用的内存地址是通过将这两个寄存器加在一起来确定的。

指令格式的重要性

为了解寻址模式对于 PowerPC 处理器上的加载和存储指令是如何工作的,我们必须先要对 PowerPC 指令格式有点了解。PowerPC 使用了加载/存储(也成为 RISC)指令集,这意味着访问主存的惟一 时机就是将内存加载到寄存器或将寄存器中的内容复制到内存中时。所有实际的处理都发生在寄存器之间(或 寄存器和立即寻址模式操作数之间)。另外一种主要的处理器体系结构 CISCx86 处理器就是一种流行的 CISC 指令集)几乎允许在每条指令中进行内存访问。采用加载/存储体系架构的原因是这样可以使处理器的其他操作更加有效。实际上,现代 CISC 处理器将自己的指令转换成了内部使用的 RISC 格式,以实现更高的效率。

PowerPC 上的每条指令都正好是 32 位长,指令的 opcode(操 作符,告诉处理器这条指令是什么的代码)占据了前 6 这个 32 位的长度包含了所有的立即寻址模式的值、寄存器引用、显式地址以及指令选项。这实现了非常好的压缩。实际上,内存地址对于任何指令格式可以使用的最大长度 只有 24 位!最多只能给我们提供 16MB 的可寻址空间。不要担心 —— 有很多方法都可以解决这个问题。这只是为了说明为什么指令格式在 PowerPC 处理器上是如此重要 —— 您需要知道自己到底需要使用多少空间!

您不必记住所有的指令格式就能 使用它们。然而,了解一些指令的基本知识可以帮助您读懂 PowerPC 文档,并理解 PowerPC 指令集中的通用策略和一些细微区别。PowerPC 具有 15 种不同的指令格式,很多指令格式都有几种子格式。但只需要关心其中的几种即可。

 

 

 

使用 D-Form DS-Form 指令格式对内存进行寻址

D-Form 指令是主要的内存访问指令格式之一。它看起来像下面这样:

D-Form 指令格式

0 5

操作码

6 10

/目标寄存器

11 15

地址/索引寄存器/操作数

16 31

数字地址、偏移量或立即寻址模式值

这种格式用来进行加载、存储和立即寻址模式的计算。它可以用于以下寻址模式:

  • 立即寻址模式
  • 直接寻址模式(通过指定地址/索引寄存器为 0
  • 索引寻址模式
  • 间接寻址模式(通过指定地址为 0
  • 基指针寻址模式

如 您所见,D-Form 指令非常灵活,可以用于任何寄存器加地址的内存访问模式。然而,对于直接寻址和索引寻址来说,它的用处就非常有限了;这是因为它只能使用一个 16 位的地址域。它所提供的最大寻址范围是 64K。因此,直接和索引寻址模式都很少用来获取或存储内存。相反,这种格式更多用于立即寻址模式、间接寻址模式和基指针寻址模式,因为在这些寻址模式 中,64K 限制几乎都不是什么问题,因为基寄存器中就可以保存完整的 64 位的范围。

DS-Form 只在 64 位指令中使用。它与 D-Form 非常类似,不同之处在于它使用地址的最后两位作为扩展操作符。然而,它会在地址中 Value 部分最右边加上两个 0 。其范围与 D-Form 指令相同(64K),但是却将其限定为 32 位对齐的内存。对于汇编程序来说,这个值是通常是指定的 —— 它会通过汇编进行浓缩。例如,如果我们希望偏移量为 8,就仍然可以输入 8;汇编程序会将这个值转换成位表示 0b000000000010,而不是 0b00000000001000。如果我们输入一个不是 4 的部署的数字,那么汇编程序就会出错。

注意在 D-Form DS-Form 指令中,如果源寄存器被设置为 0,而不是使用寄存器 0,那么它就不会使用寄存器参数。

下面让我们来看一个使用 D-Forms DS-Forms 构成的指令。

立即寻址模式指定在汇编程序中是这样指定的:

opcode dst, src, value

 

此处 dst 是目标寄存器,src 是源寄存器(在计算中使用),value 是所使用的立即寻址模式的值。立即寻址模式指令永远都不会使用 DS-Form。下面是几个立即寻址模式的指令:


清单 1. 立即寻址模式的指令

 

#Add the contents of register 3 to the number 25 and store in register 2

addi 2, 3, 25

 

#OR the contents of register 6 to the number 0b0000000000000001 and store in register 3

ori 3, 6, 0b00000000000001

 

#Move the number 55 into register 7

#(remember, when 0 is the second register in D-Form instructions

#it means ignore the register)

addi 7, 0, 55

#Here is the extended mnemonics for the same instruction

li 7, 55

 

在使用 D-Form 的非立即寻址模式中,第二个寄存器被加到这个值上来计算加载或存储数据的内存的最终地址。这些指令的通用格式如下:

opcode dst, d(a)

 

在这种格式中,加载/存储数据的地址是作为 d(a) 指定的,其中 d 是数字地址/偏移量,而 a 是地址/偏移量所使用的寄存器的编号。它们被加在一起计算加载/存储数据的最终有效地址。下面是几个 D-Form/DS-Form 加载/存储指令的例子:


清单 2. 使用 D-Form DS-Form 加载/存储指令的例子

 

#load a byte from the address in register 2, store it in register 3,

#and zero out the remaining bits

lbz 3, 0(2)

 

#store the 64-bit contents (double-word) of register 5 into the

#address 32 bits past the address specified by register 23

std 5, 32(23)

 

#store the low-order 32 bits (word) of register 5 into the address

#32 bits past the address specified by register 23

stw 5, 32(23)

 

#store the byte in the low-order 8 bits of register 30 into the

#address specified by register 4

stb 30, 0(4)

 

#load the 16 bits (half-word) at address 300 into register 4, and

#zero-out the remaining bits

lhz 4, 300(0)

 

#load the half-word (16 bits) that is 1 byte offset from the address

#in register 31 and store the result sign-extended into register 18

lha 18, 1(31)

 

仔细观察,您就可以看出在有一种在指令开头指定的基址操作码,随后是几个修饰符。l s 用于 “load(加载) “store(存储)指令。b 表示一个字节,h 表示一个双字节(16 位)。w 表示一个字(32 位), d 表示一个双字节(64 位)。然后对于加载指令来说,a z 修饰符说明在将数据加载到寄存器中时,该值是符号扩展的,还是简单进行零填充的。最后,还可以附加上一个 u 来告诉处理器使用这条指令的最终计算地址来更新地址计算过程中所使用的寄存器。

 

 

 

使用 X-Form 指令格式对内存进行寻址

X-Form 用来进行索引寄存器间接寻址模式,其中两个寄存器中的值会被加在一起来确定加载/存储的地址。X-Form 的格式如下:

X-Form 指令格式

0 5

操作码

6 10

/目标寄存器

11 15

地址计算寄存器 A

16 20

地址计算寄存器 B

21 30

扩展操作符

31

保留未用

操作符的格式如下:

opcode dst, rega, regb

 

此处 opcode 是指令的操作符,dst 是数据传输的目标(或源)寄存器,rega regb 是用来计算地址所使用的两个寄存器。

下面给出几个使用 X-Form 的指令的例子:


清单 3. 使用 X-Form 寻址的例子

 

#Load a doubleword (64 bits) from the address specified by

#register 3 + register 20 and store the value into register 31

ldx 31, 3, 20

 

#Load a byte from the address specified by register 10 + register 12

#and store the value into register 15 and zero-out remaining bits

lbzx 15, 10, 12

 

#Load a halfword (16 bits) from the address specified by

#register 6 + register 7 and store the value into register 8,

#sign-extending the result through the remaining bits

lhax 8, 6, 7

 

#Take the doubleword (64 bits) in register 20 and store it in the

#address specified by register 10 + register 11

stdx 20, 10, 11

 

#Take the doubleword (64 bits) in register 20 and store it in the

#address specified by register 10 + register 11, and then update

#register 10 with the final address

stdux 20, 10, 11

 

X-Form 的优点除了非常灵活之外,还为我们提供了非常广泛的寻址范围。在 D-Form 中,只有一个值 —— 寄存器 —— 可以指定一个完整的范围。在 X-Form 中,由于我们有两个寄存器,这两个组件都可以根据需要指定足够大的范围。因此,在使用基指针寻址模式或索引寻址模式而 D-Form 固定部分的 16 位范围太小的情况下,这些值就可以存储到寄存器中并使用 X-Form

 

 

编写与位置无关的代码

与 位置无关的代码是那些不管加载到哪部分内存中都能正常工作的代码。为什么我们需要与位置无关的代码呢?与位置无关的代码可以让库加载到地址空间中的任意位 置处。这就是允许库随机组合 —— 因为它们都没有被绑定到特定位置,所以就可以使用任意库来加载,而不用担心地址空间冲突的问题。链接器会负责确保每个库都被加载到自己的地址空间中。通过 使用与位置无关的代码,库就不用担心自己到底被加载到什么地方去了。

不过,最终与位置无关的代码需要有一种方法来定位全局变量。它可以通过维护一个全局偏移量表 来实现这种功能,这个表提供了函数或一组函数(在大部分情况中甚至是整个程序)访问的所有全局内容的地址。系统保留了一个寄存器来存放指向这个表的指针。 然后,所有访问都可以通过这个表中的一个偏移量来完成。偏移量是个常量。表本身是通过程序链接器/加载器来设置的,它还会初始化寄存器 2 来存放全局偏移量表的指针。使用这种方法,链接器/加载器就可以将认为适当的程序和数据放在一起,这只需要设置包含所有全局指针的一个全局偏移量表即可。

很容易陷于对这些问题的讨论细节当中。下面让我们来看一些代码,并分析一下这种方法的每个步骤都在做些什么。这是 上一篇文章 中使用的加法程序,不过现在调整成了与位置无关的代码。


清单 4. 通过全局偏移量表来访问数据

 

###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

 

要汇编、连接并运行这段代码,请按以下方法执行:


清单 5. 汇编、连接并运行代码

 

#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 仍然可以工作,不过其功能也与以前一样 —— 返回一个指向值的指针,而不是返回值本身。下面看一下这段代码:


清单 6. @got @toc 之间的区别

 

### 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 中的值来进行加法计算的例子:


清单 7. .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(存储多个字)。下面是几个例子:


清单 8. 加载和存储多个值

 

#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)

 

下面是使用多个值的加法程序:


清单 9. 使用多个值的加法程序

 

### 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

摘要溢出

摘要溢出

无序

溢出异常

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

条件寄存器可以使用 mtcrmtcrf mfcr 加载到通用寄存器中(或从通用寄存器中进行加载)。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 则将值 链接寄存器拷贝到通用寄存器中。mtctr mfctr 也可以对计数寄存器实现相同的功能。

 

 

无条件分支

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

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

 

 

条件分支

比较寄存器

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

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

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


清单 4. 条件分支的基本格式

 

#compare register 4 and 5

cmpd 4, 5

#branch if they are equal

bc 12, 2 address

 

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

有几种方法可以指定扩展记忆法。我们将着重介绍指令名和指令的 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<opname> 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 来控制循环。代码如下所示:


清单 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
  • 只能修改条件寄存器域 cr0cr1cr5cr6 cr7
  • 不能修改链接寄存器,除非在调用 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 平台上是如何工作的。

 

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值