清单 3. 使用 X-Form 寻址的例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #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. 通过全局偏移量表来访问数据1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| ###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. 汇编、连接并运行代码1
2
3
4
5
6
7
8
9
10
11
| #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 段,并显式地进行声明,如下所示:
1
2
3
| .section .toc
name:
.tc unused_name[TC], initial_value
|
这会创建一个内容表入口。name 是在代码中引用它所使用的符号。initial_value 是初始化分配的一个 64 位的值。unused_name 是历史记录,现在在 ELF 系统上已经没有任何用处了。我们可以不再使用它了(此处包含进来只是为了帮助我们阅读遗留代码),不过 [TC] 是需要的。
要访问内容表中直接保存的数据,我们需要使用 @toc 来引用它,而不能使用 @got。@got 仍然可以工作,不过其功能也与以前一样 —— 返回一个指向值的指针,而不是返回值本身。下面看一下这段代码:
清单 6. @got 和 @toc 之间的区别1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| ### 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 段中定义的数字相加1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| ### 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. 加载和存储多个值1
2
3
4
5
6
7
8
9
10
11
12
| #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. 使用多个值的加法程序
带更新的模式大多数加载/存储指令都可以使用加载/存储指令最终使用的有效地址来更新主地址寄存器。例如,ldu 5, 4(8) 会将寄存器 8 中指定的地址加上 4 个字节加载到寄存器 5 中,然后将计算出来的地址存回 寄存器 8 中。这称为带更新 的加载和存储,这可以用来减少执行多个任务所需要的指令数。在下一篇文章中我们将更多地使用这种模式。
结束语有效地进行加载和存储对于编写高效代码来说至关重要。了解可用的指令格式和寻址模式可以帮助我们理解某种平台的可能性和限制。PowerPC 上的 D-Form 和 DS-Form 指令格式对于与位置无关的代码来说非常重要。与位置无关的代码允许我们创建共享库,并使用较少的指令就可以完成加载全局地址的工作。
本系列的下一篇文章将介绍分支、函数调用以及与 C 代码的集成问题。 |