C语言和汇编语言函数调用

C语言和汇编语言函数调用关系

1.汇编语言函数调用

X86结构中,cs寄存器和rip寄存器共同控制着CPU要执行的下一条指令(当前在不同的模式中控制方式不同,如:实地址2模式和保护模式,长模式等),一般会按照指令在内存中存储的顺序,依次执行。如果想要在普通程序(除去系统调用和中断)执行中跳转到某一条指令,就需要使用JMPCALLRET及其变种指令。

  • jmp指令是无条件跳转指令,直接跳转到某条指令。
  • call指令在执行时,会先将下一条指令地址压栈,然后跳转到目标指令。
  • ret指令执行时,会从当前栈顶弹出目标指令的地址,然后跳转到目标指令。

利用上述相关指令进行搭配,就可以实现在一块汇编代码(a)执行中,跳转到另一块汇编代码(b),执行完(b)后返回到(a)的下一条指令继续执行;从而可以实现函数的调用和返回。

2.c语言函数调用

通过分析c语言编译->反汇编后汇编代码,可以看出c语言的调用方式,注意不同的编译环境调用机制可能不一样,实验环境是gcc 4.4.7

1.测试代码
int add(int a,int b){
    int c = a+b;
    return c;
}

void main(){
    int i = 0;
    int j = 1;
    add(i,j);
}
2.反汇编后的汇编代码

栈的生长方向是由高地址到低地址,即压栈时(pop),rsp寄存器会减小;出栈时(push),rsp寄存器会增大。

leaveq等效于movq %rbp, %rsp; popq %rbp;这条指令的主要为了对应函数入口处的push %rbp;mov %rsp,%rbp。

000000000000009a <add>: ;add函数机器码和对应的汇编代码(64位)
; 地址   机器码                   汇编代码
  9a:	55                   	push   %rbp
  9b:	48 89 e5             	mov    %rsp,%rbp
  9e:	89 7d ec             	mov    %edi,-0x14(%rbp)
  a1:	89 75 e8             	mov    %esi,-0x18(%rbp)
  a4:	8b 45 e8             	mov    -0x18(%rbp),%eax
  a7:	8b 55 ec             	mov    -0x14(%rbp),%edx
  aa:	8d 04 02             	lea    (%rdx,%rax,1),%eax
  ad:	89 45 fc             	mov    %eax,-0x4(%rbp)
  b0:	8b 45 fc             	mov    -0x4(%rbp),%eax
  b3:	c9                   	leaveq 
  b4:	c3                   	retq   

00000000000000b5 <main>: ;main函数机器码和对应的汇编代码(64位)
; 地址   机器码                   汇编代码
  b5:	55                   	push   %rbp
  b6:	48 89 e5             	mov    %rsp,%rbp
  b9:	48 83 ec 10          	sub    $0x10,%rsp
  bd:	c7 45 f8 00 00 00 00 	movl   $0x0,-0x8(%rbp)
  c4:	c7 45 fc 01 00 00 00 	movl   $0x1,-0x4(%rbp)
  cb:	8b 55 fc             	mov    -0x4(%rbp),%edx
  ce:	8b 45 f8             	mov    -0x8(%rbp),%eax
  d1:	89 d6                	mov    %edx,%esi
  d3:	89 c7                	mov    %eax,%edi
  d5:	48 b8 9a 00 00 00 00 	mov    $0x9a,%rax
  dc:	00 00 00 
  df:	ff d0                	callq  *%rax
  e1:	89 45 f8             	mov    %eax,-0x8(%rbp)
  e4:	c9                   	leaveq 
  e5:	c3                   	retq   

分析上述反汇编后的汇编代码。

main函数和add函数的入口部分代码和出口部分代码都是:

;函数入口
push %rbp   ;保存rbp的值
mov %rsp,%rbp ;将当前栈顶数值(a)->rbp寄存器
;中间部分,不会改变rbp寄存器的值
;函数出口
;以下2条汇编代码等同于leaveq
mov %rbp,%rsp ;将rbp寄存器的数值(a)->rsp,现在栈顶的数就是入口保存的rbp值
popq %rbp ;将栈顶值恢复到rbp寄存器

可以得出所有c语言函数的入口和出口部分都会保存和恢复rbp的值,并且在保存rbp旧值到当前栈顶后,会将当前栈顶赋值给rbp;阅读函数中间部分汇编代码,发现不会改变rbp的值了,这样的话不管函数中间部分如何改变rsp的值(可能该函数会使用push或pop指令,或者使用call指令进行了压栈等等可以改变rsp寄存器的值的指令),最终都可以在函数出口时,将栈环境恢复到入口未执行时的栈环境(栈环境指:rsp和rbp寄存器的值,还有栈中的值)。

add函数中间部分:

mov    %edi,-0x14(%rbp) ;保存第一个参数的值
mov    %esi,-0x18(%rbp) ;保存第二个参数的值
mov    -0x18(%rbp),%eax
mov    -0x14(%rbp),%edx
lea    (%rdx,%rax,1),%eax
mov    %eax,-0x4(%rbp) ;保存局部变量c的值
mov    -0x4(%rbp),%eax ;保存返回值到eax

首先rbp在入口部分结束时,被赋予了新值(当前栈顶),其次因为栈时由高地址向低地址扩展;所以上述代码等于先将edi和esi的值保存到内存栈扩展方向的某2个位置上(rbp-0x14,rbp-0x18),然后将内存中这两个值读出来进行运算,完成后保存到内存栈扩展方向的另一个位置上(rbp-0x4);通过对比add函数的c语言代码,可以发现内存(rbp-0x14,rbp-0x18)位置最终保存该函数的2个输入参数,内存(rbp-0x4)位置,最终保存局部变量c;由此得出,寄存器edi,esi分别为add传入了2个参数,寄存器eax保存了返回参数。

