这个例子是在过程调用的时候对局部变量使用了“&”,并且寄存器不足够存放所有的本地数据,这时候局部数据必须放到内存中。笔记中函数比较简单,不过个人觉得这个例子比较有代表性。笔记中汇编代码的操作数是S(源)在前,D(目的)在后。最近挺喜欢Linux的,笔记也用的gcc汇编,返回地址是8字节。这个可以比较"玩具化"一点有助于我这种新手理解,毕竟现阶段我的目的不是去写,以后会用MASM,微软vs自带的那个。
一、为了方便,笔记中代码标注中的rsp代表栈顶,即栈指针%rsp,R[[%rsp]]这个地址值。为了简化理解,也假设call_proc的栈帧很简单,没有寄存器保存的部分等等。
二、例子的C代码部分
被调用过程proc代码如下
void proc(long a1, long *a1p,
int a2, int *a2p,
short a3, short *a3p,
char a4, char *a4p)
{
*a1p += a1;
*a2p += a2;
*a3p += a3;
*a4p += a4;
}
过程call_proc代码如下
long call_proc()
{
long x1 = 1; int x2 = 2;
short x3 = 3; char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
return (x1+x2)*(x3-x4);
}
因为在调用函数call_proc中调用了proc,而proc需要8个参数,寄存器不足以存储这么多参数,并且对局部变量x1,x2,x3,x4使用了&,因为必须为他们产生地址。这个时候就需要使用栈帧来处理这些问题。结合汇编代码来分析。
三、调用函数生成的汇编代码
#这段主要工作就是给proc设置参数
call_proc:
subq $32,%rsp #先分配32字节栈帧,栈向前了,旧rsp-新rsp=32
movq $1, 24(%rsp) #存储1到&x1中,这个值占64位,即rsp+24指向的字节到rsp+31指向的字节这个空间
movl $2, 20(%rsp) #存储2到&x2中,这个值占32位,即rsp+20指向的字节到rsp+23指向的字节这个空间
movw $3, 18(%rsp) #存储3到&x3中,这个值占16位,即rsp+18指向的字节到rsp+19指向的字节这个空间
movb $4, 17(%rsp) #存储4到&x4中,这个值占8位,即rsp+17指向的这个字节
leaq 17(%rsp), %rax #创建&x4,存入寄存器%rax中
movq %rax, 8(%rsp) #设置&x4作为参数8,这个值占64位它在rsp+8指向的字节到rsp+15指向的字节这个空间
#这里注意一下现在栈帧中rsp+16指向的这个字节是空着的
movl $4, (%rsp) #设置4作为参数7,其实rsp+1指向的字节到rsp+7指向的字节这个7个字节属于空位
leaq 18(%rsp), %r9 #设置&x3作为参数6
movl $3, %r8d #设置3作为参数5
leaq 20(%rsp), %rcx #设置&x2作为参数4
movl $2, %edx #设置2作为参数3
leaq 24(%rsp), %rsi #设置&x1作为参数2
movl $1, %edi #设置1作为参数1
分配的32字节空间大致意思如下,画了一个粗糙的图
调用函数call_proc的栈帧草图是这样的,下图中的返回地址不是call proc以后push进栈的那个返回地址,而是call_proc上一层的返回地址,画的不是特别好哈哈。当然也是假设没有寄存器保存的部分。
接着上面的call_proc的汇编代码
#下面就要开始调用proc了
call_proc:
call proc #调用proc后,返回地址会被push进栈,所以此时rsp会-8
#调用结束返回到call_proc,进行了rsp+8,此时rsp又回到了调用前的值
movslq 20(%rsp), %rdx #读取到x2的值,并将其符号扩展到long
addq 24(%rsp), %rdx #这步是在计算 x1+x2
movswl 18(%rsp),%eax #读取x3,并进行符号扩展到int
movsbl 17(%rsp),%ecx #读取x4,并进行符号扩展到int
subl %ecx, %eax #这步是计算x3-x4
cltq #将%eax符号扩展%rax
imulq %rdx, %rax #计算(x1+x2)*(x3-x4)
addq $32, %rsp #清栈
ret #return
到此call_proc就结束了
四、扩展下proc被调用时程序计数器pc跳转到proc代码的部分
#call_proc为被调用的proc做的参数准备工作
#参数1 在 %rdi中 (64位)
#参数2 在 %rsi中 (64位)
#参数3 在 %edx中 (32位)
#参数4 在 %rcx中 (64位)
#参数5 在 %r8w中 (16位)
#参数6 在 %r9中 (64位) 由于寄存器放不开了,就要把参数7和参数8放入栈中
#参数7 在 R[%rsp]+8 (8位)
#参数8 在 R[%rsp]+16 (64位) 这里因为call之后返回地址被压入栈中,栈指针向前了8字节(64位)
#所以对于参数7和参数8的地址相对于之前都要加8
proc :
movq 16(%rsp), %rax #先取得参数8
addq %rdi, (%rsi) #进行*a1p += a1 对应的就是*(&x1) 和 x1的操作
addl %edx, (%rcx) #进行*a2p += a2 对应的就是*(&x2) 和 x2的操作
addw %r8w, (%r9) #进行*a3p += a3 对应的就是*(&x3) 和 x3的操作
movl 8(%rsp), %edx #取得参数7
addb %dl, (%rax) #进行*a4p += a4 对应的就是*(&x4) 和 x4的操作
ret
#调用proc结束,通过call proc时候push进栈的那个返回地址程序计数器pc回到了call_proc代码中call proc后面那个指令,也就是说把此时栈顶的返回地址pop %rip
调用proc结束,可以看到proc不需要额外分配栈帧空间。利用call_proc通过call proc指令调用proc的时候push进栈的那个返回地址,程序计数器pc回到了call_proc代码中call proc后面那个指令,{ movslq 20(%rsp), %rdx #读取到x2的值,并将其符号扩展到long }这里,也就是说此时栈顶的返回地址已经被pop %rip了,当然rsp也随之进行了+8。
总结
当寄存器不足够存放所有的本地数据,这时局部数据要放到内存中,这样才能为被调用过程实现传递数据。有很多函数其实根本用不上栈帧,比如一些叶子程序当寄存器足够处理本地所有变量的时候,并且这个过程没有调用其他过程。学习计算机系统的时候个人感觉没必要去死记一些栈帧的结构,要理解搞清楚原理是最重要的。很多东西实现起来都是活的并不是一成不变的,理解了栈帧的结构自然就熟记在心。晚一些整理下x86寄存器的局部存储,主要关于那些被调用者保存寄存器。