如何获取函数调用时传入的未知数量参数(含栈中剩余参数)?
1. 如何访问栈上传入的未知数量参数?
在x86-64的System V AMD64调用约定下,可变参数的传递规则是固定的:前6个参数靠%rdi、%rsi、%rdx、%rcx、%r8、%r9这6个寄存器传递,剩下的参数会被调用者依次压入栈中(栈向下增长,所以第7个参数的地址最靠近返回地址)。
要访问栈上的这些参数,你需要先在函数内建立好栈帧,然后通过基指针%rbp来定位:
ft_function: push %rbp ; 保存旧的基指针 movq %rsp, %rbp ; 让rbp指向当前栈顶,建立栈帧
此时栈帧的布局很清晰:
%rbp指向旧的%rbp值%rbp + 8是调用者通过call指令压入的返回地址%rbp + 16就是栈上的第7个参数,%rbp + 24是第8个,以此类推(每个参数占8字节,64位架构下的标准宽度)
如果想把前6个寄存器参数也统一放到栈上方便批量处理,直接在建立栈帧后依次压入即可:
push %rdi ; 第1个参数 push %rsi ; 第2个参数 push %rdx ; 第3个参数 push %rcx ; 第4个参数 push %r8 ; 第5个参数 push %r9 ; 第6个参数
这样栈顶开始就是第6到第1个参数,而栈上的第7个参数依然在%rbp + 16的位置,互不干扰。
2. 如何确定需要回溯多远才能获取所有参数?
首先得明确:CPU本身不知道你传了多少个参数,必须通过函数约定来确定总数——这就是为什么printf需要格式化字符串,或者有些可变参数函数会把参数个数作为第一个传入参数(比如C里的void func(int arg_count, ...))。你之前想通过%rbp和%rsp的差值来判断是行不通的,因为栈里还会包含函数内部的局部变量,这个差值根本无法直接对应参数数量。
另外你提到的「返回地址和基指针被压在参数之间」是个误解,实际的栈布局顺序是这样的:
调用者在执行call之前,会把第7及以后的参数依次压栈;call指令执行时,会把返回地址压入栈顶;进入你的函数后,push %rbp又会把旧基指针压到栈顶。所以参数的位置是在返回地址的下方(更低的内存地址),基指针和返回地址都在参数的上方,根本不会夹在参数中间。
知道参数总数后,获取所有参数的步骤就很明确了:
- 先处理前6个寄存器参数:要么直接用寄存器操作,要么压栈统一处理
- 对于剩下的
n-6个参数,从%rbp + 16开始,每次偏移8字节去访问下一个参数,直到取完所有参数为止
举个简单例子,如果总共有10个参数,那栈上的4个参数分别在%rbp+16(第7个)、%rbp+24(第8个)、%rbp+32(第9个)、%rbp+40(第10个)的位置。
内容的提问来源于stack exchange,提问作者grumps

