从shell 到main() — 剖析应用程序的启动与执行

本文分阶段介绍从shell 执行一条命令运行应用程序到应用程序main() 函数被调用过程中发生了什么。

0 起因

用ftrace 抓了一段程序的页面故障(page fault)的异常,其目的是为了查看一个简单程序哪里会产生页面故障(使用strace 也大致能查看到,但无法精确定位到那个语句产生了)。这个脚本大致如下:

cd /sys/kernel/debug/tracing
echo 1 > events/exceptions/enable   # 使能监控异常
echo 1 > events/syscalls/enable     # 方便起见,同时监控系统调用
echo nop > current_tracer           # 不记录函数调用
echo 8 > tracing_cpumask            # 监控 isolated CPU2
echo  > trace                       # 记录前先清空文件
echo 1 > tracing_on                 # 开始记录
sudo taskset -c 2 /path/to/a.out    # run on CPU2
cat trace

测试程序源码如下:

#include <unistd.h>

int main() {
  write(0, "hello\n", 6);
  return 0;
}

trace 输出的内容如下:

# tracer: nop
#
# entries-in-buffer/entries-written: 34/34   #P:8
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
           a.out-12405   [003] ...1 89386.976089: sys_sched_setaffinity -> 0x0
           a.out-12405   [003] ...1 89386.976091: sys_execve(filename: 7fffffffe7ef, argv: 7fffffffe5a0, envp: 7fffffffe5b0)
           a.out-12405   [003] d... 89386.976143: page_fault_kernel: address=0x555555558010 ip=__clear_user error_code=0x2
           a.out-12405   [003] d... 89386.976149: page_fault_kernel: address=0x7ffff7ffdff8 ip=__clear_user error_code=0x2
           a.out-12405   [003] ...1 89386.976154: sys_execve -> 0x0
           a.out-12405   [003] d... 89386.991695: page_fault_user: address=0x7ffff7fd0100 ip=0x7ffff7fd0100 error_code=0x14
           a.out-12405   [003] d... 89386.991697: page_fault_user: address=0x7ffff7ffc5e0 ip=0x7ffff7fd0e18 error_code=0x6
           a.out-12405   [003] d... 89386.991698: page_fault_user: address=0x7ffff7fcfb00 ip=0x7ffff7fd1128 error_code=0x4
           a.out-12405   [003] d... 89386.991699: page_fault_user: address=0x7ffff7feb700 ip=0x7ffff7feb700 error_code=0x14
           a.out-12405   [003] d... 89386.991701: page_fault_user: address=0x7ffff7ffe138 ip=0x7ffff7feb737 error_code=0x6
           a.out-12405   [003] d... 89386.991702: page_fault_user: address=0x7ffff7ff38b4 ip=0x7ffff7feb81a error_code=0x4
           a.out-12405   [003] ...1 89386.991705: sys_brk(brk: 0)
           a.out-12405   [003] ...1 89386.991705: sys_brk -> 0x555555559000
           a.out-12405   [003] ...1 89386.991707: sys_arch_prctl(option: 3001, arg2: 7fffffffe4c0)
           a.out-12405   [003] ...1 89386.991707: sys_arch_prctl -> 0xffffffffffffffea
           a.out-12405   [003] d... 89386.991707: page_fault_user: address=0x7ffff7ff1e20 ip=0x7ffff7ff1e20 error_code=0x14
           a.out-12405   [003] ...1 89386.991711: sys_access(filename: 7ffff7ff79e0, mode: 4)
           a.out-12405   [003] ...1 89386.991712: sys_access -> 0xfffffffffffffffe
           a.out-12405   [003] d... 89386.991712: page_fault_user: address=0x7fffffffdd80 ip=0x7ffff7fde54f error_code=0x6
...... 省略后续trace 记录

注意,address 是发生页面故障的内存地址,ip 是发生页面故障的指令地址,error_code 是发生页面故障的类型(究竟是内存不存在,还是权限,以及是读还是写等)ip 地址是运行时的,所以需要使用gdb 查看对应地址的指令。

那么从shell 执行该命令开始到main 函数运行,主要包括了1)shell 过程;2)动态连接器过程;3)用户程序过程。

shell 负责fork 出新的线程,即包裹有动态连接器代码的用户程序;

动态连接器过程,加载可执行文件依赖的共享对象文件,并进行符号重定位;

用户程序过程,其中包含了调用用户main 的过程。

注意:动态连接器过程和用户程序过程的开头都是_start,有些文章就把两者混淆了。

