C语言和汇编语言函数调用
C语言和汇编语言函数调用关系
1.汇编语言函数调用
X86结构中,cs寄存器和rip寄存器共同控制着CPU要执行的下一条指令(当前在不同的模式中控制方式不同,如:实地址2模式和保护模式,长模式等),一般会按照指令在内存中存储的顺序,依次执行。如果想要在普通程序(除去系统调用和中断)执行中跳转到某一条指令,就需要使用JMP
、CALL
、RET
及其变种指令。
- 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 %rbp9b: 48 89 e5 mov %rsp,%rbp9e: 89 7d ec mov %edi,-0x14(%rbp)a1: 89 75 e8 mov %esi,-0x18(%rbp)a4: 8b 45 e8 mov -0x18(%rbp),%eaxa7: 8b 55 ec mov -0x14(%rbp),%edxaa: 8d 04 02 lea (%rdx,%rax,1),%eaxad: 89 45 fc mov %eax,-0x4(%rbp)b0: 8b 45 fc mov -0x4(%rbp),%eaxb3: c9 leaveq b4: c3 retq 00000000000000b5 <main>: ;main函数机器码和对应的汇编代码(64位)
; 地址 机器码 汇编代码b5: 55 push %rbpb6: 48 89 e5 mov %rsp,%rbpb9: 48 83 ec 10 sub $0x10,%rspbd: 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),%edxce: 8b 45 f8 mov -0x8(%rbp),%eaxd1: 89 d6 mov %edx,%esid3: 89 c7 mov %eax,%edid5: 48 b8 9a 00 00 00 00 mov $0x9a,%raxdc: 00 00 00 df: ff d0 callq *%raxe1: 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 %rbp28: 48 89 e5 mov %rsp,%rbp2b: 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),%xmm038: f3 0f 58 45 ec addss -0x14(%rbp),%xmm03d: f3 0f 2c c0 cvttss2si %xmm0,%eax41: 89 45 fc mov %eax,-0x4(%rbp)44: 8b 45 fc mov -0x4(%rbp),%eax47: 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,%raxdc: 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保存到新栈newStack9b: 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地址
C语言和汇编语言函数调用相关推荐
- C语言与汇编语言相互调用原理以及实例
下面两个分别是一个foo.asm(汇编语言文件),bar.c(c语言文件) 首先来了解C语言为什么能调用汇编语言,以及汇编语言为什么能调用C语言.其实不管是C语言还是汇编语言想要执行都是最终编译链接成 ...
- c语言可以调用汇编语言吗,C语言与汇编语言混编方式
C语言是目前非常流行的一种编程语言,除具有高级语言使用方便灵活.数据处理能力强.编程简单等优点外:还可实现汇编语言的大部分功能,如可直接对硬件进行操作.生成的目标代码质量较高等,而汇编语言没有高级语言 ...
- C语言数据交换算法和伪指令,补充:单片机c语言与汇编语言混合编程.ppt
补充:单片机c语言与汇编语言混合编程 reg51.h和reg52.h:实质上是没有区别的,都是一些特殊功能寄存器的申明 sfr P0 = 0x80; sfr P1 = 0x90; sfr P2 = 0 ...
- keil4c语言和汇编混合,keil C语言与汇编语言混合编程
keil C语言与汇编语言混合编程 1. C语言中嵌入汇编 1.在 C 文件中要嵌入汇编代码片以如下方式加入汇编代码: #pragma ASM ; Assembler Code Here #pragm ...
- c语言与汇编语言混合编程
如何从汇编语言过渡到c语言? 从编译过程谈起 编译小知识 源代码编译后得到目标文件 (二进制文件) 不同语言可编译得到相同格式的目标文件 链接器负责将目标文件组装得到可执行文件 老生常谈的问题... ...
- c语言与汇编语言混合编程实验,C语言与汇编语言混合编程实验
混合编程方法: 模块链接法 汇编指令嵌入法 1: 模块链接法则 模块链接法是指分别用汇编语言和C语言实现独立的模块(或子程序),再用链接程序把各模块生成的obj文件连接成一个可执行程序. 1:C语言调 ...
- ARM下C语言和汇编语言混合编程
文章目录 一. 在C语言中调用汇编语言 1. 编写代码 2. 调试验证 二. 在汇编语言中调用C语言 1. 编写代码 2. 调试验证 关于函数的传递参数以及接受返回值的原理,在另一篇博客:X86与AR ...
- 嵌入式c语言汇编混合编程,嵌入式C语言和汇编语言的混合编程
此文章简单介绍 单片机 C语言和汇编语言混合编程的例子.主要用单片机汇编语言编写DS1302的底层驱动,在C语言里通过调用汇编语言,从而实现C和汇编的混合编程. ;汇编语言源文件 ;========= ...
- 汇编和python-python语言属于汇编语言吗?_后端开发
c语言主函数名是什么?_后端开发 C语言主函数名是main,main函数又称主函数,是程序执行的起点,如果有其他函数,则完成对其他函数的调用后再返回到主函数,最后由main函数结束整个程序. pyth ...
最新文章
- 为什么分布式一定要有延时任务?
- Linux运维必会的实战编程笔试题(19题)
- linux18.04安装显卡驱动,详细介绍ubuntu18.04安装NVIDIA显卡驱动(亲测有效!)
- 00018计算机应用2018年4月,全国2019年4月自考00018《计算机应用基础》试题及答案...
- 程序员你写的代码,被人挖出了黑产
- 【计算机网络】物理层设备
- 复制书稿(信息学奥赛一本通-T1278)
- 《Linux编程》上机作业 ·005【进程管理与通信】
- 1 Oracle数据库环境搭建
- 孙鑫VC学习笔记:第十三讲 (五) 保存可串行化的类对象 如何获取文档与视类指针
- Android Listener侦听的N种写法
- python贝叶斯分析方法实例_python 贝叶斯分析对应的代码
- java数组下标从几开始的_为什么数组角标从0开始
- aes key iv从mysql_OpenSSL AES 算法中 Key 和 IV 是如何生成的?
- Mysql 窗口函数
- NB-IOT中eNB是什么,eNB的作用是什么
- 撸一个聊天室(vue+koa2+websokect+mongodb)
- pdf转换成jpg转换器教程
- 搭配Online|原光辉调研沁水县文物保护和城市建设工作
- Authentication failed for 解决办法