源代码如下:
点击(此处)折叠或打开
- /*littletrick.c*/
- #include <stdio.h>
- int main()
- {
- int a = 100;
- int b = 25;
- if (a > b)
- {
- return a;
- }
- else
- {
- return b;
- }
- }
当初学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
(关于call和ret的更多故事,请继续关注本博客后续的相关系列博文)
要说明白我们遇到的这个问题的缘由还得稍微摆摆call和ret指令的故事。
call是汇编语言中函数调用最常见的指令,它通常会完成两件事:
第一:当call指令 执行时 (注意用词,不是 执行前 ),它会首先将指令寄存器EIP的值保存在栈里,这一步是自动完成的。
第二:修改当前的EIP值,让它指向要跳转的函数地址处。也就是接下来立即是要调用的函数的入口地址处。
当被调用的函数执行完,返回时,其末尾通常都会有一句ret指令。而该指令的作用就是自动到栈上面将被call指令存入的EIP的值恢复到EIP寄存器里,使得进程可以继续往下执行。这里我们差不多可以猜到,段错误的原因肯定是EIP的值混乱所致,但这只是猜想,待会我们还要进一步分析,EIP是怎么混乱的?为什么会混乱?怎么解决的问题。
先反编译一下我们最终的可执行文件littletrick:
程序刚开始运行时,我们在main的地方打个断点:
好的,到这里问题明白了,原因也清楚了:
某些版本的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函数,让它对人家操作系统总得有个交代才行。最后改过的版本:
编译、链接再运行:
PS:对从*.c生成的汇编语言源程序*.s,如果想继续用C库,那么你可以用“gcc -c”来编译*.s文件,然后用“gcc -o ”生成最终的可执行文件。这样一来,你就不会遇到本文所提到的烦人的"Segmetation fault"了。