Segmetation fault你来的真不是时候
问题是这样的,今天一个简单的C程序,用gcc编译成汇编语言后,原本想在里面改点东西,结果运行时就报了“Segmetation fault”。它丫来的还真不是时候,恰好最近正好烦它呢,谁知本身倒送上门来了。OK,择日不如撞日,今儿就拿你开刀了。
源代码以下:
- /*littletrick.c*/
- #include <stdio.h>
- int main()
- {
- int a = 100;
- int b = 25;
- if (a > b)
- {
- return a;
- }
- else
- {
- return b;
- }
- }
用gcc将其编译成汇编源文件:
还没来得及在里面作修改,三条命令下去,结果“Segmetation fault”了:
可能有些人曾经遇到过这样的问题,或许有些人未来可能会遇到。不知道你们对这个问题有什么感想?这里说说我本身的分析、追踪和解决过程,也都是一些片汤话,顺便和你们分享分享。
当初学C语言了,老师就说过main()函数是C语言的入口函数,因此你写的C程序里必定要以main()做为函数入口。注意这里说的是“main()函数是C库的入口函数”。
而在学习汇编语言的时候,老师又说过“汇编语言源程序的入口点是_start",因此当咱们写汇编源程序时须要一个_start标记,指明程序的入口地方。
有了这两点基础知识,咱们必定不会有main()或者_start就是进程入口点的错误观念了。关于main()函数以前,阿彬有两篇人气爆高、超经典的博文,想继续探究这些问题的盆友们请点“man函数以前”和“北极以北 main函数以前”。
回到咱们的问题上来,咱们是从C语言源程序里生成的汇编源代码的,所以gcc在将C文件编译成汇编语言源程序时就默认滴认为咱们的程序最终是经过C库(不论是静态连接仍是动态连接)来调用main()函数,因此看汇编出来的文件最末尾有两条指令leave和ret:
- //省略部分代码
- .L3:
- leave
- ret
- .size main, .-main
- .ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-3)"
- .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"了。
欢迎关注本站公众号,获取更多信息