从汇编角度看待函数调用
引言
函数调用对于程序员而言,就像每天吃饭睡觉一样普通寻常。几乎每种编程语言都会提供函数定义、函数调用的功能。但是,在看起来寻常不过的函数调用背后,系统内核帮助我们做了很多事情。下面,我打算通过反汇编的方法,从汇编语言的层次来阐释函数调用的实现。
基础知识
先回顾几个概念,这样可以帮助我们顺利地理解后面实验的结果。
调用函数(caller)和被调函数(callee)
调用函数(caller)向被调函数(callee)传入参数,被调函数(callee)返回结果。首先要明确这两个名词,免得被下文的表述弄混淆。
高地址和低地址
每个进程都有自己的虚拟地址空间。高地址和低地址是相对的,我们通常用16进制数来表示一个内存地址。例如,相比于0x00
,0x04
数值上比0x00
大,所以0x04
称为高地址, 0x00
称为低地址。
进程内存布局
如图,一个进程的内存布局从低地址到高地址分别是
- 代码段
- 数据段,包括初始化区和未初始化区(bss)
- 堆段
- 栈段
- 内核地址空间
栈段(stack segment)
栈是最常用的数据结构之一,可以进行push/pop,且只允许在一端进行操作,后进先出(LIFO)。但就是这个最简单的数据结构,构成了计算机中程序执行的基础,用于内核中程序执行的栈具有以下特点:
- 每一个进程在用户态对应一个调用栈结构(call stack)
- 程序中每一个未完成运行的函数对应一个栈帧(stack frame),栈帧中保存函数局部变量、传递给被调函数的参数等信息
- 栈底对应高地址,栈顶对应低地址,栈由内存高地址向低地址生长
一个进程的调用栈图示如下:
寄存器(register)
寄存器位于CPU内部,用于存放程序执行中用到的数据和指令,CPU从寄存器中取数据,相比从内存中取快得多。
寄存器又分通用寄存器和特殊寄存器。
通用寄存器有ax/bx/cx/dx/di/si,尽管这些寄存器在大多数指令中可以任意选用,但也有一些规定某些指令只能用某个特定“通用”寄存器,例如函数返回时需将返回值mov到ax寄存器中;特殊寄存器有bp/sp/ip等,特殊寄存器均有特定用途,例如sp寄存器用于存放以上提到的栈帧的栈顶地址,除此之外,不用于存放局部变量,或其他用途。
对于有特定用途的几个寄存器,简要介绍如下:
- ax(accumulator): 可用于存放函数返回值
- bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
- sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
- ip(instruction pointer): 指向当前执行指令的下一条指令
不同架构的CPU,寄存器名称被添以不同前缀以指示寄存器的大小。例如对于x86架构,字母“e”用作名称前缀,指示各寄存器大小为32位;对于x86_64寄存器,字母“r”用作名称前缀,指示各寄存器大小为64位。
大学课程(例如微机原理、汇编语言)里应该都会介绍Intel 8086汇编或类似知识,相信应该可以触类旁通,很多时候只是寄存器的名字发生了变化,大体的思想还是共通的。
函数调用样例
在掌握了基础知识之后,我们选取下面这个简单的例子进行分析。
//call_example.c
int add(int a, int b) { return a + b; }
int main(void) {add(2, 5);return 0;
}
通过gcc call_example.c -g -o call_example
命令得到可执行文件call_example
。
加上参数-g是为了让目标文件call_example
包含符号表等调试信息。
我们可以用objdump -D -M att ./call_example
命令先来对call_example
进行反汇编看看结果。截取了部分结果如下:
00000000004004a6 <add>:4004a6: 55 push %rbp4004a7: 48 89 e5 mov %rsp,%rbp4004aa: 89 7d fc mov %edi,-0x4(%rbp)4004ad: 89 75 f8 mov %esi,-0x8(%rbp)4004b0: 8b 55 fc mov -0x4(%rbp),%edx4004b3: 8b 45 f8 mov -0x8(%rbp),%eax4004b6: 01 d0 add %edx,%eax4004b8: 5d pop %rbp4004b9: c3 retq 00000000004004ba <main>:4004ba: 55 push %rbp4004bb: 48 89 e5 mov %rsp,%rbp4004be: be 05 00 00 00 mov $0x5,%esi4004c3: bf 02 00 00 00 mov $0x2,%edi4004c8: e8 d9 ff ff ff callq 4004a6 <add>4004cd: b8 00 00 00 00 mov $0x0,%eax4004d2: 5d pop %rbp4004d3: c3 retq 4004d4: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)4004db: 00 00 004004de: 66 90 xchg %ax,%ax
objdump
固然是一个好工具,但是有时候看起来不是那么直观,下面我着重介绍用gdb进行分析反汇编分析。
利用gdb进行反汇编分析
我们利用gdb
跟踪main->add的过程。
启动
利用gdb
载入可执行程序call_example
$ gdb ./call_example
GNU gdb (GDB) 7.12.1
Reading symbols from ./call_example...done.
(gdb) start
Temporary breakpoint 1 at 0x4004be: file call_example.c, line 3.
Starting program: /tmp/call_exampleTemporary breakpoint 1, main () at call_example.c:3
3 add(2, 5);
(gdb)
start命令用于拉起被调试程序,并执行至main函数的开始位置,程序被执行之后与一个用户态的调用栈关联。
main函数
现在程序停止在main函数,用disassemble命令显示当前函数的汇编信息:
(gdb) disassemble /mr
Dump of assembler code for function main:
2 int main(void) {0x00000000004004ba <+0>: 55 push %rbp0x00000000004004bb <+1>: 48 89 e5 mov %rsp,%rbp3 add(2, 5);
=> 0x00000000004004be <+4>: be 05 00 00 00 mov $0x5,%esi0x00000000004004c3 <+9>: bf 02 00 00 00 mov $0x2,%edi0x00000000004004c8 <+14>: e8 d9 ff ff ff callq 0x4004a6 <add>4 return 0;0x00000000004004cd <+19>: b8 00 00 00 00 mov $0x0,%eax5 }0x00000000004004d2 <+24>: 5d pop %rbp0x00000000004004d3 <+25>: c3 retq End of assembler dump.
(gdb)
disassemble命令的/m指示显示汇编指令的同时,显示相应的程序源码;/r指示显示十六进制的计算机指令(raw instruction)。
以上输出每行指示一条汇编指令,除程序源码外共有四列,各列含义为:
- 0x00000000004004ba: 该指令对应的虚拟内存地址
- <+0>: 该指令的虚拟内存地址偏移量
- 55: 该指令对应的计算机指令
- push %rbp: 汇编指令
回忆一下我们用汇编语言写调用函数的代码时,第一步是“保护现场”,也就是:
- 将调用函数的栈帧栈底地址入栈,即将bp寄存器的值压入调用栈中
- 建立新的栈帧,将被调函数的栈帧栈底地址放入bp寄存器中,其值为调用函数的栈顶地址sp
以下两条指令即完成上面动作:
push %rbp
mov %rsp, %rbp
通过objdump
和gdb
的结果,我们发现main函数也包含了这两条指令,这是因为main函数也会被__libc_start_main
所调用,这里不多加赘述。
main调用add函数,两个参数传入通用寄存器中:
mov $0x5,%esi
mov $0x2,%edi
咦?汇编语言课上老师不是教过传递的参数会被压入栈中么?
其实,x86和x86_64定义了不同的函数调用规约(calling convention)。x86_64采用将参数传入通用寄存器的方式,x86则将参数压入调用栈中。我们利用gcc -S -m32 call_example.c
来直接生成x86平台的汇编代码,找到传递参数那段代码:
pushl $5
pushl $2
call add
原来如此!
准备完参数之后,就可以放心大胆的将控制权交给add函数了,callq指令完成这里的交接任务:
0x00000000004004c8 <+14>: e8 d9 ff ff ff callq 0x4004a6 <add>
callq指令会在调用函数的时候将下一条指令的地址push到stack上,当本次调用结束后,retq指令会跳转到被保存的返回地址处使程序继续执行。
本次callq指令,完成了两个任务:
- 将调用函数(main)中的下一条指令(这里为0x00000000004004cd)入栈,被调函数返回后将取这条指令继续执行
- 修改指令指针寄存器rip的值,使其指向被调函数(add)的执行位置,这里为0x00000000004004a6
我们可以用stepi
指令进行指令级别的操作,相比于一般调试时候按行调试的粒度会更精细。
(gdb) stepi 3
add (a=0, b=4195248) at call_example.c:1
1 int add(int a, int b) { return a + b; }
(gdb) disassemble /mr
Dump of assembler code for function add:
1 int add(int a, int b) { return a + b; }
=> 0x00000000004004a6 <+0>: 55 push %rbp0x00000000004004a7 <+1>: 48 89 e5 mov %rsp,%rbp0x00000000004004aa <+4>: 89 7d fc mov %edi,-0x4(%rbp)0x00000000004004ad <+7>: 89 75 f8 mov %esi,-0x8(%rbp)0x00000000004004b0 <+10>: 8b 55 fc mov -0x4(%rbp),%edx0x00000000004004b3 <+13>: 8b 45 f8 mov -0x8(%rbp),%eax0x00000000004004b6 <+16>: 01 d0 add %edx,%eax0x00000000004004b8 <+18>: 5d pop %rbp0x00000000004004b9 <+19>: c3 retq End of assembler dump.
(gdb)
至此,main函数的执行到此就暂时告一段落了,我们进入了add函数的新篇章。
add函数
add函数也是一样的套路,头两条指令先建立自己的栈帧,然后调用add指令计算结果,结果存放在eax寄存器中。计算完之后,需要“恢复现场”:
0x00000000004004b8 <+18>: 5d pop %rbp
因为此例比较特殊,add函数没有包含局部变量,main和add函数的栈顶恰好相同,所以忽略了对栈顶rsp的恢复。
通常,完整的“恢复现场”需要以下两条指令:
mov %rbp, %rsp
pop %rbp
参考:
https://web.stanford.edu/clas...
从汇编角度看待函数调用相关推荐
- 从汇编角度理解 ebpesp 寄存器、函数调用过程、函数参数传递以及堆栈平衡
关于函数参数的传递及堆栈指针的变化,一直缺乏系统的认识和了解,各种博客也只是片面的讲解某个局部知识点,并没有全局的把握和对栈的深刻理解.本文试图从汇编以及整体上,讲解函数调用时,堆栈的变化,以及到底是 ...
- 视频教程-C语言-从汇编角度理解C语言的本质-C/C++
C语言-从汇编角度理解C语言的本质 擅长JavaWeb开发,游戏逆向外挂与反外挂,游戏保护对抗 孙冉 ¥49.00 立即订阅 扫码下载「CSDN程序员学院APP」,1000+技术好课免费看 APP订阅 ...
- DayDayUp:寒门女孩考入北大→换角度看待表达《感谢贫穷》—关于吃苦与穷~~~Python之wordcloud词云图可视化
DayDayUp:寒门女孩考入北大→换角度看待表达<感谢贫穷>-关于吃苦与穷~~~Python之wordcloud词云图可视化 目录 博主看法-关于吃苦与穷 文本内容 寒门女孩考入北大-& ...
- 从运营角度看待UE设计
我们有没有经常吐槽各种游戏"这操作真变扭""升星累死了""界面怎么这么多图标都找到活动在哪了"等等,从运营的角度该如何看待UE设计呢? UE ...
- 献给汇编初学者-函数调用堆栈变化分析
献给汇编初学者-函数调用堆栈变化分析 标 题: 献给汇编初学者-函数调用堆栈变化分析 作 者: 堕落天才 时 间: 2007-01-19,19:20 链 接: http://bbs.pediy.com ...
- 一切皆服务:以蓝天的角度看待云
本文讲的是一切皆服务:以蓝天的角度看待云,[IT168 报道]当人们谈到云计算,不乏各种意见.预测,甚至定义.其结果是:也不乏困惑和混淆.好在人们有一点达成了共识:我们正处于一场重大市场变革的顶峰,这 ...
- 从数据结构及汇编角度深入学习go语言
原文连接 Golang基础知识 源码调试 从汇编角度理解go go/c/c++常用功能对应的汇编指令 数据结构 内建容器简介 array/slice map 字符串 结构体 接口 常用关键字 for和 ...
- 【比赛总结】从编程位队长的角度看待第十三届华中杯数学建模比赛A题
前言 有幸以编程位和队长的身份大一就参加了一次数学建模比赛,这次比赛是"华中杯",所以第一次打还是比较有新鲜感和有很多收获的,故记于此. 因为--在前期找指导老师的时候一说是大一的 ...
- 1、从软件开发角度看待PCI和PCIe
1.从软件开发角度看待PCI和PCIe 转载教程 01 1. 最容易访问的设备是什么 2. 地址空间的概念 3. 理解PCI和PCIE的关键 3.1 地址空间转换 3.2 PCI接口速览 3.3 PC ...
最新文章
- MySQL字符串函数substring:字符串截取
- 李宏毅深度学习——Why Deep?
- jquery找祖先包含_jquery如何获取祖先元素
- oracle 1天后,Oracle Code One - 第1天 精彩亮点回顾
- 浏览器输入一个url会发生什么
- mysql 联表比对,MySQL联表查询详解/超详细mysql left join,right join,inner join用法分析比较...
- 用python写出九九乘法表
- OpenGL(5)——变换
- JVM垃圾收集器(2)
- NO.4 计算有序数组的平方
- 【零知ESP8266教程】快速入门28 六轴传感器模块的使用
- 了解CSS的float高度坍塌的原理,并懂得怎么解决高度坍塌!
- 高项_第十三章项目合同管理
- 小孟5w接了个盲盒小程序,三周开发完毕
- 有关神经网络的训练算法,神经网络算法通俗解释
- sbrkr.c:(.text._sbrk_r+0xc): undefined reference to `_sbrk'
- 完美解决各种spring项目报错问题
- 计算机科学应用论文题目,比较好写的计算机科学与应用论文题目 计算机科学与应用论文题目怎么取...
- nestjs入门(controller,service,module)
- 浅谈UPS不间断电源的重要性