问题是这样的,今天一个简单的C程序,用gcc编译成汇编语言后,本来想在里面改点东西,结果运行时就报了“Segmetation fault”。它丫来的还真不是时候,刚好最近正好烦它呢,谁知自己倒送上门来了。OK,择日不如撞日,今儿就拿你开刀了。
    源代码如下:

点击(此处)折叠或打开

  1. /*littletrick.c*/
  2. #include stdio.h>
  3. int main()
  4. {
  5. int a = 100;
  6. int b = 25;
  7. if (a > b)
  8. {
  9. return a;
  10. }
  11. else
  12. {
  13. return b;
  14. }
  15. }

用gcc将其编译成汇编源文件:

还没来得及在里面做修改,三条命令下去,结果“Segmetation fault”了:

可能有些人曾经遇到过这样的问题,或许有些人将来可能会遇到。不知道大家对这个问题有什么感想?这里说说我自己的分析、追踪和解决过程,也都是一些片汤话,顺便和大家分享分享。
    当初学C语言了,老师就说过main()函数是C语言的入口函数,所以你写的C程序里一定要以main()作为函数入口。注意这里说的是“main()函数是C库的入口函数”。
   而在学习汇编语言的时候,老师又说过“汇编语言源程序的入口点是_start",所以当我们写汇编源程序时需要一个_start标记,指明程序的入口地方。

有了这两点基础知识,我们一定不会有main()或者_start就是进程入口点的错误观念了。关于main()函数之前,阿彬有两篇人气爆高、超经典的博文,想继续探究这些问题的盆友们请点“man函数之前”和“北极以北 main函数之前”。
    回到我们的问题上来,我们是从C语言源程序里生成的汇编源代码的,因此gcc在将C文件编译成汇编语言源程序时就默认滴认为我们的程序最终是通过C库(不管是静态链接还是动态链接)来调用main()函数,所以看汇编出来的文件最末尾有两条指令leave和ret:

点击(此处)折叠或打开

  1. //省略部分代码
  2. .L3:
  3. leave
  4.     ret
  5. .size main, .-main
  6. .ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-3)"
  7. .section .note.GNU-stack,"",@progbits

leave指令其实和enter指令是配对使用,它们主要用来声明C语言风格的函数,其中enter是函数的“前言”(prologues),leave是函数的“结尾”(epilogues)。也就是说这两条指令是AT&T为C语言函数设计的“开头”和“结尾”的模板,并真心地希望每一个汇编源文件面的函数们开头都放一条enter指令,末尾都放一条leave指令。然而,这并不是一个强制的约定,所以很多时候你会看到一些汇编源代码的函数里只有leave没有enter,或者,即使人家是个函数,开头既没有enter,结尾也没有leave。这里我们看到,GCC也没有严格按照这样的风格来实现。想想当初提出这个机制的人心里会是何其的悲催啊。闲话不表,接下来是ret指令,我们都知道,在汇编语言里它通常都是和call指令配合使用,完成函数调用功能。这兄弟俩关系还算比较好,一对好基友。一般见到call的地方,在虚拟世界的某个地方大多数情况下(注意不是一定)都可以找到一个ret与它惺惺相惜,隔江相望。
   (关于call和ret的更多故事,请继续关注本博客后续的相关系列博文)
    要说明白我们遇到的这个问题的缘由还得稍微摆摆call和ret指令的故事。
   call是汇编语言中函数调用最常见的指令,它通常会完成两件事:
   第一:当call指令执行时(注意用词,不是执行前),它会首先将指令寄存器EIP的值保存在栈里,这一步是自动完成的。
   第二:修改当前的EIP值,让它指向要跳转的函数地址处。也就是接下来立即是要调用的函数的入口地址处。

当被调用的函数执行完,返回时,其末尾通常都会有一句ret指令。而该指令的作用就是自动到栈上面将被call指令存入的EIP的值恢复到EIP寄存器里,使得进程可以继续往下执行。这里我们差不多可以猜到,段错误的原因肯定是EIP的值混乱所致,但这只是猜想,待会我们还要进一步分析,EIP是怎么混乱的?为什么会混乱?怎么解决的问题。

先反编译一下我们最终的可执行文件littletrick:

大家应该看出来了,我们最终的可执行文件并没有通过C库来启动,而是直接赤裸裸滴就只有一个可执行的代码段,而且指明该程序的入口点就是main。最后一句是ret但是死活找不到call在哪里,问题恰恰刚好出在没有call和ret配对这个关键点上。我们用GDB调试运行一下看看详细过程,是不是如我们猜测的那样。重新编译littletrick.s时加-gstabs(别忘了重新链接)参数以让其支持GDB调试。

程序刚开始运行时,我们在main的地方打个断点:

看看栈、还有各个寄存器里的值:

我们看到EIP的值0x8048074就是接下来要执行的指令,也就是main函数入口的地址。这和我们上面反编译出来的main函数在虚拟地址空间的值是一致的。此时,栈顶指针ESP的值是0xbffff7a0,说明从0xbffff7a0到0xbfffffff的栈空间里已经有些一些数据,简单看一下这些数据里前128字节都长啥样子:

至于这些数据是什么,以后的博文会详细解释,这里只要知道当进程run起来的时候,栈上已经有了部分初始化数据就OK了。我们一直往下执行:

在执行ret指令前,可以看到EIP和ESP的值分别是0x8048099和0xbffff7a0。对照反汇编的结果0x8048099刚好就是ret指令的虚拟地址,而当执行完前面的leave指令时,栈上的局部变量a和b都已经被“弹”出去了。也就是说此时栈又恢复到了进程执行时的初始模样。前面我们也说过,ret会自动到栈上去取原来的EIP的值把它设置到EIP寄存器里,而此时栈顶的位置由ESP里的值0xbffff7a0来指定,从该地址开始4字节(因为EIP是32位寄存器),小端字节序的值是0x00000001。所以,当ret执行完后EIP里的值就错误的被设置成了0x00000001:

很显然,对进程来说,这是一个非法的访问地址,操作系统不允许它直接访问,因此就像上一篇博文所说的,给了一个"Segmetation fault"的错误提示信息。这里,关于ret指令我们还明确到一点,那就是ret不是从栈上取(retrieve)数据,而是弹(pop)数据出来,这会影响栈顶寄存器ESP的值。

好的,到这里问题明白了,原因也清楚了:
   某些版本的gcc在将C语言源程序编译成汇编源代码时,会在主函数main的末尾,放置一条ret指令。当我们想用gcc生成汇编模板时,如果用as命令(而不是用gcc提供的-c或者-o控制选项)去汇编*.s文件,然后用ld进行链接成可执行程序,运行时就一定会报“Segmetation fault”。至于GCC在通过C源代码生成汇编时在main函数末尾加不加ret这也和gcc的版本有关,有些版本的gcc关于C语言的return语句人家就用了exit系统调用来处理。如果你的GCC在C语言源程序编译出来的汇编代码里,在main函数末尾加了一个ret,而你也和我一样喜欢折腾,那么这里就需要注意了。

问题弄明白了,至于解决办法也就简单多了。既然问题是ret和call不配对所致,那么这里汇编出来的ret就是多余的,所以将它删掉就可以了。当然为了严紧起见,我们将它改成linux系统调用的exit函数,让它对人家操作系统总得有个交代才行。最后改过的版本:

新增的第一条movl指令是将系统调用的返回结果保存到ebx寄存器,在shell里我们可以通过检查变量"$?"来查看执行结果;第二条movl指令是将exit的系统调用号1送到eax寄存器里;第三条int $0x80就是陷入内核,执行Linux的exit系统调用。如果想深入了解系统调用的童鞋,请猛戳这里或者这里都行。
   编译、链接再运行:

结果很愉快了,然后该干啥就干啥了。
   PS:对从*.c生成的汇编语言源程序*.s,如果想继续用C库,那么你可以用“gcc -c”来编译*.s文件,然后用“gcc -o ”生成最终的可执行文件。这样一来,你就不会遇到本文所提到的烦人的"Segmetation fault"了。

