​ 在我们开始学习C语言的时候,老师就跟我们讲”main函数就是程序开始执行的地方”,所有的代码都是从这里开始的,可事实真的是这样的吗?

“所有”?这个词似乎有点以偏概全,如果main函数就是一切的开始,那么程序的堆栈,main函数传递的参数,I/O操作是凭空出现的吗?显然不是,是操作系统在main函数之前,就已经帮我们初始化好了一切,所以我们的main函数才能顺利执行,那么入口点不是main,那会是谁呢?我们可以编译一个静态的demo并对main函数进行交叉引用一下,可以发现它的名字叫start,当我们跟进去的时候,就能发现另一个新的世界!

1
2
3
4
5
6
7
8
9
 #include<stdio.h>

int main(void)
{
printf("welcome to exit\n");
exit(0);
return 1;
}
//gcc exit.c -o exit -no-pie -static

在IDA里面可以看到_start的汇编,_start可以分为两个部分上半部分

上面的_start函数是x86下的(有做删减),下面的是x86-64,对比来看x86的就长了很多,原因是因为压栈的时候,不能直接将内存中的地址压入栈中,需要存到后寄存器再压入栈中,虽然长了点,但调用的本质都是一样的

  • xor ebp,ebpebp进行异或置为0完成对栈底指针的初始化,之后pop esiargc弹出到esi中,因为再最开始初始化的时候就已经将env,argv,argc压入栈中,并且esp指向了argc的位置,之后就将esp进行异或0FFFFFFF0hesp会根据当前的位置下降0-15个字节,为什么要这么做呢?目的是为了对齐,保证栈上所有的变量都能够被内存和cache快速的访问

env是系统的环境变量,包括系统的一些基本信息,所以__environ一直指向的一直都是栈上的地址,这就是为什么它能够泄露栈地址的原因,平时在路由器里面执行printenv的时候就能打印路由器的环境变量,这对于接下来的攻击也有很大的辅助作用

  • 之后的语句就是压入___libc_start_main函数所需要的参数,为了字节对齐,压入的第一个参数eax只有对齐的效果,并没有使用到,下面的参数就是按照__libc_start_main的函数定义依次压入,stack_end是栈顶指针,rtld_fini动态加载有关的收尾工作,init为main调用前的初始化,fini为main函数结束之后的收尾工作

    __libc_start_mainlibc-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:08048340 _start          proc near               ; DATA XREF: LOAD:08048018↑o
.text:08048340 xor ebp, ebp
.text:08048342 pop esi
.text:08048343 mov ecx, esp
.text:08048345 and esp, 0FFFFFFF0h
.text:08048348 push eax
.text:08048349 push esp ; stack_end
.text:0804834A push edx ; rtld_fini
.text:08048356 lea eax, (__libc_csu_fini - 804A000h)[ebx]
.text:0804835C push eax ; fini
.text:0804835D lea eax, (__libc_csu_init - 804A000h)[ebx]
.text:08048363 push eax ; init
.text:08048364 push ecx ; ubp_av
.text:08048365 push esi ; argc
.text:08048366 mov eax, offset main
.text:0804836C push eax ; main
.text:0804836D call ___libc_start_main
.text:08048372 hlt
.text:08048372 _start endp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:0000000000400450 _start          proc near               ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000400450 ; __unwind {
.text:0000000000400450 xor ebp, ebp
.text:0000000000400452 mov r9, rdx ; rtld_fini
.text:0000000000400455 pop rsi ; argc
.text:0000000000400456 mov rdx, rsp ; ubp_av
.text:0000000000400459 and rsp, 0FFFFFFFFFFFFFFF0h
.text:000000000040045D push rax
.text:000000000040045E push rsp ; stack_end
.text:000000000040045F mov r8, offset __libc_csu_fini ; fini
.text:0000000000400466 mov rcx, offset __libc_csu_init ; init
.text:000000000040046D mov rdi, offset main ; main
.text:0000000000400474 call cs:__libc_start_main_ptr
.text:000000000040047A hlt
.text:000000000040047A ; } // starts at 400450

删除大量的宏之后,留下了一些比较重要的函数,如下:

atexit函数有个特点,就是当main函数返回的时候才会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  /* Result of the 'main' function.  */
int result;
char **ev = &argv[argc + 1];

__environ = ev;

__libc_stack_end = stack_end;
//=======================================

__pthread_initialize_minimal ();

__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);

__libc_init_first (argc, argv, __environ);

__cxa_atexit ((void (*) (void *)) fini, NULL, NULL);

(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);

result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);

exit (result);

程序的最终调用链(简化版)是:

_start -> __libc_start_main -> __libc_csu_init -> main -> exit

看完之后,看看下面这张图片是不是很亲切!

参考文章:

Linux X86 程序启动 – main函数是如何被执行的?

《程序员的自我修养》P317