知识点
push : -= 8 , 存 pop : 弹, += 8
所以当前的rsp表示栈顶元素的地址.
call label:: 记录返回地址 + 跳转
1. push rip // 程序计数器入栈 (返回地址)
2. jmp lable // 跳转
ret: pop rip (将栈顶元素弹到rip寄存器,就回到了原来的程序继续执行)
leave: 修改rsp + 还原rbp
1. mov %rbp , %rsp
2. pop %rbp
实践分析
源代码:
# include <stdio.h>
int mul(int a,int b)
{
return a * b;
}
int add(int a,int b)
{
int c = a + b;
int d = c * mul(c,c);
return d;
}
int main()
{
int a = 1;
int b = 2;
int c = 3;
add(1,2);
}
汇编分析:
int main()
{
1178: f3 0f 1e fa endbr64
# {rbp,rsp} : {0,0x7fffffffde18}
117c: 55 push %rbp
# {0,0x7fffffffde10}
117d: 48 89 e5 mov %rsp,%rbp
# {0x7fffffffde10, 0x7fffffffde10}
# 此时栈顶存着rbp寄存器, %rbp=%rsp
1180: 48 83 ec 10 sub $0x10,%rsp
# {0x7fffffffde10, 0x7fffffffde00}
# 预留0x10的局部变量,所以rsp直接-=0x10
int a = 1;
1184: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
int b = 2;
118b: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp)
int c = 3;
1192: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp)
add(1,2);
1199: be 02 00 00 00 mov $0x2,%esi
119e: bf 01 00 00 00 mov $0x1,%edi
# {0x7fffffffde10, 0x7fffffffde00}
11a3: e8 98 ff ff ff callq 1140 <add>
************************* <add开头> *************************
int add(int a,int b)
{
1140: f3 0f 1e fa endbr64
# {0x7fffffffde10, 0x7fffffffddf8}
# 执行call指令,会将返回地址pop到栈顶,所以rsp -= 了8
1144: 55 push %rbp
# {0x7fffffffde10, 0x7fffffffddf0}
# 此时push进去的rbp是上一个栈帧(main)的rbp
1145: 48 89 e5 mov %rsp,%rbp
# {0x7fffffffddf0, 0x7fffffffddf0}
# 现在的rbp是记录存rbp值的那个栈地址
1148: 48 83 ec 18 sub $0x18,%rsp
# {0x7fffffffddf0, 0x7fffffffddd8}
114c: 89 7d ec mov %edi,-0x14(%rbp)
114f: 89 75 e8 mov %esi,-0x18(%rbp)
int c = a + b;
1152: 8b 55 ec mov -0x14(%rbp),%edx
1155: 8b 45 e8 mov -0x18(%rbp),%eax
1158: 01 d0 add %edx,%eax
115a: 89 45 f8 mov %eax,-0x8(%rbp)
int d = c * mul(c,c);
115d: 8b 55 f8 mov -0x8(%rbp),%edx
1160: 8b 45 f8 mov -0x8(%rbp),%eax
1163: 89 d6 mov %edx,%esi
1165: 89 c7 mov %eax,%edi
# {0x7fffffffddf0, 0x7fffffffddd8}
1167: e8 bd ff ff ff callq 1129 <mul>
******************* <mul开头> ****************
int mul(int a,int b)
{
1129: f3 0f 1e fa endbr64
# {0x7fffffffddf0, 0x7fffffffddd0} 此时栈顶0x7fffffffddd0存着返回地址
112d: 55 push %rbp
# {0x7fffffffddf0, 0x7fffffffddc8} 此时栈顶0x7fffffffddc8存着rbp的值
112e: 48 89 e5 mov %rsp,%rbp
# {0x7fffffffddc8, 0x7fffffffddc8}
1131: 89 7d fc mov %edi,-0x4(%rbp)
1134: 89 75 f8 mov %esi,-0x8(%rbp)
return a * b;
1137: 8b 45 fc mov -0x4(%rbp),%eax
113a: 0f af 45 f8 imul -0x8(%rbp),%eax
}
# {0x7fffffffddc8, 0x7fffffffddc8}
113e: 5d pop %rbp
# {0x7fffffffddf0, 0x7fffffffddd0}
# 由于这里没有让rsp-=某个数,而是直接用rbp来操纵栈内存,所以rsp存着 记录原来rbp的栈地址,所以这个pop 就恢复了rbp的值,此时的栈顶记录着返回地址,所以retq 就可以返回那个地址啦!
113f: c3 retq
# {0x7fffffffddf0, 0x7fffffffddd8}
****************** <mul结尾> ****************
116c: 8b 55 f8 mov -0x8(%rbp),%edx
116f: 0f af c2 imul %edx,%eax
1172: 89 45 fc mov %eax,-0x4(%rbp)
return d;
1175: 8b 45 fc mov -0x4(%rbp),%eax
}
# {0x7fffffffddf0, 0x7fffffffddd8} rbp没有变,存着的是 记录rbp的栈地址
1178: c9 leaveq
# {0x7fffffffde10, 0x7fffffffddf8}
# 1.rsp=rbp (rbp中存着记录rbp的栈地址)
# 2.pop rbp: 这个pop就是恢复rbp的值
1179: c3 retq
# {0x7fffffffde10, 0x7fffffffde00}
# 此时栈顶是返回地址,所以pop后就可以回到上一个函数继续执行
************************* <add结尾> *************************
11a8: b8 00 00 00 00 mov $0x0,%eax
11ad: c9 leaveq
11ae: c3 retq
# 有了上面的分析,也就懂了这个leaveq+retq的就是修改rsp到那个位置,恢复rbp和返回地址
11af: 90 nop
总结
进入一个函数,他的栈帧如下:
-
当我们call的时候,会将返回地址放在栈顶,然后将rip寄存器指向对应的lable去执行那里的代码.
-
进入一个函数后,我们通常会直接执行
push %rbp来保存原来的rbp的值 -
我们有两种策略来完成栈帧的创建与恢复
-
让rbp入栈,且保证rbp不会改变型(用leave命令来恢复):
由于rsp会改变,所以我们在进入函数后先把 记录rbp的栈地址存到rbp寄存器中,然后在函数ret之前执行一个leave(来将rsp调到rbp的位置,然后pop rbp来恢复rbp),然后执行ret来回到原来的执行流去执行
以main函数为例:
117e: 55 push %rbp # rbp入栈 117f: 48 89 e5 mov %rsp,%rbp # 将这个栈内存存到rbp 1182: 48 83 ec 10 sub $0x10,%rsp # 减少的是rsp,而rbp不变,所以后面就可以用rbp寄存器的值来找到这个栈内存 ......................................... 11af: c9 leaveq # rsp=rbp,pop rbp,恢复了rsp和rbp 11b0: c3 retq # 回到原来的执行流如果我们的这个函数还会调用别的函数的话,那就要采用这种方式,rbp来记录那个栈地址,保证rbp不会变, 然后ret前通过这个rbp来恢复rbp和rsp.
以add函数为例:
1144: 55 push %rbp # 保存main函数的rbp 1145: 48 89 e5 mov %rsp,%rbp ...................................................... 1178: c9 leaveq # 恢复rsp,恢复main函数的rbp 1179: c3 retq可以看到,add函数会在进入后保存main函数的rbp,在退出前恢复main函数的rbp,这样来保证main函数的那个rbp不会被变掉.
所以,其实这个保证正确性的逻辑是,每个函数都遵守这个保存前一个函数的rbp的规则,那么:
假设当前是函数A, 如果它的rbp不会改变,函数A就会正确执行.
比如函数A调用函数B,由于函数B会遵守这个规则,所以调用函数B前后的rbp不会改变,所以函数A的rbp不会改变,所以函数A是正确的,递归到函数B....所有函数都是正确的
其实仅仅用这种策略就已经可以保证正确性了,但是可以发现,最后一个不会再去调用别的函数的那个尾函数,其实可以不用leave去利用rbp来恢复rsp,而是干脆就不用rsp来使用栈内存,那么rsp就一直指向 存着rbp的那个栈地址,这样就可以成功完成恢复rbp和rsp的任务,具体见2
-
不改变rsp的值,而是直接去操纵内存
mul函数:112d: 55 push %rbp 112e: 48 89 e5 mov %rsp,%rbp 1131: 89 7d fc mov %edi,-0x4(%rbp) 1134: 89 75 f8 mov %esi,-0x8(%rbp) 1137: 8b 45 fc mov -0x4(%rbp),%eax 113a: 0f af 45 f8 imul -0x8(%rbp),%eax 113e: 5d pop %rbp 113f: c3 retq可以看到,这里的rsp一直就是 存着rbp的那个栈地址,所以用rsp可以直接pop rbp和回到返回地址
-
总结:这两种型号:
- 第一种是通过让rbp来记录那个栈地址,并保证rbp寄存器不会改变,这样就可以成功找到那个栈地址了
- 第二种是让rsp来记录那个栈地址,并保证rsp不会改变,这样就可以成功找到那个栈地址了
可以明显的发现他们的相同点就是,都是去维持下面的栈帧结构
![]()
在返回前,都是让rsp回到 保存原来的rbp 那个地址,从而恢复rbp,rsp,以及返回