AArch64教程第七章
在本系列的前面几章,我们看到了怎么修改我们程序的顺序。今天我们会看到我们怎么通过分支的方式重复使用指令。让我们说一说函数。
例程(routine)
在用计算机解决一个问题的过程中,我们会碰到一些步骤,这些步骤是反复使用的。这些步骤可能是某个算法的一部分,而这些步骤是能够用指令编码的。这也就意味着我们可能会用一些指令,而这些指令的用途是一样的。如果我们能够找出这些指令,并且把这些指令放在一个位置上,并且当需要的时候使用它们。这就是一个例程(routine)的基本观点。我们在今日很少使用routine这个单词并且大部分的程序语言都用另外的名称,例如function, procedures, subroutines, methods, lambda表达式等等。当然,它们之间还是有些不同,但是它们都包含了代码复用的观点。我会用最常用的术语——function。
使用一个函数
函数能够被用于控制,例如值(举例,一个整型值)并且我们能够在函数内部做一些操作。在这个层面上,尽管,我们只关心2个部分:
-
获得函数的地址,对于地址而言,我们一般使用一个label表示
-
调用函数,这一般对于我们而言比较有趣
函数地址
函数是我们复用的一系列指令的序列。我们确定函数的方式是通过使用第一条指令的地址。大部分时间,一个label会被标明出函数的地址。
调用一个函数
调用一个函数是一个过程,该过程在实际操作中意味着传递一些数据到该函数,跳转到该函数的地址。当函数结束,它会回到调用点。
调用一个函数的核心是跳转分支,但是这是一个特殊的常用分支,因此,有必要为此单独设置一个指令。在AArch64中,这个指令就是bl
,其意思是branch和link。它是一个无条件分支,该分支的作用是设置x30
寄存器中的值是下一条指令的地址。回忆一下,x30
是一个通用目的寄存器,但是在本例中,我们给予它特殊的意义:它包含了函数结束之后的地址。历史原因,当x30
寄存器被用于此目的时,它被称为链接寄存器。
从函数返回后,唯一我们叫做的事情就是跳转到x30
寄存器中的的地址。有一个指令能够无条件跳转到保存在寄存器中的地址,叫br
。所以,调用一个函数和返回一个函数的过程如下。
.text
my_function:
br x30
caller:
bl my_function
// more instructions ...
但是从一个函数返回是一个通用操作,所以,我们直接用ret
,而不用br x30
。
.text
my_function:
ret
caller:
bl my_function
// more instructions ...
参数传递
OK,所以,我们知道了调用一个函数和返回一个函数的基本知识。但是,在这一层,唯一我们要做的事情就是复用这个指令序列。一般,我们想给函数传递参数。例如,我们想用一个函数计算两个给定数字的平均值。这也意味着当调用函数我们需要一种机制传递参数到这个函数。
在这点上,我们可以考虑几个方法。第一是使用全局变量,在这些变量中我们首先放入值然后函数会读这些值并且把结果放入其他的全局变量。这种方式是一些早期语言的工作方法。这个问题本身存在机制性问题,因为它会递归甚至更差,它不适合于多线程环境。在现代环境中,这种方式很少用了。
另外一种方式是,我们用私有内存,私有内存是指我们只在调用函数的时候使用。这时我们采用一些栈方式:我们能在栈顶放入或者移除一些东西(但是我们能够访问在栈顶的下面几个顺序元素)。在我们调用一个函数之前,我们把参数放入栈顶。该函数能够访问栈顶并且在下面的元素就是参数。结果能够放在栈中,例如,函数能够用结果值替换参数,所以,调用者只是再一次检查栈顶。这种技术对于递归也是工作得比较好的并且对于多线程也是比较好的。这就是用在很多编程语言中的技术和在那些缺少寄存器的架构中(例如32位x86)
其实有一种混合的方法,在大多数的RISC架构中,加入了一些寄存器用于传递参数并且如果我们耗尽了它们,就会使用一个如上述的栈方法。这种方式工作得很好,因为大多数函数都使用少于4个参数。并且假设在AArch64架构中,我们大约有30个寄存器,那么拿出几个寄存器作为参数传递就说得通了。那么是哪些寄存器呢?嗯~,这其实是一个带点传统意味的事情,并且作为传统,它应该有传承性。
我们能够自定义,但是在AArch64中,已经有了一个函数调用标准(或者简称为PCS)。这时一个包含了很多细节的非常长的文档。这个系列的目的并不是将这个,因此我们简化该文档的内容:
x0
-x7
被用来传递参数和返回值。这些寄存器的值可能会被调用函数自由修改(callee)所以调用者会忽略它们中的内容。即使它们不被用来传递参数和返回值。这也意味着它们在实际应用中是调用者保存寄存器。x8
-x18
是对每个函数而言是临时寄存器。对于函数而言,它不管它们里面的值,因此,在实际过程中,它们是调用者保存寄存器。x19
-x28
寄存器是被调用者保存寄存器,即它们被函数调用之前应保存,在返回之后应该恢复- 我们已经知道x30是链接寄存器并且它的值必须被保存直到函数使用
ret
指令返回到调用者。
这也意味着我们能传递8个参数x0
到x7
。如果我们正在传递一个64位整型或者一个地址,我们将使用对应的x*i*
寄存器。对于32位整型,我们将使用对应的w*i*
(我们不会打包2个32位整型到一个单独的64位寄存器中)
如果我们必须传递超过8个参数呢?我们怎么保持x19
到x28
并且更重要的是,我们怎么保持x30
?嗯~,在这种情况下,我们必须使用栈,但是我们将在其他章节阐述。在本章,我们将使用全局变量临时地保存x30
。
Say hello!
好了,有了这些知识,我们限制能够做一些有意思的例子了。作为今天的开始,我们会say hello。我们能够用函数C库的puts
。这个函数只接收一个参数:一个地址到一个空结束的字符串。
.data
.balign 8
/* This is the greeting message */
say_hello: .asciz "Hello world!"
.balign 8
/* We need to keep x30 otherwise we will not be able to return from main! */
keep_x30: .dword 0
.text
/* We are going to call a C-library puts function */
.globl puts
.globl main
main:
ldr x0, addr_keep_x30 // w0 ← &keep_30 [64]
str x30, [x0] // *keep_30 ← x30 [64]
ldr x0, addr_say_hello // w0 ← &say_hello [64]
bl puts // call puts
ldr x0, addr_keep_x30 // w0 ← &keep_30 [64]
ldr x30, [x0] // x30 ← *keep_30 [64]
mov w0, #0 // w0 ← 0
ret // return
addr_keep_x30 : .dword keep_x30
addr_say_hello: .dword say_hello
第三行的标记使我们确认了下一个由汇编器发送的数据对一个地址的8字节(64位)对齐。在这个例子中,我们希望汇编器发送一个没有结尾的字符串Hello world!
。我们使用指令.asciz(第五行)。为了在后面使用这个字符串,我们设置标签say_hello
用于这个字符串的地址。
因为我们不会在本章看到栈的使用,我们仍然会保存x30
的值。所以,我们为它分配一些存储空间。再一次,我们想要这个空间与8个字节对齐。所以,我们再一次使用.balign
指令(第七行)。然后我们定义这个存储空间,然后我们标记这个存储空间为keep_x30
,所以我们能够在后面引用它。正如我们所知道的,.dword
指令会按照64位整型发送特殊的整型值。这就是我们这个小程序的数据段。
在第14行,我们说我们在用puts
。这时一个在C语言库中定义的函数,所以我们用.globl(第十四行)去声明这是一个全局符号(相对于私有变量)。正如我们所知道的,我们需要在17行为main函数做同样的事情。
现在检查一下30行和31行。在这里定义了一个say_hello
和keep_x30
的内存地址。你回顾一下第五章,这是因为我们需要将这个地址保存,以加载指令。
现在,回到第18行,在这里,我们把keep_x30
的地址加载到x0
中。现在我们能使用这个地址去保存x30
寄存器(第19行)。
现在,我们已经保存x30了,我们能够调用puts
。首先,我们需要准备符合规范的函数调用。函数puts
是一个在C语言库中的函数,这个函数只能接受一个地址作为参数,这个地址是一个空结尾的字节缓存。精确来讲,就是我们在say_hello
中的值。因为puts接受的是地址,不是内容本身,我们将使用addr_say_hello
。如上所述,第一个参数放在x0
,所以我们只加载say_hello
的地址,这个地址在x0
中,21行。
现在,每件事情都准备好了,那么我们开始在22行调用puts
。如果所有都是正确的,我们的程序会在下一个调用指令继续(第24行)。在这里,我们简单的保存x30
的值,因为在22行的bl
指令把这个寄存器里的值覆盖了。基本上,我们再一次加载keep_x30地址,然后我们在24-25行做一次x30寄存器的加载。现在每件事都在位置准备返回了,所以我们设置w0为0(第27行),然后我们调用ret(第28行)。
如果我们尝试运行这个程序,我们会看到
$ ./hello
Hello world!
耶! 😃
今天就到这里!