0%

Principles:main函数执行前后的流程

execve

当 shell 中的那一段命令按下时,一个程序开始执行,shell 或者 GUI 会调用 execve()

  • 系统会为你设置栈,并且将 argcargvenvp 压入栈中
  • 对于文件描述符 0,1 和 2(stdin,stdout 和 stderr)则保留 shell 之前的设置
  • 加载器会帮你完成重定位,调用你设置的 预初始化函数
  • 最后,控制权会传递给 _start()

_start

_start 的反汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                              public _start
_start proc near
; __unwind {
F3 0F 1E FA endbr64
31 ED xor ebp, ebp
49 89 D1 mov r9, rdx ; rtld_fini
5E pop rsi ; argc
48 89 E2 mov rdx, rsp ; ubp_av
48 83 E4 F0 and rsp, 0FFFFFFFFFFFFFFF0h
50 push rax
54 push rsp ; stack_end
49 C7 C0 20 12 40 00 mov r8, offset __libc_csu_fini ; fini
48 C7 C1 B0 11 40 00 mov rcx, offset __libc_csu_init ; init
48 C7 C7 56 11 40 00 mov rdi, offset main ; main
FF 15 52 2F 00 00 call cs:__libc_start_main_ptr

F4 hlt
_start endp

_start 中会调用 _libc_start_main,它的原型如下:

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

__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
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7fffffffdfc8
00:0000│ rsi 0x7fffffffdfc8 —▸ 0x7fffffffe31a ◂— 0x68792f656d6f682f ('/home/yh')
01:00080x7fffffffdfd0 —▸ 0x7fffffffe338 ◂— 0x33323100636261 /* 'abc' */
02:00100x7fffffffdfd8 —▸ 0x7fffffffe33c ◂— 0x5f48535300333231 /* '123' */
03:00180x7fffffffdfe0 ◂— 0x0
04:0020│ rdx 0x7fffffffdfe8 —▸ 0x7fffffffe340 ◂— 'SSH_AUTH_SOCK=/run/user/1000/keyring/ssh'
05:00280x7fffffffdff0 —▸ 0x7fffffffe369 ◂— 'SESSION_MANAGER=local/yhellow-virtual-machine:@/tmp/.ICE-unix/1824,unix/yhellow-virtual-machine:/tmp/.ICE-unix/1824'
06:00300x7fffffffdff8 —▸ 0x7fffffffe3dd ◂— 'GNOME_TERMINAL_SCREEN=/org/gnome/Terminal/screen/f60ef9cf_9126_4c39_b1ee_af08e3e804a7'
07:00380x7fffffffe000 —▸ 0x7fffffffe433 ◂— 'SSH_AGENT_PID=1557'

envp 建立了之后,__libc_start_main 函数会使用相同的小技巧,越过 envp 数组之后的 NULL 字符,获取另一个向量:ELF 辅助向量(ELF 加载器使用它给进程传递一些信息)

  • ELF 辅助向量(Auxiliary Vectors)是 将某些内核级信息传输到用户进程的机制(例如:指向[系统调用]入口点的指针-AT_SYSINFO

设置环境变量 LD_SHOW_AUXV=1 就可以查看 ELF 辅助向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
exp LD_SHOW_AUXV=1 ./main 
AT_SYSINFO_EHDR: 0x7ffc4a3f3000
AT_??? (0x33): 0xe30
AT_HWCAP: f8bfbff
AT_PAGESZ: 4096
AT_CLKTCK: 100
AT_PHDR: 0x555c45893040
AT_PHENT: 56
AT_PHNUM: 13
AT_BASE: 0x7fd1cbb48000
AT_FLAGS: 0x0
AT_ENTRY: 0x555c458941c0
AT_UID: 1000
AT_EUID: 1000
AT_GID: 1000
AT_EGID: 1000
AT_SECURE: 0
AT_RANDOM: 0x7ffc4a3e5899
AT_HWCAP2: 0x2
AT_EXECFN: ./main
AT_PLATFORM: x86_64
  • 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 函数,并把 argcargv 参数、环境变量传递给它
  • 调用 exit 函数(会在其中调用 __libc_csu_fini),并将 main 函数的返回值传递给它

libc_csu_init & libc_csu_fini

对于任意的可执行程序都可以有一个C函数的“构造函数” __libc_csu_init 和C函数的“析构函数” __libc_csu_fini,在构造函数内部,可执行程序会找到全局C函数组成的构造函数集,并且调用它们

构造函数和析构函数是 Cpp 中的概念:

  • 构造函数:是一种特殊的函数,用来在对象实例化的时候初始化对象的成员变量
  • 析构函数:是构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动调用析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
class Line
{
public: /* 构造函数/析构函数都要放到public中 */
void setLength(double len);
double getLength(void);
Line(); /* 这是构造函数声明(与类名相同) */
~Line(); /* 这是析构函数声明(与类名相同,在前面加~) */
private:
double length;
};
Line::Line(void){ /* 构造函数的定义 */
cout << "Object is being created" << endl;
}
Line::~Line(void){ /* 析构函数的定义 */
cout << "Object is being deleted" << endl;
}
void Line::setLength(double len){
length = len;
}
double Line::getLength(void){
return length;
}

int main( )
{
Line line; /* 执行构造函数 */
line.setLength(6);
cout << "Length of line : " << line.getLength() <<endl;
return 0; /* 执行析构函数 */
}
1
2
3
Object is being created
Length of line : 6
Object is being deleted

C 语言没有构造函数和析构函数的概念,但 gcc 为函数提供了几种类型的属性,其中包含:

  • 构造函数(constructors):static void start(void) __attribute__ ((constructor))
  • 析构函数(destructors):static void stop(void) __attribute__ ((destructor))
  • 带有“构造函数”属性的函数将在 main 函数之前被执行,而声明为“析构函数”属性的函数则将在 main 退出时执行(其实就有点像针对 main 的构造函数)

源码如下:(根据 IDA 反编译出来的代码改的)

1
2
3
4
5
6
7
8
9
void _libc_csu_init(){
init_proc(); /* 初始化 */
const size_t size = &_do_global_dtors_aux_fini_array_entry - &_frame_dummy_init_array_entry;
if (size){
for (size_t i = 0LL; i != size; ++i)
/* 遍历并调用'_frame_dummy_init_array_entry+i'中的函数 */
(*(&_frame_dummy_init_array_entry + i))();
}
}
  • _frame_dummy_init_array_entry 中的数据如下:(提前写好了“构造函数”)
1
2
3
4
.init_array:0000000000003D88 40 11 00 00 00 00 00 00       __frame_dummy_init_array_entry dq offset frame_dummy
.init_array:0000000000003D90 49 11 00 00 00 00 00 00 dq offset a_constructor
.init_array:0000000000003D98 60 11 00 00 00 00 00 00 dq offset b_constructor
.init_array:0000000000003DA0 77 11 00 00 00 00 00 00 dq offset c_constructor
  • _do_global_dtors_aux_fini_array_entry 中的数据如下:(提前写好了“析构函数”)
1
2
3
4
5
.fini_array:0000000000003DA8 00 11 00 00 00 00 00 00       __do_global_dtors_aux_fini_array_entry dq offset __do_global_dtors_aux
.fini_array:0000000000003DB0 8E 11 00 00 00 00 00 00 dq offset A_destructor
.fini_array:0000000000003DB8 A5 11 00 00 00 00 00 00 dq offset B_destructor
.fini_array:0000000000003DC0 BC 11 00 00 00 00 00 00 dq offset C_destructor

main 函数执行前后的流程

函数调用关系图:

  • 首先程序调用 execve 生成一个进程,并且设置好数据
  • 然后调用 _start 简单设置后就调用 __libc_start_main
  • __libc_start_main 会先调用 __libc_init_first 获取环境变量 envp(作为 main 的第三个参数),然后越过 envp 数组之后的 NULL 字符,获取 ELF 辅助向量
  • finirtld_fini 作为参数传递给 at_exit 调用
  • 调用其 init 参数,执行 __libc_csu_init
  • 调用 main 函数,并把 argcargv 参数、环境变量传递给它
  • 调用 exit 函数,执行其中的 __libc_csu_fini