概述 #
procedures:面向过程编程中的函数、过程,或者,面向对象中的方法
abi(应用程序二进制接口): application binary interface。它要求所有Linux程序、编译器、操作系统、系统所有不同部分,都需要对如何管理机器上的资源有一些共同的理解。机器程序级别的接口。
- ABI关注的是二进制代码如何在实际硬件上执行和交互
- Windows、Linux 和 macOS 的 ABI(应用程序二进制接口)在核心设计上有显著差异,这导致它们的二进制程序通常无法直接跨平台运行。
控制(函数调用) #
- 将控制权转移给另一个函数,且函数执行完毕后返回控制权
- 如何传参,以及被调用函数如何将结果传给调用函数
- 被调用函数中的局部变量分配问题
- 尽量减小过程调用中的开销(对于程序员,良好的编程习惯: 尽量多功能多函数)
控制权转移 #
- stack(栈):其实只是普通内存的一块区域
- 对于汇编层面的程序员来说,内存只是一个巨大的字节数组
- 用栈来管理过程调用与返回的状态
- 利用栈后进先出的特点:调用函数时需要一些信息,从函数返回需要丢掉一些信息。
pop #
这是三个操作复合成一个操作(先读取,后递减栈指针,最后存储值)
栈的释放只是移动栈指针,没有其他特殊操作。数据还在内存中,只不过不是栈的一部分了。
push #
- 被push的参数,也必须是寄存器,或立即数。(因为不允许直接从内存传递数据到内存)
- 这也是个复合操作:获取操作数,递减栈指针(而且,内存读一个数据,数据的地址指的是它的(最)低地址),最后写入值
例子1 #
- call和ret引用了栈的思想
- 局部变量的存取通过 %rsp 偏移(%rsp-8,%rsp+8之类)实现,不会直接修改 %rsp
调用前将返回地址压入栈中,然后去执行函数,调用后从栈顶(%rsp)取出调用函数前存的下一条指令的地址,并将%rip(程序计数器[Program Counter, PC]设置为该值注意:在函数返回前,需通过 add $N, %rsp 或 leave 指令(相当于 mov %rbp, %rsp + pop %rbp)恢复[很重要!!] %rsp 到调用前的状态。
例子2(call) #
例子3(ret) #
传递数据 #
这里的参数说的是整型,浮点数是另一组
如果超过了6个,如下图,最后的那个参数(接近栈底)先分配
例子 #
无论是什么参数,都按列出的顺序被传递给这(上图)一系列寄存器
本地数据(local variable) #
- 栈帧:一种特别的内存分配模式(在当前函数分配他需要的空间)
- 给定任何时间内只有一个函数在运行(单线程)
- 把栈上用于特定call的每个内存块,成为栈帧
CallChainExample #
栈帧 #
- %rsp(栈指针),始终指向栈的顶部(即当前可用内存的最低地址),由 CPU 硬性维护。必须存在(不可省略)
- %rbp(基址指针/帧指针),通常用于标记当前栈帧的起始位置,便于访问局部变量和参数,可选(可被优化省略)
几个误区 #
- 误区:返回地址存储在 %rbp 中。事实:返回地址在栈中,由 %rsp 或 %rbp 的偏移量定位(如 8(%rbp))。
- 误区:每次访问局部变量都会改变 %rsp。事实:只有分配/释放栈空间时 %rsp 会变,访问变量是通过固定偏移(如 -4(%rbp))完成。
- 误区:%rbp 必须用于访问局部变量。事实:优化模式下编译器直接用 %rsp 偏移访问变量(省略 %rbp)。
例子 #
每调用一次函数,%rbp都会改变,如果分配了新变量,%rsp也会改变
系统限制了栈的最大深度,递归
可能耗尽空间
提问 #
既然%rbp是可选的,那么程序怎么知道如何释放空间?如何将栈重置回原来位置
- 编译器知道当它分配时,需要分配16(假设)个字节,那么它最终就会释放16个字节
- 如果分配的是可变大小的数组或内存缓冲区,会在这种情况下使用%rbp来实现这个目的
x86-64/LinuxStackFrame #
在函数开始之前,所有的信息都已经在栈中
例子1 #
该函数返回第一个参数原来的值,且第一个参数的值改为两参数之和。
例子2 #
- 这里分配了16字节而不是8字节,是因为一些约定要求内存地址保持对齐
- movl(4字节);movq(8字节);movl命令比movq命令,少一个字节表示(二进制表示)
关于寄存器的约定 #
- 如果调用者想要调用函数后再使用某寄存器的值,需要在调用函数前保存它
- 如果被调用者想要修改某寄存器,需要在修改前保存,返回前恢复它的值
x86-64 LinuxRegisterUsage #
调用者保存 #
允许被任意修改,无需恢复
如果下列寄存器在调用其他函数后还需用到原值,调用者自己要保存
被调用者保存 #
不允许被任意修改,如果被修改了,那么得在函数返回前恢复
如果下列寄存器需要被修改,那么修改前得保存,函数返回前得恢复
被调用者保存的示例 #
栈指针%rsp在退出函数前得恢复(被调用者保存)参数%rdi在调用其他函数前得保存(会被其他函数覆盖,调用者保存)- 普通寄存器%rbx被修改了(它是被调用者保存,修改前得被备份到栈中)
- 代码中,%rdi在函数执行后并没有被恢复(调用者保存,可以任意修改)
- %rsp和%rbx在函数返回前均被恢复了(被调用者保存,不能任意修改,如果修改了需要在函数返回前恢复)
总结 #
递归 #
- 编译器不特殊处理递归函数,与普通函数一样处理
- 计算该数的二进制表示有几个1
- testq b,a相当于计算 a&b
- %rbx是被调用者保存,%rdi是调用者保存
- 这里%rdi旧值存到了%rbx中,后面没有再用到%rdi了。虽然后面还会用到%rbx,但是%rbx寄存器每次在函数返回后都会被恢复
- 到最后发现,那些可能被重用的值,都在栈上了
- (%ebx)任何计算结果是32位(四字节)的计算,会把寄存器其余32位设置为0(高四字节)
- 逻辑移位:右移时高位补0,左移低位补0
- 算数位移:右移时高位补符号位(MSB),左移补0
- 对于c,只有
<<
和>>
概述 #
寄存器保存约定确保了函数之间避免出现相互摧毁彼此数据的情况
关于过程调用,你需要记住的就是栈原则