1 Shell 过程

解析命令行,比如找到命令的路径(ls -> /usr/bin/ls)等。

创建子进程fork(),新的子进程成为当前进程,并通过系统调用sys_execve,并将命令行中程序作为参数。

首先看trace 中前5条记录,即

           a.out-12405   [003] ...1 89386.976089: sys_sched_setaffinity -> 0x0
           a.out-12405   [003] ...1 89386.976091: sys_execve(filename: 7fffffffe7ef, argv: 7fffffffe5a0, envp: 7fffffffe5b0)
           a.out-12405   [003] d... 89386.976143: page_fault_kernel: address=0x555555558010 ip=__clear_user error_code=0x2
           a.out-12405   [003] d... 89386.976149: page_fault_kernel: address=0x7ffff7ffdff8 ip=__clear_user error_code=0x2
           a.out-12405   [003] ...1 89386.976154: sys_execve -> 0x0

execv 相关函数位于 exec.c 文件中,其中 do_execveat()函数主要执行过程如下(基于5.4 内核)

  • do_execveat_common(fd, filename, argv, envp, flags);
  • exec.c 文件__do_execve_file() 函数retval = exec_binprm(bprm);
  • exec.c 文件exec_binprm()函数ret = search_binary_handler(bprm);
  • exec.c 文件search_binary_handler()函数 fmt->load_binary(bprm);
  • binfmt_elf.c文件load_elf_binary()函数(解析elf header,分配地址空间,加载程序内容,建立页表,解析动态链接库,重定位,初始化程序状态,跳转至程序入口)

此处内容请看内核相关代码。

2.1 libc-start/_start

首先看trace 记录中第三条,也就是第一条在用户态发生的页面故障

a.out-5309    [003] d... 30851.627208: page_fault_user: address=0x7ffff7fd0100 ip=0x7ffff7fd0100 error_code=0x14

其发生的指令地址和地址相同,说明这是一条指令页故障,使用gdb 查看对应地址(0x7ffff7fd0100)执行的指令:

(gdb) x/5i 0x7ffff7fd0100
   0x7ffff7fd0100 <_start>:	mov    %rsp,%rdi
   0x7ffff7fd0103 <_start+3>:	callq  0x7ffff7fd0df0 <_dl_start>
   0x7ffff7fd0108 <_dl_start_user>:	mov    %rax,%r12
   0x7ffff7fd010b <_dl_start_user+3>:	mov    0x2c4e7(%rip),%eax        # 0x7ffff7ffc5f8 <_dl_skip_args>
   0x7ffff7fd0111 <_dl_start_user+9>:	pop    %rdx

这就是execve() 返回用户态调用的动态链接器的执行位置,源码可参考glibc 的sysdeps/x86_64/dl-machine.h 文件

#define RTLD_START asm ("\n\
.text\n\
	.align 16\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
	movq %rsp, %rdi\n\
	call _dl_start\n\
_dl_start_user:\n\
	# Save the user entry point address in %r12.\n\
	movq %rax, %r12\n\

需要将这里动态链接库的_start 和用户程序的_start 区分开来。

2.2 _dl_start

接着看下一处页面故障

a.out-12405   [003] d... 89386.991697: page_fault_user: address=0x7ffff7ffc5e0 ip=0x7ffff7fd0e18 error_code=0x6

查看ip 地址执行的什么

(gdb) x/5i 0x7ffff7fd0e18
   0x7ffff7fd0e18 <_dl_start+40>:	mov    %rax,0x2b7c1(%rip)        # 0x7ffff7ffc5e0 <start_time>
   0x7ffff7fd0e1f <_dl_start+47>:	mov    0x2c042(%rip),%rax        # 0x7ffff7ffce68
   0x7ffff7fd0e26 <_dl_start+54>:	mov    %rdx,%r12
   0x7ffff7fd0e29 <_dl_start+57>:	sub    0x2c1d0(%rip),%r12        # 0x7ffff7ffd000
   0x7ffff7fd0e30 <_dl_start+64>:	mov    %rdx,0x2cbc1(%rip)        # 0x7ffff7ffd9f8 <_rtld_global+2456>

这里的页面故障是因为从动态链接库入口_start 处调用了_dl_start (位于elf/rtld.c) ,而_dl_start 中修改了start_time 对应地址的值引发了页面故障,(对应语句rtld_timer_start (&start_time);)。

下面不再详细追踪剩余trace 中的页面故障函数(及其对应地址的指令与函数,如_dl_sysdep_start 等)。

