execve
当 shell 中的那一段命令按下时,一个程序开始执行,shell 或者 GUI 会调用 execve()
- 系统会为你设置栈,并且将
argc,argv和envp压入栈中 - 对于文件描述符 0,1 和 2(stdin,stdout 和 stderr)则保留 shell 之前的设置
- 加载器会帮你完成重定位,调用你设置的 预初始化函数
- 最后,控制权会传递给
_start()
_start
_start 的反汇编如下:
1 | public _start |
_start 中会调用 _libc_start_main,它的原型如下:
1 | int __libc_start_main( int (*main) (int, char * *, char * *), |
__libc_start_main
__libc_start_main 函数的参数中没有 envp(main 中的第三个参数,envp 这个数组包含许多的指针,当中每个指针指向的是系统中的环境变量),因此会调用 __libc_init_first 使用内部信息去找到环境变量
- 环境变量(environment variables):一般是指在操作系统中用来指定操作系统运行环境的一些参数,环境变量可以使系统运行环境配置更加简单灵活,可以通过设置环境变量给进程传递参数信息
- PATH:指定命令的搜索路径
- HOME:当前用户的主工作目录(即 Linux 登录时,默认的目录)
- SHELL:当前 shell,它的值是通常是
/bin/shell
- Linux 为什么要有环境变量:
- 因为 Linux 执行一些命令时,它会去很多目录去搜索对应的可执行程序
- 如果可执行程序分散在不同的目录下,当搜索时,这样会非常的耗费时间
- 所以 Linux 就约定,当执行一个命令时,就到一个指定的文件中去寻找可执行程序所在的目录,这个指定的文件就是环境变量配置文件
- 可以用 GDB 打印一下 argv & envp:
1 | pwndbg> telescope 0x7fffffffdfc8 |
当 envp 建立了之后,__libc_start_main 函数会使用相同的小技巧,越过 envp 数组之后的 NULL 字符,获取另一个向量:ELF 辅助向量(ELF 加载器使用它给进程传递一些信息)
- ELF 辅助向量(Auxiliary Vectors)是 将某些内核级信息传输到用户进程的机制(例如:指向[系统调用]入口点的指针-
AT_SYSINFO)
设置环境变量 LD_SHOW_AUXV=1 就可以查看 ELF 辅助向量
1 | ➜ exp LD_SHOW_AUXV=1 ./main |
AT_ENTRY就是_start的地址,AT_PHDR是 ELF program header 的位置,AT_PHENT是header entry 的字节数 ,还输出了 UID、UID 和 GID- 这些都是内核态才能拿到的数据,但是用户态程序又需要这些数据,于是就通过 ELF 辅助向量将这些信息传输给用户态进程
当进程获取到必要的数据后,就会执行 __libc_start_main 函数的主要功能:
- 处理关于
setuid、setgid程序的安全问题 - 启动线程
- 把
fini函数(实际上是__libc_csu_fini)和rtld_fini函数作为参数传递给at_exit调用(使它们在at_exit里被调用,从而完成用户程序和加载器的调用结束之后的清理工作) - 调用其
init参数(实际上是执行__libc_csu_init函数) - 调用
main函数,并把argc和argv参数、环境变量传递给它 - 调用
exit函数(会在其中调用__libc_csu_fini),并将main函数的返回值传递给它
libc_csu_init & libc_csu_fini
对于任意的可执行程序都可以有一个C函数的“构造函数” __libc_csu_init 和C函数的“析构函数” __libc_csu_fini,在构造函数内部,可执行程序会找到全局C函数组成的构造函数集,并且调用它们
构造函数和析构函数是 Cpp 中的概念:
- 构造函数:是一种特殊的函数,用来在对象实例化的时候初始化对象的成员变量
- 析构函数:是构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动调用析构函数
1 |
|
1 | Object is being created |
C 语言没有构造函数和析构函数的概念,但 gcc 为函数提供了几种类型的属性,其中包含:
- 构造函数(constructors):
static void start(void) __attribute__ ((constructor)) - 析构函数(destructors):
static void stop(void) __attribute__ ((destructor)) - 带有“构造函数”属性的函数将在
main函数之前被执行,而声明为“析构函数”属性的函数则将在main退出时执行(其实就有点像针对main的构造函数)
源码如下:(根据 IDA 反编译出来的代码改的)
1 | void _libc_csu_init(){ |
_frame_dummy_init_array_entry中的数据如下:(提前写好了“构造函数”)
1 | .init_array:0000000000003D88 40 11 00 00 00 00 00 00 __frame_dummy_init_array_entry dq offset frame_dummy |
_do_global_dtors_aux_fini_array_entry中的数据如下:(提前写好了“析构函数”)
1 | .fini_array:0000000000003DA8 00 11 00 00 00 00 00 00 __do_global_dtors_aux_fini_array_entry dq offset __do_global_dtors_aux |
main 函数执行前后的流程
函数调用关系图:
- 首先程序调用 execve 生成一个进程,并且设置好数据
- 然后调用
_start简单设置后就调用__libc_start_main __libc_start_main会先调用__libc_init_first获取环境变量 envp(作为 main 的第三个参数),然后越过 envp 数组之后的NULL字符,获取 ELF 辅助向量- 把
fini和rtld_fini作为参数传递给at_exit调用 - 调用其
init参数,执行__libc_csu_init - 调用
main函数,并把argc和argv参数、环境变量传递给它 - 调用
exit函数,执行其中的__libc_csu_fini