x86函数调用策略

412 阅读5分钟

知识点

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

总结

进入一个函数,他的栈帧如下:

image-20210911002334925
  • 当我们call的时候,会将返回地址放在栈顶,然后将rip寄存器指向对应的lable去执行那里的代码.

  • 进入一个函数后,我们通常会直接执行push %rbp来保存原来的rbp的值

  • 我们有两种策略来完成栈帧的创建与恢复

    1. 让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

    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不会改变,这样就可以成功找到那个栈地址了

可以明显的发现他们的相同点就是,都是去维持下面的栈帧结构

image-20210911002319843

在返回前,都是让rsp回到 保存原来的rbp 那个地址,从而恢复rbp,rsp,以及返回