main真的是函数入口吗?
在我们开始学习C语言的时候,老师就跟我们讲”main函数就是程序开始执行的地方”,所有的代码都是从这里开始的,可事实真的是这样的吗?
“所有”?这个词似乎有点以偏概全,如果main函数就是一切的开始,那么程序的堆栈,main函数传递的参数,I/O操作是凭空出现的吗?显然不是,是操作系统在main函数之前,就已经帮我们初始化好了一切,所以我们的main函数才能顺利执行,那么入口点不是main,那会是谁呢?我们可以编译一个静态的demo并对main函数进行交叉引用一下,可以发现它的名字叫start
,当我们跟进去的时候,就能发现另一个新的世界!
1 |
|
在IDA里面可以看到_start
的汇编,_start
可以分为两个部分上半部分
上面的_start
函数是x86
下的(有做删减),下面的是x86-64
,对比来看x86
的就长了很多,原因是因为压栈的时候,不能直接将内存中的地址压入栈中,需要存到后寄存器再压入栈中,虽然长了点,但调用的本质都是一样的
xor ebp,ebp
将ebp
进行异或置为0完成对栈底指针的初始化,之后pop esi
将argc
弹出到esi
中,因为再最开始初始化的时候就已经将env,argv,argc
压入栈中,并且esp
指向了argc
的位置,之后就将esp
进行异或0FFFFFFF0h
,esp
会根据当前的位置下降0-15
个字节,为什么要这么做呢?目的是为了对齐,保证栈上所有的变量都能够被内存和cache
快速的访问
env
是系统的环境变量,包括系统的一些基本信息,所以__environ
一直指向的一直都是栈上的地址,这就是为什么它能够泄露栈地址的原因,平时在路由器里面执行printenv
的时候就能打印路由器的环境变量,这对于接下来的攻击也有很大的辅助作用
之后的语句就是压入
___libc_start_main
函数所需要的参数,为了字节对齐,压入的第一个参数eax
只有对齐的效果,并没有使用到,下面的参数就是按照__libc_start_main
的函数定义依次压入,stack_end
是栈顶指针,rtld_fini
动态加载有关的收尾工作,init
为main调用前的初始化,fini
为main函数结束之后的收尾工作__libc_start_main
在libc-start.c
的文件里面,其函数定义如下
1
2
3
4
5
6 int __libc_start_main( int (*main) (int, char * *, char * *),
int argc, char * * ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void (* stack_end));
- 当
___libc_start_main
正常执行的时候,会在exit
处退出,而hlt
的是为了保证程序在___libc_start_main
调用失败的时候不会让程序一直在跑,它就是充当一个栅栏,强行把程序停下来.
1 | .text:08048340 _start proc near ; DATA XREF: LOAD:08048018↑o |
1 | .text:0000000000400450 _start proc near ; DATA XREF: LOAD:0000000000400018↑o |
删除大量的宏之后,留下了一些比较重要的函数,如下:
atexit
函数有个特点,就是当main
函数返回的时候才会执行
1 | /* Result of the 'main' function. */ |
程序的最终调用链(简化版)是:
_start -> __libc_start_main -> __libc_csu_init -> main -> exit
看完之后,看看下面这张图片是不是很亲切!
参考文章:
Linux X86 程序启动 – main函数是如何被执行的?
《程序员的自我修养》P317