本章所说的程序/过程(procedure),它即可以是函数过程,也可以是面向对象编程中的方法,这些大致相同的东西,我们统一使用术语程序(procedure)来称呼。
ABI: Application Binary Interface, 应用程序二进制接口。
它要求所有Linux程序,所有的编译器,所有的操作系统,系统的所有不同部分,都需要对如何管理机器上的资源有一些共同的理解,并共同遵守这套规则。
因此,它超越了硬件实际提供的软件标准。
它是机器程序级别的接口。
传递控制:我们需要将控制权转移给一个函数并确保它能返回正确的位置。
当P调用Q时,程序必须以某种方式跳转到Q中,并开始执行Q的代码。然后当Q执行到它的退出点,程序需要以某种方式回到P。
不是返回到P的任何地方都可以,必须恰好在P调用Q后的位置。
因此为了返回正确的位置,我们需要记录返回位置的信息。
传递参数:我们如何传递参数?
Q是一个函数,它接受单个参数i并能在函数内部使用这个参数,在P调用它的地方,P给Q传递了一个称为x的值,所以x的值必须以某种形式记录下来,使的在Q内,程序有权访问其他信息。
类似的,当Q想要返回有一个值时,P也将用相同的方式利用该值。
函数中的局部数据:需要分配一些空间
那么在哪里分配这些空间?如何确保正确分配?如何确保空间被正确释放?
将程序分解成更小的函数,尽可能减少过程调用的开销。
在好的编程风格中,你写的函数往往专注于很小的功能。
设计者的原则之一是:只做绝对必要的事情
如果数据不需要本地存储空间则不要分配和释放空间了。
如果你没有必要传递任何值,那就不要传递它们。
前景问题:如何将控制传递给一个函数? 栈
栈并不是什么特殊的内存,栈并不是特殊的内存,它只不过是普通内存的一个区域。
对于汇编层面的程序员而言,内存只是一个巨大的字节数组。在那一堆自己中的某个地方,我们将其称为栈。
程序用栈来管理过程与返回的状态。
在栈中,程序传递潜在信息,控制信息和数据,并分配本地数据。栈可以用于管理这些信息的原因在于栈这种数据结构,符合过程调用和返回的整个想法的实质。
当你调用时,可能需要一些信息,但是当你从调用中返回时,所有这些信息可以被丢弃,因此它利用了栈的那种后进先出的原则,这与调用与返回的思想十分吻合。
%rsp
包含最低栈地址。也就是最上面的元素(最先出栈的元素)每次在栈上分配更多空间时,都会通过递减该指针来实现。
pushq Src
popq Dest
拓展:
弹出数据之后,寄存器%rsp地址加8,增加栈指针来释放空间,并不意味着我神奇的抹去了什么,仅仅移动了栈指针而已,原来的栈顶元素热然保存在内存中,只是不再时栈中的一部分了。
指令call与ret使用了栈push与pop相同的思想。
long mult2(long a, long b)
{
long s = a * b;
return s;
}
void multstore(long x,long y,long *dest)
{
long t = mult2(x, y);
*dest = t;
}
0000000000400540 <multstore>:
400540: push %rbx # Save %rbx
400541: mov %rdx,%rbx # Save dest
400544: callq 400550 <mult2> # mult2(x,y)
400549: mov %rax,(%rbx) # Save at dest
40054c: pop %rbx # Restore %rbx
40054d: retq # Return
0000000000400550 <mult2>:
400550: mov %rdi,%rax # a
400553: imul %rsi,%rax # a * b
400557: retq # Return
如图:
调用call:此时栈指针(%rsp)指向0x120, 指令指针(%rip)指向call指令。
运行mult2: 栈指针(%rsp)-8,指向0x118。指令指针(%rip)指向mult2中的程序。
mult2返回:指令指针(%rip)指向mult2中的ret。
调用函数之后,程序恢复执行:栈指针(%rsp)+8,指向0x120。指令指针(%rip)指向multstore中的程序。
拓展:
0x120 - 8 = 288 - 8 = 280 = 0x118
%rip: register instruction pointer, 指令指针寄存器。
它存储了当前正在执行的指令的内存地址。
当处理器执行程序时,它会按顺序读取内存中的指令并执行它们。%rip 寄存器指示了下一条要执行的指令的内存地址。当处理器执行完一条指令后,它会自动更新 %rip 寄存器,使其指向下一条要执行的指令的地址。
%rip 寄存器在程序执行期间是自动更新的,程序员无法直接修改它的值。
前面六个参数存储在寄存器中:
...
Arg n
...
Arg 8
Arg 7
返回值:
%rax
基本上代码能运行基于这样的假设:无论什么参数都按列出的顺序被传递给这一系列寄存器。
并且代码显然是依赖于这些假设。类似的,返回值的处理方式也是传递给指定的寄存器%rax。
以上面的例子为例:
# x in %rdi, y in %rsi, dest in %rdx
# t in %rax
void multstore(long x,long y,long *dest)
{
long t = mult2(x, y);
*dest = t;
}
调用和返回的功能之一是你可以对函数进行嵌套调用。
执行特定程序时,它只需要引用该函数内部的数据或已传递给它的值。
我们可以再这个栈上分配这个当前函数需要任意多的空间,我们不需要保留与该函数相关的任何信息,返回时,之前被调用的函数就可以永远消失了。这就是为什么我们要用栈的思想。
在栈上分配空间,它们返回的时候退出栈,并释放空间。这些栈的规则完全适用,它与过程调用和返回思想完美匹配。
因此,我们把栈上用于特定call(特定函数)的每个内存块称为栈帧。
call
调用指令,推送数据(参数)ret
指令,弹出数据(返回数据)通常一个栈帧由两个指针分隔:栈指针, 基指针。
基指针式一个可选的指针,所以这个寄存器实际上并不会在你的过程中以帧指针的形式出现,所以通常情况西啊,你知道栈帧的唯一事情栈指针。因此,你甚至无法弄清楚栈帧的确切范围。你只知道栈的顶部式对应于最顶层函数的顶部栈帧。
每次开始调用一个函数时,栈上就会为它的栈帧分配一些空间。然后该栈帧的位置由一个指针或两个指针指示。
栈指针寄存器 %rsp : register stack pointer. 用于指向当前栈顶的位置。
栈是一种用于存储临时数据和函数调用信息的数据结构。通过修改 %rsp 的值,可以在栈上分配和释放内存空间。
基址指针寄存器 %rbp :用于指向当前函数的基址。基址指针通常用于访问函数的局部变量和参数。通过 %rbp,可以在栈帧中定位和访问这些变量和参数。
每层调用都会由自己管理局部状态,这样栈的原则再次保证了它能正确的工作。
call
指令推送数据。long incr(long *p, long val)
{
long x = *p;
long y = x + val;
*p = y;
return x;
}
long call_incr()
{
long v1 = 15213;
long v2 = incr(&v1, 3000);
return v1 + v2;
}
incr:
movq (%rdi), %rax
addq %rax, %rsi
movq %rsi, (%rdi)
ret
call_incr:
subq $16, %rsp
movq $15213, 8(%rsp)
movl $3000, %esi
leaq 8(%rsp), %rdi
call incr
addq 8(%rsp), %rax
addq $16, %rsp
ret
movl $3000, %esi
: 因为3000足够小,所以适用movl指令就行了。%esi寄存器,高32位会设置为0。编译器喜欢这么干的原因是movl比movq少一个字节。
%rax
返回值
调用者保存
可以被程序修改。
%rdi,…,%r9
参数
调用者保存
可以被程序修改
%r1o,%r11
调用者保存
可以被程序修改。
这些寄存器也经常被作为临时寄存器。
%rbx,%r12,%r13,%r14
被调用者保存
被调用者必须保存和恢复
%rbp
被调用者保存
被调用者必须保存和恢复
可能被用作栈帧指针
可以混搭
%rsp
特殊形式被调用者保存
退出程序后恢复为原始值
long pcount_r(unsigned long x)
{
if (x == 0)
return 0;
else
return (x & 1) + pcount_r(x >> 1);
}
pcount_r:
testq %rdi, %rdi
jne .L8
movl $0, %eax
ret
.L8:
pushq %rbx
movq %rdi, %rbx
andl $1, %ebx
shrq %rdi
call pcount_r
addq %rbx, %rax
popq %rbx
ret
每个程序都会以这种方式处理%rbx, 即修改之前将它先暂存在栈上。
《深入理解计算机系统》学习笔记 - 第一课 - 课程简介
《深入理解计算机系统》学习笔记 - 第二课 - 位,字节和整型
《深入理解计算机系统》学习笔记 - 第三课 - 位,字节和整型
《深入理解计算机系统》学习笔记 - 第四课 - 浮点数
《深入理解计算机系统》学习笔记 - 第五课 - 机器级别的程序
《深入理解计算机系统》学习笔记 - 第六课 - 机器级别的程序二
《深入理解计算机系统》学习笔记 - 第七课 - 机器级别的程序三