Linux Debugging(一): 使用反汇编理解C++程序函数调用栈
拿到CoreDump后,如果看到的地址都是????,那么基本上可以确定,程序的栈被破坏掉了。GDB也是使用函数的调用栈去还原“事故现场”的。因此理解函数调用栈,是使用GDB进行现场调试或者事后调试的基础,如果不理解调用栈,基本上也从GDB得不到什么有用的信息。当然了,也有可能你非常“幸运”, 一个bt就把哪儿越界给标出来了。但是,大多数的时候你不够幸运,通过log,通过简单的code walkthrough,得不到哪儿出的问题;或者说只是推测,不能确诊。我们需要通过GDB来最终确定CoreDump产生的真正原因。
本文还可以帮助你深入理解C++函数的局部变量。我们学习时知道局部变量是是存储到栈里的,内存管理对程序员是透明的。通过本文,你将明白这些结论是如何得出的。
栈,是LIFO(Last In First Out)的数据结构。C++的函数调用就是通过栈来传递参数,保存函数返回后下一步的执行地址。接下来我们通过一个具体的例子来探究。
int func1(int a)
{
int b = a + 1;
return b;
}
int func0(int a)
{
int b = func1(a);
return b;
} int main()
{
int a = 1234;
func0(a);
return 0;
}
可以使用以下命令将上述code编程成汇编代码:
g++ -g -S -O0 -m32 main.cpp -o-|c++filt >main.format.s
c++filt 是为了Demangle symbols。-m32是为了编译成x86-32的。因为对于x86-64来说,函数的参数是通过寄存器传递的。
main的汇编代码:
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx) pushl %ebp #1:push %ebp指令把ebp寄存器的值压栈,同时把esp的值减4
movl %esp, %ebp #2 把esp的值传送给ebp寄存器。
#1 + #2 合起来是把原来ebp的值保存在栈上,然后又给ebp赋了新值。
#2+ ebp指向栈底,而esp指向栈顶,在函数执行过程中esp
#2++随着压栈和出栈操作随时变化,而ebp是不动的
pushl %ecx
subl $20, %esp #3 现在esp地址-20/4 = 5, 及留出5个地址空间给main的局部变量
movl $1234, -8(%ebp)#4 局部变量1234 存入ebp - 8 的地址
movl -8(%ebp), %eax #5 将地址存入eax
movl %eax, (%esp) #6 将1234存入esp指向的地址
call func0(int) #7 调用func0,注意这是demangle后的函数名,实际是一个地址
movl $0, %eax
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
对于call指令,这个指令有两个作用:
func0函数调用完之后要返回到call的下一条指令继续执行,所以把call的下一条指令的地址压栈,同时把esp的值减4。修改程序计数器
eip,跳转到func0函数的开头执行。
至此,调用func0的栈就是下面这个样子:
下面看一下func0的汇编代码:
func0(int):
pushl %ebp
movl %esp, %ebp
subl $20, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call func1(int)
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
需要注意的是esp也是留了5个地址空间给func0使用。并且ebp的下一个地址就是留给局部变量b的,调用栈如图:
通过调用栈可以看出,8(%ebp)其实就是传入的参数1234。
func1的代码:
func1(int):
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %eax #去传入的参数,即1234
addl $1, %eax # +1 运算
movl %eax, -4(%ebp)
movl -4(%ebp), %eax #将计算结果存入eax,这就是返回值
leave
ret
leave指令,这个指令是函数开头的push %ebp和mov %esp,%ebp的逆操作:
把
ebp的值赋给esp现在
esp所指向的栈顶保存着foo函数栈帧的ebp,把这个值恢复给ebp,同时esp增加4。注意,现在esp指向的是这次调用的返回地址,即上次调用的下一条执行指令。
最后是ret指令,它是call指令的逆操作:
现在
esp所指向的栈顶保存着返回地址,把这个值恢复给eip,同时esp增加4,esp指向了当前frame的栈顶。修改了程序计数器
eip,因此跳转到返回地址继续执行。
调用栈如下:
至此,func1返回后,控制权交还给func0,当前的栈就退化成func0的栈的情况,因为栈保存了一切信息,因此指令继续执行。直至func0执行
leave
ret
以同样的方式将控制权交回给main。
到这里,你应该知道下面问题的答案了:
1. 局部变量的生命周期,
2. 局部变量是怎么样使用内存的;
3. 为什么传值不会改变原值(因为编译器已经帮你做好拷贝了)
4. 为什么会有栈溢出的错误
5. 为什么有的写坏栈的程序可以运行,而有的却会crash(如果栈被破坏的是数据,那么数据是脏的,不应该继续运行;如果破坏的是上一层调用的bp,或者返回地址,那么程序会crash,or unexpected behaviour...)
小节一下:
1. 在32位的机器上,C++的函数调用的参数是存到栈上的。当然gcc可以在函数声明中添加_attribute__((regparm(3)))使用eax, edx,ecx传递开头三个参数。
2. 通过bp可以访问到调用的参数值。
3. 函数的返回地址(函数返回后的执行指令)也是存到栈上的,有目的的修改它可以使程序跳转到它不应该的地方。。。
4. 如果程序破坏了上一层的bp的地址,或者程序的返回地址,那么程序就很有可能crash
5. 拿到一个CoreDump,应该首先先看有可能出问题的线程的的frame的栈是否完整。
6. 64位的机器上,参数是通过寄存器传递的,当然寄存器不够用就会通过栈来传递
支持原创,转载请注明出处:anzhsoft http://blog.csdn.net/anzhsoft/article/details/18730605
Linux Debugging(一): 使用反汇编理解C++程序函数调用栈的更多相关文章
- Linux Debugging(三): C++函数调用的参数传递方法总结(通过gdb+反汇编)
上一篇文章<Linux Debugging:使用反汇编理解C++程序函数调用栈>没想到能得到那么多人的喜爱,因为那篇文章是以32位的C++普通函数(非类成员函数)为例子写的,因此只是一个特 ...
- Linux Debugging(二): 熟悉AT&T汇编语言
没想到<Linux Debugging:使用反汇编理解C++程序函数调用栈>发表了收到了大家的欢迎.但是有网友留言说不熟悉汇编,因此本书列了汇编的基础语法.这些对于我们平时的调试应该是够用 ...
- Linux Debugging(五): coredump 分析入门
作为工作几年的老程序猿,肯定会遇到coredump,log severity设置的比较高,导致可用的log无法分析问题所在. 更悲剧的是,这个问题不好复现!所以现在你手头唯一的线索就是这个程序的尸体: ...
- Linux 虚拟内存和物理内存的理解
关于Linux 虚拟内存和物理内存的理解. 首先,让我们看下虚拟内存: 第一层理解 1. 每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构 2. 一个新进程建立的时候,将会建立起自 ...
- linux下,一个运行中的程序,究竟占用了多少内存
linux下,一个运行中的程序,究竟占用了多少内存 1. 在linux下,查看一个运行中的程序, 占用了多少内存, 一般的命令有 (1). ps aux: 其中 VSZ(或VSS)列 表示,程序占用 ...
- Linux文件系统十问---深入理解文件存储方式(rhel6.5,EXT4)【转】
本文转载自:https://blog.csdn.net/tongyijia/article/details/52832236 前几天在红黑联盟上看了一篇博客<Linux文件系统十问—深入理解文件 ...
- 【转帖】linux内存管理原理深入理解段式页式
linux内存管理原理深入理解段式页式 https://blog.csdn.net/h674174380/article/details/75453750 其实一直没弄明白 linux 到底是 段页式 ...
- Linux多任务编程之六:编写多进程程序及其代码(转)
来源:CSDN 作者:王文松 转自Linux公社 ------------------------------------------------------------------------- ...
- Linux下使用Eclipse开发Hadoop应用程序
在前面一篇文章中介绍了如果在完全分布式的环境下搭建Hadoop0.20.2,现在就再利用这个环境完成开发. 首先用hadoop这个用户登录linux系统(hadoop用户在前面一篇文章中创建的),然后 ...
随机推荐
- webpack 前后端分离开发接口调试解决方案,proxyTable解决方案
如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用. dev-server 使用了非常强大的 http-proxy-middleware 包. ...
- log4j日志的基本使用方法(1)——概述、配置文件
一.概述 Log4j由三个重要的组件构成:日志信息的优先级,日志信息的输出目的地,日志信息的输出格式.日志信息的优先级从高到低有ERROR.WARN.INFO.DEBUG,分别用来指定这条日志信息的重 ...
- Python:操作数据库
(一) 前言 本文说明如何连接Oracle.MySQL.sqlserver,以及执行sql.获取查询结果等. (二) DB-API DB-API阐明一系列所需对象和数据库 ...
- Luogu P1919 【模板】A*B Problem升级版(FFT快速傅里叶_FFT
这其实就是一道裸的FFT 核心思想:把两个数拆成两个多项式用FFT相乘,再反序输出 py解法如下: input() print(int(input())*int(input())) 皮一下hihi f ...
- 码农代理免费代理ip端口字段js加密破解
起因 之前挖过爬取免费代理ip的坑,一个比较帅的同事热心发我有免费代理ip的网站,遂研究了下:https://proxy.coderbusy.com/. 解密 因为之前爬过类似的网站有了些经验,大概知 ...
- MongoDB 数据库引用
MongoDB 引用有两种: 手动引用(Manual References) DBRefs DBRefs vs 手动引用 考虑这样的一个场景,我们在不同的集合中 (address_home, addr ...
- 浏览器加载和渲染html的顺序(html/css/js)
最近在学习前端的技术,把html.js.css的基础知识看了看.感觉越看越觉得前端并不比后端容易,技术含量还是相当大的.今天突然想弄明白浏览器到底是怎么加载和渲染html的?html中的DOM.js文 ...
- 重温java基础
Java标识符 Java所有的组成部分都需要名字.类名.变量名以及方法名都被称为标识符. 关于Java标识符,有以下几点需要注意: 所有的标识符都应该以字母(A-Z或者a-z),美元符($).或者下划 ...
- python 反人类函数式编程模拟while和if控制流
比如下面这个简单明了的命令式程序,它不断捕捉用户输入的内容,然后对其求和.直到用户输入一个以'0'开头的字符串,停止捕捉. while 1: line = input() ': print(sum(m ...
- Apache shiro集群实现 (八) web集群时session同步的3种方法
Apache shiro集群实现 (一) shiro入门介绍 Apache shiro集群实现 (二) shiro 的INI配置 Apache shiro集群实现 (三)shiro身份认证(Shiro ...