3.1 用户程序 _start

书接上回,先看看动态链接器的_start 最后跳转执行什么(这是在页面故障和系统调用的trace 中看不到的),我们在_start 指令所在地址打断点(地址为0x7ffff7fd0100):

(gdb) x/100i 0x7ffff7fd0100
=> 0x7ffff7fd0100 <_start>:	mov    %rsp,%rdi
   0x7ffff7fd0103 <_start+3>:	callq  0x7ffff7fd0df0 <_dl_start>
   0x7ffff7fd0108 <_dl_start_user>:	mov    %rax,%r12
   0x7ffff7fd010b <_dl_start_user+3>:	mov    0x2c4e7(%rip),%eax        # 0x7ffff7ffc5f8 <_dl_skip_args>
   0x7ffff7fd0111 <_dl_start_user+9>:	pop    %rdx
   0x7ffff7fd0112 <_dl_start_user+10>:	lea    (%rsp,%rax,8),%rsp
   0x7ffff7fd0116 <_dl_start_user+14>:	sub    %eax,%edx
   0x7ffff7fd0118 <_dl_start_user+16>:	push   %rdx
   0x7ffff7fd0119 <_dl_start_user+17>:	mov    %rdx,%rsi
   0x7ffff7fd011c <_dl_start_user+20>:	mov    %rsp,%r13
   0x7ffff7fd011f <_dl_start_user+23>:	and    $0xfffffffffffffff0,%rsp
   0x7ffff7fd0123 <_dl_start_user+27>:	mov    0x2cf36(%rip),%rdi        # 0x7ffff7ffd060 <_rtld_global>
   0x7ffff7fd012a <_dl_start_user+34>:	lea    0x10(%r13,%rdx,8),%rcx
   0x7ffff7fd012f <_dl_start_user+39>:	lea    0x8(%r13),%rdx
   0x7ffff7fd0133 <_dl_start_user+43>:	xor    %ebp,%ebp
   0x7ffff7fd0135 <_dl_start_user+45>:	callq  0x7ffff7fe0c20 <_dl_init>
   0x7ffff7fd013a <_dl_start_user+50>:	lea    0x10c1f(%rip),%rdx        # 0x7ffff7fe0d60 <_dl_fini>
   0x7ffff7fd0141 <_dl_start_user+57>:	mov    %r13,%rsp
   0x7ffff7fd0144 <_dl_start_user+60>:	jmpq   *%r12
   ......

这段代码最后跳转到%r12 寄存器指向的位置(其实也就是用户程序对应的_start),看看是什么

(gdb) info registers 
......
r12            0x555555555060      93824992235616
......
(gdb) x/20i 0x555555555060
0x555555555060 <_start>: endbr64
0x555555555064 <_start+4>: xor %ebp,%ebp
0x555555555066 <_start+6>: mov %rdx,%r9
0x555555555069 <_start+9>: pop %rsi
0x55555555506a <_start+10>: mov %rsp,%rdx
0x55555555506d <_start+13>: and $0xfffffffffffffff0,%rsp
0x555555555071 <_start+17>: push %rax
0x555555555072 <_start+18>: push %rsp
0x555555555073 <_start+19>: lea 0x166(%rip),%r8 # 0x5555555551e0 <__libc_csu_fini>
0x55555555507a <_start+26>: lea 0xef(%rip),%rcx # 0x555555555170 <__libc_csu_init>
0x555555555081 <_start+33>: lea 0xc1(%rip),%rdi # 0x555555555149
0x555555555088 <_start+40>: callq *0x2f52(%rip) # 0x555555557fe0
0x55555555508e <_start+46>: hlt

这里的_start 是用户程序的,从执行代码内容可以看出和动态链接库_start 不同,用户程序的_start 是GCC 编译器在生成可执行文件时添加进去的,位置在代码段.text 的开头,最先执行,可以使用objdump 查看其汇编代码

#objdump -d a.out
省略其他段
Disassembly of section .text:

