入口函数和函数初始化
程序在进入我们编写的入口之前,就已经初始化好了堆栈,外围IO,全局变量等。
这些工作都是函数库完成的,他们才是一个独立程序最开始执行的代码。
入口函数的实现(静态glibc+可执行文件)
首先我们要明白在PC指向E入口地址执行之前,是谁在handle这个ELF文件?应该是装载器(ld),装载器按照其ELF文件中的Program Header等信息将其相关的部分装载内存中,同时也会将用户的参数和环境变量压入栈中。有了这个背景知识,我们可以从Entry point address出发,分析该ELF文件的执行过程。
tar xvf glibc-2.35.tar
vim .//sysdeps/arm/start.S
我们来看看arm平台的入口实现:
_start:
/* Protect against unhandled exceptions. */
.fnstart
/* Clear the frame pointer and link register since this is the outermost frame. */
/* 清栈帧指针和lr指针,说明这是最外层的栈(FP实际上就是R11寄存器,在APCS调用规则中,使用R11作为帧指针寄存器) */
mov fp, #0
mov lr, #0
/* Pop argc off the stack and save a pointer to argv */
/* a2 a3代表的是x2 x3寄存器,代表的是传参argc和argv */
pop { a2 }
mov a3, sp
/* Push stack limit a3保存的argv参数指针压栈 */
push { a3 }
/* Push rtld_fini 压栈rtld函数*/
push { a1 }
#ifdef PIC /* 编译时候加-fPIC参数 PIC就是position independent code 地址无关代码 PIC使.so文件的代码段变为真正意义上的共享库 */
ldr sl, .L_GOT /* 加载GOT表 */
adr a4, .L_GOT
add sl, sl, a4
mov a4, #0 /* Used to be init. */
push { a4 } /* Used to be fini. */
ldr a1, .L_GOT+4 /* main */
ldr a1, [sl, a1]
/* __libc_start_main (main, argc, argv, init, fini, rtld_fini, stack_end) */
/* Let the libc call main and exit with its return code. */
bl __libc_start_main(PLT)
#else
mov a4, #0 /* Used to init. */
push { a4 } /* Used to fini. */
ldr a1, =main /* 这就是我们常说的main是入口函数由来 */
/* __libc_start_main (main, argc, argv, init, fini, rtld_fini, stack_end) */
/*实际上前面对于a1 a2 a3的操作都成为了__libc_start_main的入参;
* a1 = main使我们要执行的函数入口
* a2/a3是包含环境变量的传参
* a4 = init是加载main之前的一些初始化工作
* a5 = fini是main结束之后的收尾工作
* a6 = rtld_fini是动态加载器的收尾工作
* a7 = stack_end标明了栈底的地址,即最高的栈地址
*/
/* Let the libc call main and exit with its return code. */
bl __libc_start_main
/*跳转执行,这个函数就完整了我们整个程序的运行,
* 其实主要功能就是:__libc_start_main这个函数完成了运行环境的初始化,然后再调用了我们函数中的main,
* 执行完成后,使用exit函数结束程序。finit用的函数注册在了exit函数中,所以调用exit会执行finit的过程。
* 到此我们就明白了运行时库是如何帮助我们初始化运行环境,并执行我们的函数,然后退出的过程
* */
#endif
/* should never get here....*/
bl abort
#ifdef PIC
.align 2
.L_GOT:
.word _GLOBAL_OFFSET_TABLE_ - .L_GOT
.word main(GOT)
#endif
.cantunwind
.fnend