观察main函数中间部分,来验证上述假设:

sub    $0x10,%rsp 		;rsp=rsp-0x10 使得在callq调用时,将下一条指令地址保存到(rsp-0x10)位置
movl   $0x0,-0x8(%rbp)	;保存第一个局部变量i的值0
movl   $0x1,-0x4(%rbp)	;保存第二个局部变量j的值1
mov    -0x4(%rbp),%edx	
mov    -0x8(%rbp),%eax	
mov    %edx,%esi		;将j的值保存到esi中(第二个参数)
mov    %eax,%edi		;将i的值保存到edi中(第一个参数)
mov    $0x9a,%rax       ;将add函数的入口地址保存到rax中

callq  *%rax            ;调用add函数(将下一条指令地址压栈,然后跳转到0x9a处执行,即跳到add函数执行第一条指令)
mov    %eax,-0x8(%rbp)  ;将返回值赋值给i,(rbp-0x8)位置代表变量i

当改变add函数的输入参数为一个float变量和一个int变量时:

int add(float a,int b){

    int c = a+b;

    return c;

}

查看反汇编后的汇编代码:

0000000000000027 <add>:
  27:	55                   	push   %rbp
  28:	48 89 e5             	mov    %rsp,%rbp
  2b:	f3 0f 11 45 ec       	movss  %xmm0,-0x14(%rbp)
  30:	89 7d e8             	mov    %edi,-0x18(%rbp)
  33:	f3 0f 2a 45 e8       	cvtsi2ssl -0x18(%rbp),%xmm0
  38:	f3 0f 58 45 ec       	addss  -0x14(%rbp),%xmm0
  3d:	f3 0f 2c c0          	cvttss2si %xmm0,%eax
  41:	89 45 fc             	mov    %eax,-0x4(%rbp)
  44:	8b 45 fc             	mov    -0x4(%rbp),%eax
  47:	c9                   	leaveq 
  48:	c3                   	retq   

可以发现函数会从xmm0寄存器获取输入的float参数。
add函数和mian函数返回时,都使用了下述指令:

retq 

该指令时是64位时,ret指令的变种。因为执行到该指令时,被调用函数的出口部分已经执行完毕,栈恢复到调用者刚进入时的环境,被调用者执行ret指令将会弹出当前rsp指向的栈顶的指令地址,然后跳转到该指令执行。所以在a函数调用b函数时,要确保跳转到b入口处时,当前rsp指向的栈顶保存了a函数在b函数返回后想要指行的指令地址。返回值保存到了寄存器eax中。

3.总结

如果将调用者mian函数callq指令和被调用者add函数的retq指令联系起来,如下:

00000000000000b5 <main>: ;main函数机器码和对应的汇编代码(64位)
; 地址   机器码                   汇编代码
  b9:	48 83 ec 10          	sub    $0x10,%rsp  ;将rsp栈指针赋予新值newRsp,相当于初始化了一个空栈newStack
  ;...........
  d5:	48 b8 9a 00 00 00 00 	mov    $0x9a,%rax
  dc:	00 00 00  
  df:	ff d0                	callq  *%rax	   ;将mian函数下一条指令地址(0xel)压到栈newStack,然后跳转到(0x9a)执行
  e1:	89 45 f8             	mov    %eax,-0x8(%rbp)	;add函数retq执行后,跳回到该指令继续执行main函数。
  ;...........
  e4:	c9                   	leaveq             ;mian函数执行完成后,恢复到main函数入口时的栈环境,同时销毁了newStack(rsp = rbp)
  ;...........   
000000000000009a <add>: ;add函数机器码和对应的汇编代码(64位)
; 地址   机器码                   汇编代码
  9a:	55                   	push   %rbp        ;将mian函数的rbp保存到新栈newStack
  9b:	48 89 e5             	mov    %rsp,%rbp   ;让rbp指向新栈newStack栈顶
  ;........................
  b3:	c9                   	leaveq             ;add业务代码执行完成,恢复到入口时的新栈newStack环境,此时新栈newStack中只保存了(0xel),即main下一条指令地址
  b4:	c3                   	retq               ;弹出0xel,并跳转到(0xel)执行,此时rsp=newRsp,恢复栈环境到mian函数未执行Call时。

当执行一个c语言函数X时,都会在执行到函数X的第一条指令前,初始化好一个栈(rsp减去一个栈单位的地址)是当前函数X的栈基址—减去一个栈单位,是因为在函数X退出时(ret)会额外在栈中弹出一个栈单位的数作为返回地址,因为ret指令也是在当前函数X中执行,因此函数X的栈基地址应该为函数X执行第一条指令前的(rsp减去一个栈单位的地址)

栈底部保存着函数的返回地址(即函数X执行完后,要返回到哪个地址处执行指令)和函数X入口时的rbp值,紧接着保存函数X会使用到的局部变量和函数参数。函数X如果有输入参数,则按照从左向右的顺序依次将通用寄存器RDI、RSI、RDX、RCX、R8和R9中的参数值保存到栈中;同时,寄存器XMM0~XMM7用来获取浮点参数,而RAX寄存器则用于保存函数的返回值;因为函数的所有参数还有局部变量都会被保存到该函数的栈中,在参数保存到栈中后,对于这些寄存器就可以随意使用,因为对于变量和参数的赋值都会直接通过进行。

函数X执行结束返回后,会使当前的rsp指向函数 X的栈基址
在这里插入图片描述

3.参考

AMD64 Architecture Programmer’s Manual, Volume 1: Application Programming

一个64位操作系统的设计与实现 第 2 章 环境搭建及基础知识

自制操作系统GitHub地址

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值