莫言笔下的性描写:PowerPC汇编语言编程详解

来源:百度文库 编辑:中财网 时间:2024/04/28 19:51:58

POWER5™ 处理器是支持 PowerPC® 指令集的高性能处理器系列产品中的最新产品。此系列处理器中的第一款 64 位处理器就是 POWER3。Macintosh 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 位。

Debian、Red Hat、SUSE 和 Gentoo 均有一个或多个发布版支持 POWER5 处理器,只有 Red Hat Enterprise Linux AS、SUSE 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 的扩展记忆法。也就是说,它们实际上是一条更为通用的指令的特殊形式。例如,上述所有条件跳转指令实际上都是 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 的汇编语言编程应有了基本的了解。指令最初看上去可能有点麻烦,但习惯总会成自然。在下一期文章中,我们将介绍 PowerPC 处理器的各种寻址模式,说明如何更有效地将它们用于 64 位编程。第 3 篇文章将更全面地介绍 ABI,讨论有哪些寄存器、分别有哪些用途、如何调用函数并从函数返回,以及关于 ABI 的其他有趣内容。

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

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

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

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

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

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

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

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

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

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

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

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

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

指令格式的重要性

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

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

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

 

 

 

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

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

D-Form 指令格式

到 5 位

操作码

到 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 指令格式

到 5 位

操作码

到 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。