Segmetation fault你来的真不是时候的更多相关文章

  1. Segmentation fault到底是何方妖孽

    http://blog.chinaunix.net/uid-23069658-id-3959636.html?page=2 进程运行的时候,它虚拟地址空间的布局和它所占用的物理内存到底是什么样子呢?虚 ...

  2. 越狱Season 1-Episode 6: Riots, Drills and the Devil: Part 1

    Season 1, Episode 6: Riots, Drills and the Devil: Part 1 - Diamond: Just a few more rides. 就再多玩几次吧 O ...

  3. DirectShow音频采集声音不连续问题分析与解决办法经验总结

    最近广州大雨不断,并且多数无前兆,突然就来场大雨,给同学们降降温,说来本也是好事,但有时候下的真不是时候,最近这段时间都是即将下班了,大雨就来了,昨晚快下班前又出现了大雨,北方人总爱忘带雨伞,这不就被 ...

  4. iOS 真机调试不能连接网络的排错过程

    开发环境: macOS 10.12.1 Xcode 8.1 Qt 5.8 gSOAP 2.8 iPhone 6S+iOS 10.1.1   问题: 使用 Qt Quick 写了一个跨平台的应用,在Wi ...

  5. linux segmentation fault记录

    文章将记录linux学习使用中出现的各种segmentation fault,持续更新,希望对看到人有所帮助 1. linux pcap segmentation fault -- 2013.11.2 ...

  6. Xcode 5.1 编译模拟器以及真机都能使用的静态库

    Xcode 5.1.dmg 下载地址 http://pan.baidu.com/s/1jGJpKm6 1.新建 Framework & Library 工程 我起名叫ShowInfo,下面为其 ...

  7. Rust v1.39发布 - 这个编程语言真不一般!

    https://zhuanlan.zhihu.com/p/90612241 今天(2019-11-07)Rust终于发布了期待已久的v1.39版本,增加了重量级的async/await关键字支持.Ru ...

  8. 探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密

    下面的图片是我使用firefox和chrome浏览百度首页时候记录的http请求 下面是firefox: 下面是chrome: 在浏览百度首页前我都将浏览器的缓存全部清理掉,让这个场景最接近第一次访问 ...

  9. geotrellis使用(二十八)栅格数据色彩渲染(多波段真彩色)

    目录 前言 实现过程 总结 一.前言        上一篇文章介绍了如何使用Geotrellis渲染单波段的栅格数据,已然很是头疼,这几天不懈努力之后工作又进了一步,整清楚了如何使用Geotrelli ...

随机推荐

  1. 关于iscroll阻止浏览器默认动作

    使用iscroll时,移动端遇到需要长按复制功能,但是iscroll屏蔽了浏览器默认事件,所以实现不了. 解决方案: myScroll = new IScroll('#wrapper',{ preve ...

  2. CRM 403错误

    1 IIS 正常 2 CRM 各项服务正常. 3  应该程序池--CRMAppPool 停止

  3. log4net 发布到生产环境不写日志的解决方法--使用 NLog日志

    1.升级到log4net的最新版 PM下执行 Install-Package log4net 还是无法解决的,使用下面的方法 2.使用Nlog替换之,详见https://github.com/NLog ...

  4. 样例20-汽车SHOW

    观看样例点这里 素材下载 1.设置场景大小为400*3002.执行:文件->导入->导入到库,选择需要的汽车图片文件,将其导入到库面板中3.按照同样的方式,在库面板中导入所需的背景音乐文件 ...

  5. ssh整合(http://blog.csdn.net/songanling/article/details/22454973)

    http://blog.csdn.net/songanling/article/details/22454973

  6. Ms sql 2000互转2005

    BCP导入导出数据.非常的方便.速度很快.--还原部分,指定强制还原(使用WITH MOVE指定文件还原,RELPACE由于版本不一样,所以要指定REPLACE,如果不指定REPLACE参数,会提示版 ...

  7. Inside The C++ Object Model - 03

    object Lessons 1.C++中布局以及存取时间上的的额外负担是由virtual引起的:virtual function.virtual base class.或是由于多继承引起的. 2.C ...

  8. TextView 中添加超链接

    在textView添加超链接,有两种方式,第一种通过HTML格式化你的网址,一种是设置autolink,让系统自动识别超链接,下面为大家介绍下这两种方法的实现   代码如下:    第一种    pu ...

  9. C# 或 Asp.net 2.0 邮件发送模块(亲测)

    using System.Net.Mail;using System.Net; public class Mail    {        MailMessage mm;        SmtpCli ...

  10. redis 安装使用

    在 centos 7.2 系统上,安装使用redis.了解学习redis功能及特性. 版本: 3.2.4 1.安装: # yum install redis 2.配置: /etc/logrotate. ...