0000000000001060 <_start>:
    1060:	f3 0f 1e fa          	endbr64 
    1064:	31 ed                	xor    %ebp,%ebp
    1066:	49 89 d1             	mov    %rdx,%r9
    1069:	5e                   	pop    %rsi
    106a:	48 89 e2             	mov    %rsp,%rdx
    106d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
    1071:	50                   	push   %rax
    1072:	54                   	push   %rsp
    1073:	4c 8d 05 66 01 00 00 	lea    0x166(%rip),%r8        # 11e0 <__libc_csu_fini>
    107a:	48 8d 0d ef 00 00 00 	lea    0xef(%rip),%rcx        # 1170 <__libc_csu_init>
    1081:	48 8d 3d c1 00 00 00 	lea    0xc1(%rip),%rdi        # 1149 <main>
    1088:	ff 15 52 2f 00 00    	callq  *0x2f52(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    108e:	f4                   	hlt    
    108f:	90                   	nop
省略其他段

_start 的一个重要任务就是调用__libc_start_main

3.2 __libc_start_main

__lib_start_main 也是定义在C 库(glibc),在我使用的glibc 2.31 版本中,其函数接受7 个参数的输入

define LIBC_START_MAIN __libc_start_main
STATIC int LIBC_START_MAIN ( int (*main) (int, char **, char ** MAIN_AUXVEC_DECL), 
                             int argc, 
                             char **argv,  
                             __typeof (main) init, 
                             void (*fini) (void), 
                             void (*rtld_fini) (void), 
                             void *stack_end) {

上面用户程序的_start 函数的主要功能就是初始化调用__libc_start_main 函数的参数栈,在X86-64 架构中,前6个参数通过寄存器来传递,我们看看都是啥

(gdb) disassemble _start
Dump of assembler code for function _start:
   0x0000555555555060 <+0>:	endbr64 
   0x0000555555555064 <+4>:	xor    %ebp,%ebp
   0x0000555555555066 <+6>:	mov    %rdx,%r9
   0x0000555555555069 <+9>:	pop    %rsi
   0x000055555555506a <+10>:	mov    %rsp,%rdx
   0x000055555555506d <+13>:	and    $0xfffffffffffffff0,%rsp
   0x0000555555555071 <+17>:	push   %rax
   0x0000555555555072 <+18>:	push   %rsp
   0x0000555555555073 <+19>:	lea    0x166(%rip),%r8        # 0x5555555551e0 <__libc_csu_fini>
   0x000055555555507a <+26>:	lea    0xef(%rip),%rcx        # 0x555555555170 <__libc_csu_init>
   0x0000555555555081 <+33>:	lea    0xc1(%rip),%rdi        # 0x555555555149 <main>
=> 0x0000555555555088 <+40>:	callq  *0x2f52(%rip)        # 0x555555557fe0
   0x000055555555508e <+46>:	hlt    
End of assembler dump.
(gdb) inf reg
rdi            0x555555555149      93824992235849            # <main> 函数
rsi            0x1                 1                         # argc
rdx            0x7fffffffe598      140737488348568           # **argv
rcx            0x555555555170      93824992235888            # <__libc_csu_init>
r8             0x5555555551e0      93824992236000            # <__libc_csu_finit>
r9             0x7ffff7fe0d60      140737354009952           # <_dl_fini>
// 省略其他寄存器的值

从寄存器保存的数值可以看出传递的参数信息。其中rdx 寄存器指向的是argv,保存了命令行参数

(gdb) x/gx 0x7fffffffe598
0x7fffffffe598: 0x00007fffffffe7eb
(gdb) x/20c 0x00007fffffffe7eb
0x7fffffffe7eb:	47 '/'	104 'h'	111 'o'	109 'm'	101 'e'	47 '/'	102 'f'	111 'o'
0x7fffffffe7f3:	111 'o'	108 'l'	47 '/'	116 't'	109 'm'	112 'p'	47 '/'	97 'a'
0x7fffffffe7fb:	46 '.'	111 'o'	117 'u'	116 't'
(gdb) x/s *(char **) (0x7fffffffe598)
0x7fffffffe7eb:	"/home/fool/tmp/a.out"

__libc_start_main 主要流程包括:

  • 调用 __libc_csu_fini 和 __libc_csu_init 等初始化
  • 调用应用程序main
  • 调用exit 退出程序。

至此结束。

无论是动态链接库的_start 还是__libc_start_main 都是非常复杂的过程,没办法一句话,一篇文章讲清楚,实属遗憾。

参考:

https://www.gnu.org/software/hurd/glibc/startup.html

https://stackoverflow.com/questions/62709030/what-is-libc-start-main-and-start

Linux x86 Program Start Up (dbp-consulting.com)

https://tldp.org/LDP/LG/issue84/hawk.html

https://stackoverflow.com/questions/9885545/how-to-find-the-main-functions-entry-point-of-elf-executable-file-without-any-s

https://www.cnblogs.com/jiqingwu/p/linux_binary_load_and_run.html