问题是这样的,今天一个简单的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. 【freemaker】之自定义指令通用select模版

    测试代码 @Test public void test08(){ List<Group> groups=Arrays.asList(new Group(1,"山口组") ...

  2. 53. Maximum Subarray

    Find the contiguous subarray within an array (containing at least one number) which has the largest ...

  3. CSS基本知识汇总

    1.CSS 简介 CSS 指层叠样式表 (Cascading Style Sheets),是一种用来表现 HTML 文档样式的语言,样式定义如何显示 HTML 元素,是能够真正做到网页表现与结构分离的 ...

  4. angular.js ng-class-even ng-class-odd ng-cloak(没啥技术含量)

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  5. HoloLens开发笔记之Gesture input手势输入

    手势是HoloLens三个首要输入形式之一.一旦你使用凝视定位了一个全息图像,手势允许你与它交互.手势输入允许你使用手或者点击器原生地与全息图像交互. 手势之外,你也可以在应用中使用语音输入来交互. ...

  6. 在CentOS 7中安装nginx服务器

    简要地介绍一下,如何在CentOS 7中安装nginx服务器  下载对应当前系统版本的nginx包(package) # wget  http://nginx.org/packages/centos/ ...

  7. hihocoder 1391 [扫描线]

    /* 题意: 两方对阵,互发导弹.防护罩可以让导弹原速反向. 每一枚导弹有发射时间航行时间伤害值. 防护罩也有开启时间和防御时间. 其中一方防护罩开启时间已知,求另一方防护罩合理安排开启时间使得己方受 ...

  8. nmap十条常用命令行格式

    1) 获取远程主机的系统类型及开放端口 nmap -sS -P0 -sV -O <target> 这里的 < target > 可以是单一 IP, 或主机名,或域名,或子网 - ...

  9. ORACLE SQL前端补0的三种方式。

    前端补0的三种方式. select lpad(sal,8,'0') from emp;select to_char(sal,'00000000') from emp;select substr('00 ...

  10. SQL Server常用技巧

    1:在SQL语句中,将存储过程结果集(表)存入到临时表中 insert into #tmp EXEC P_GET_AllChildrenComany '80047' 说明:#tmp要提前创建好 2:字 ...