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