Life Cycle of a Linux Program
一个程序的生与死(linux平台)
This is an investigation of the life cycle of a program in a Linux system.
本篇文章我将讨论的是一个程序在linux操作系统上的生命周期。
Actually, there are two (at least) meanings of “program life cycle”:
事实上,提到“程序的生命周期”,我们至少需要考虑到两个方面:
- The development life cycle (requirements, design, code, test, deploy…)
首先是程序的开发周期(提出需求,设计,编码,测试,部署......) - The execution life cycle of a program when it is run.
其次是程序的运行周期。
We will discuss the latter. How is a new program begun? How is it ended? (What happens in between is largely up to the program itself.)
本文讨论的是程序的运行周期。不知道大家有没有想过,一个程序是如何开始的? 又是如何结束的呢?(至于从开始到结束期间做了什么当然是取决于程序自身的操作了)
The discussion that follows assumes you have some familiarity with C programming, since our sample program was written in C and we will be examining some C code. It also assumes you have some familiarity with the Linux application programming interface.
接下来的讨论,需要你具备一定的C基础以及简单了解linux操作系统的系统调用接口。
It also assumes you have some familiarity with programming and running in a Linux environment.
如果你具有编程经验,还熟悉linux环境那就更好了!
All source file references are relative to the root of the run-time library source tree and are specific to Fedora 17, the system from which I got this information.
接下来的所有操作都是基于Fedora 17平台。
This article describes what happens on an x86 (aka Intel IA-32) processor. For other processor types, the machine instructions will be different but the concepts are the same.
本篇文章主要针对的是x86处理器(也就是IA-32)。对于其他处理器,对应的机器码肯定是不同的,但是概念都是相同的。
Sample Program 测试程序
Here is the program we will be investigating. It is the familiar “hello world” program:
下面是大家熟知的”hello world“程序,我们就用这个简单的程序来展开后续的讨论:
#include <stdio.h>
int main(void)
{
printf("hello world\n");
return 0;
}
As you can see this program displays a simple message to standard output.
程序做的事情很简单,仅仅在标准输出上显示一串信息。
As you can also see this program uses one C library call, printf Typically, such functions are not linked into the executable file during the compilation process. Instead they are linked (i.e. added to the program) at run time, once the program is started. The library code comes from a separate file, “libc.so”. (The actual name on my system includes a version number: libc-2.19.so.) More complex programs may have many more such shared libaries, each coming from a separate “.so” file. Adding these libraries to your program is known as “late binding” or “run-time binding”. We’ll see how this is done momentarily.
该程序调用了一个C标准库函数printf。但是你知道吗? 在最终生成的可执行文件中其实是不包含这个函数的代码的,那不禁要问了:printf函数代码在哪里呢?既然是C标准库函数,那一定是在C库libc.so中(在具体的系统中,C库名字中会带上具体的版本号,例如libc-2.19.so)。待到程序运行时需要调用printf的时候,动态链接器才会动态地在libc.so中找到printf函数。我们把运行时才会去解析出函数地址的机制称为”延迟绑定“,或者叫”运行时绑定“。具体的细节下文会讲到。
Compiling the Program 编译以及反编译程序
We used the following gcc command to build the program:
使用GCC编译程序:
$ gcc -o hello hello.c
This command creates the executable file, “hello”. This file contains the machine-language code for our program which we can examine with the objdump command:
生成一个名字叫“hello”的可执行文件。在这个文件中就包含了我们程序最终的机器指令,我们可以通过objdump来一瞥究竟:
$ objdump -d hello
hello: file format elf32-i386
...
Disassembly of section .text:
08048350 <_start>:
8048350: 31 ed xor %ebp,%ebp
8048352: 5e pop %esi
8048353: 89 e1 mov %esp,%ecx
8048355: 83 e4 f0 and $0xfffffff0,%esp
8048358: 50 push %eax
8048359: 54 push %esp
804835a: 52 push %edx
804835b: 68 e0 84 04 08 push $0x80484e0
8048360: 68 70 84 04 08 push $0x8048470
8048365: 51 push %ecx
8048366: 56 push %esi
8048367: 68 4d 84 04 08 push $0x804844d
804836c: e8 cf ff ff ff call 8048340 <__libc_start_main@plt>
8048371: f4 hlt
...
0804844d <main>:
804844d: 55 push %ebp
804844e: 89 e5 mov %esp,%ebp
8048450: 83 e4 f0 and $0xfffffff0,%esp
8048453: 83 ec 10 sub $0x10,%esp
8048456: c7 04 24 00 85 04 08 movl $0x8048500,(%esp)
804845d: e8 be fe ff ff call 8048320 <puts@plt>
8048462: c9 leave
8048463: c3 ret
(<main> is the main function seen in our source file, hello.c, shown at the beginning of this article. <_start> is the program startup code that we’ll see again shortly.) There is other code as well, which I have deleted from the above output for clarity.
(为了显示的简洁,我特地删除了一部分无关紧要的输出。)观察到的<main>就是我们程序中的main函数,而<_start>是什么鬼?其实呢,它才是我们程序的真正入口点,下文很快就会提到。
Note that the compiler substituted “puts” for “printf” as an optimization.
GCC发现我们使用printf函数时仅仅是打印一串字符串,并没有使用任何格式限定符(format specifier),因此处于优化的目的,将printf函数替换成了puts函数。
In addition to the machine-language instructions for the program, the executable file includes information about the functions to be loaded at run time from the .so libraries:
可执行文件中,除了包含机器指令外,还包括其他很多信息,这其中就包括在运行时需要解析的符号信息(这些符号来自所依赖的动态库,相对应用程序来说就是外部符号)。
$ objdump -T hello
hello: file format elf32-i386
DYNAMIC SYMBOL TABLE:
00000000 DF *UND* 00000000 GLIBC_2.0 puts
00000000 w D *UND* 00000000 __gmon_start__
00000000 DF *UND* 00000000 GLIBC_2.0 __libc_start_main
080484fc g DO .rodata 00000004 Base _IO_stdin_used
We see puts as well as __libc_start_main, which we will encounter again shortly, and some other functions used internally by the C run-time library.
果然,可以看到有puts,其中的__libc_start_main下文很快会提到,其他的符号是C运行时库内部使用的。
Running the Program — the shell 在shell中运行程序
Normally the program would be started from a shell:
绝大多数情况下,我们都是通过shell来启动应用程序:
$ ./hello
Hello, World!
Every program needs its own process to run in. Therefore the shell will fork a child process:
每个应用程序运行时都对应一个进程。因此shell运行程序时,会通过fork创建一个全新的子进程:
while ((pid = fork ()) < 0 && errno == EAGAIN && forksleep < FORKSLEEP_MAX)
{
...handle EAGAIN error
}
The shell, running in the child process, will then call the execve system function to start the program:
shell创建成功子进程后,在子进程中会调用execve()系统调用来加载并运行应用程序:
execve (command, args, env);
Of course, not every program is started from a shell, but whatever program is used to start our program, the program that starts our program will very probably call fork and will certainly call one of the exec family of system calls.
当然了,系统中的进程并不都是通过shell启动的。但是无论是通过哪个程序启动的,原理肯定都是相同的:先调用fork创建一个子进程,然后在子进程中调用exec家族函数来加载并启动程序。
execve() System Call — the Kernel 内核通过execve()加载启动程序
In the kernel, the execve system call will create a new memory space for the new program and map the program file into memory.
execve()系统调用会为程序建立一个全新的内存空间,然后将程序映射到这块内存区域。
What do we mean by “map the program file into memory”? In the early days of computers, before the use of virtual memory, programs were actually “loaded” into memory, meaning the entire program file was copied from some storage device such disk, tape, or cards, into memory. For a large program this could take considerable time.
是不是很好奇这里说的“映射”是什么意思?其实这里的“映射”在计算机发展(主要是CPU和操作系统的发展)的不同阶段有着不同的含义。在早期,也就是在不支持虚拟内存的古老年代,所谓“映射”其实是将程序整个从外部存储介质(磁盘,磁带,纸带等)加载进内存。如此这般,当程序体积比较大时,整个加载的时间就会明显增长,或者也会出现内存不足的尴尬问题。
With the use of virtual memory it is only necessary for the kernel to construct data structures that specify where the various parts of the program should go in memory and where they should come from on disk. With this mechanism, only the portions of the program that are needed are copied into memory and only once they are needed. Some portions (such as error recovery routines) may never be needed and are thus never loaded into memory.
到了后来,CPU和操作系统都支持了虚拟内存之后,所谓“映射”不再是傻傻地将整个程序加载内存了,相对的,操作系统内核只需要在内存中建立相关的数据结构,用来标识程序的哪些部分需要加载进内存以及它们在磁盘上的具体位置。如此这般,在程序开始运行前只需要加载程序的一部分必要信息,然后运行过程中,需要哪些信息才会加载进内存,因此有可能程序的某些部分(例如程序的错误处理部分)永远不会加载进内存。
In order to map the .so library files, mentioned above, into memory the kernel maps one .so file , often called “ld.so” and referred to as “the dynamic loader” into the process’s memory.
现代应用程序一般会依赖若干共享库,为了映射这些共享库到内存中,execve()系统调用最后会映射一个名字叫“ld.so”的共享库,这个共享库就是大名鼎鼎的动态链接器。
(For more details about how the kernel handles the execve system call, see Understanding the Linux Kernel, 3rd Edition, by Daniel P. Bover, Chapter 20.)
(有关execve()的详细细节请自行参考《深入理解linux内核》第三版第20小节)
参考链接:《sys_execv源码分析》
The kernel then begins running the new program, starting with code in ld.so. This allows the loading of the additional .so files to be done from within ld.so in the user space instead of by the kernel.
execve()将动态链接器映射进内存后,就从内核态进入用户态,开始运行动态链接器的代码。动态链接器的任务之一是一一加载程序所依赖的共享库。
How does the kernel actually transfer control to ld.so? Normally when the kernel finishes a system call it goes through a return sequence which concludes with an iret (interrupt return) instruction or a sysexit instruction. That instruction restores the process’s next instruction address to the IP (Instruction Pointer) register so that the next time the CPU fetches an instruction it is from the instruction following the system call.
在对动态链接器展开讨论之前,不知道大家有没有这样一个疑惑:在绝大多数情况下,系统调用完成任务后就从内核态返回到用户态(通过iret或者sysexit指令),然后继续运行用户态的代码。但是这里execve()系统调用不但没有返回,反而将程序控制权交给了动态链接器,是不是很不寻常呢?
In this case however, the program that executed the execve is no longer in the process’s memory: it has been replaced by the new program. So the kernel “diddles” with the stack where the return address was stored such that when the iret or sysexit instruction is executed, control “returns” to the first instruction of the new program which in this case is the instruction labeled _start within ld.so.
原来execve()系统调用使用新的程序替换掉了旧的程序,旧的程序已经完全被覆盖掉了,因为execve()系统调用的目的就是使用新的程序替代旧的程序,然后开始运行新的程序。那么新的程序是如何开始运行的呢? 这里内核使用了一个比较巧妙的技巧:将新程序的首地址压入栈,这样待到运行iret或者sysexit指令后,自然开始运行新程序了。在这里,内核将动态链接器ld.so的首地址,也就是符号_start的地址压入栈。
The Dyamic Loader — the Start of user-mode execution
动态链接器--用户态程序的起点
ld.so begins with the following assembly language code (defined as the RTLD_START macro in sysdeps/i386/dl-machine.h.)
动态链接器的入口代码是如下汇编(以宏的方式定义在sysdeps/i386/dl-machine.h文件中):
#define RTLD_START asm (“\n\
…
_start:\n\
# Note that _dl_start gets the parameter in %eax.\n\
movl %esp, %eax\n\
call _dl_start\n\
_dl_start_user:\n\
# Save the user entry point address in %edi.\n\
movl %eax, %edi\n\
# Point %ebx at the GOT.\n\
call 0b\n\
addl $_GLOBAL_OFFSET_TABLE_, %ebx\n\
# See if we were run as a command with the executable file\n\
# name as an extra leading argument.\n\
movl _dl_skip_args@GOTOFF(%ebx), %eax\n\
# Pop the original argument count.\n\
popl %edx\n\
# Adjust the stack pointer to skip _dl_skip_args words.\n\
leal (%esp,%eax,4), %esp\n\
# Subtract _dl_skip_args from argc.\n\
subl %eax, %edx\n\
# Push argc back on the stack.\n\
push %edx\n\
# The special initializer gets called with the stack just\n\
# as the application’s entry point will see it; it can\n\
# switch stacks if it moves these contents over.\n\
” RTLD_START_SPECIAL_INIT “\n\
# Load the parameters again.\n\
# (eax, edx, ecx, *–esp) = (_dl_loaded, argc, argv, envp)\n\
movl _rtld_local@GOTOFF(%ebx), %eax\n\
leal 8(%esp,%edx,4), %esi\n\
leal 4(%esp), %ecx\n\
movl %esp, %ebp\n\
# Make sure _dl_init is run with 16 byte aligned stack.\n\
andl $-16, %esp\n\
pushl %eax\n\
pushl %eax\n\
pushl %ebp\n\
pushl %esi\n\
# Clear %ebp, so that even constructors have terminated backchain.\n\
xorl %ebp, %ebp\n\
# Call the function to run the initializers.\n\
call _dl_init_internal@PLT\n\
# Pass our finalizer function to the user in %edx, as per ELF ABI.\n\
leal _dl_fini@GOTOFF(%ebx), %edx\n\
# Restore %esp _start expects.\n\
movl (%esp), %esp\n\
# Jump to the user’s entry point.\n\
jmp *%edi\n\
This code begins by calling _dl_start which is written in C and is in debug/glibc-2.15-a316c1f/elf/rtld.c.
这段汇编一上来就调用了定义在rtld.c中_dl_start函数。
We can use strace (which traces system calls) to follow this startup code. Here is the output from that program:
我们可以使用strace来跟踪这段启动代码。
$ strace ./hello
...
brk(0) = 0x85b0000
The call to brk(0) is a “trick” to determine the location of the program’s heap. (The heap is the memory area used for dynamic memory by the program.)
brk(0)决定了程序堆(heap)的内存位置(堆就是程序中动态申请内存时的一片内存空间)。
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
This call to access looks for a file called “/etc/ld.so.nohwcap” but the call returns the error, ENOENT, meaning the file does not exist.
使用access()检测文件“/etc/ld.so.nohwcap”是否存在,返回值-1,错误码为ENOENT,意思就是文件不存在。
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7704000
This call to mmap2 requests 8K of additional memory from the kernel which maps it into address 0xb7704000.
使用mmap2申请了8K的内存空间,首地址为0xb7704000。
Note: There is a Linux security feature, called “address space layout randomization”, which is designed to deter certain forms of hacking by making it difficult to predict where code will reside. The result is that memory is allocated in different locations each time the program is run. For example, the above memory area was allocated by the kernel at 0xb7704000. However, on a previous run of the same program on the same system, this memory had been allocated at 0xb77b7000.
这里需要补充的一点是:linux拥有一个叫做“地址空间布局随机化”的安全机制,这样即使是同一程序,在每次启动时内核分配给它的地址空间也都是不一样的,地址空间的不可预测性会有效增加对程序攻击的难度。举例来说,本次hello程序地址空间的首地址是0xb7704000,而前一次运行时获得的地址却是0xb77b7000。
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
Looking for another file, “/etc/ld.so.preload” which also does not exist.
同样的,使用access()检测文件“/etc/ld.so.preload”是否存在,结果是不存在。
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=97882, ...}) = 0
mmap2(NULL, 97882, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb76ec000
close(3) = 0
The above four system calls result in mapping the file /etc/ld.so.cache into memory at 0xb76ec000.
以上4个系统调用是将文件/etc/ld.so.cache映射到内存0xb76ec000地址处。
- open() opens the file.
open() 打开文件。 - fstat64() returns, among other things, the size of the file (which will be used by the mmap2 call).
fstat64() 返回文件状态,其中包括文件的大小信息(接下来的mmap2需要用到)。 - mmap2() maps the file into memory.
mmap2() 映射整个文件到内存中。 - and close() closes the file.
close() 关闭文件。
ld.so.cache is a file that contains information about the location of system libraries within the file system. This file, which was created by the ldconfig utility program, is used to speed up the locating of standard shared libraries.
文件ld.so.cache是由ldconfig程序生成的,它包含了系统库文件的位置信息,目的是加快动态链接器对共享库文件的搜索速度。
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\340\233\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1754876, ...}) = 0
mmap2(NULL, 1759868, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb753e000
mmap2(0xb76e6000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a8000) = 0xb76e6000
mmap2(0xb76e9000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb76e9000
close(3) = 0
The above steps are where the C run-time library, libc.so, is actually mapped into memory. There are three calls to mmap2 for three portions of the file: executable instructions, constant data, and global variable data.
这几个步奏是将C库libc.so映射进内存。三个mmap2分别映射的部分是:代码段,只读数据段,和数据段。
(译者注:从0xb753e000 ~ 0xb76e6000 ~ 0xb76e9000 ~ 0xb76eba7c来看,第一个mmap2是申请了一个大空间,然后后面两个mmap2相应的映射了.rodata和.data)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb753d000
This system call adds an additional memory area immediately above the area used by the main program. This area will be used for various housekeeping information about the program.
这个mmap2申请的空间是给我们的程序申请的,这块空间会用来存储程序的诸多有用信息。
(译者注:最终的内存布局是这样的0xb753d000 ~(main program) 0xb753e000 ~(libc.so .text) 0xb76e6000 ~(libc.so(.rodata)) 0xb76e9000 ~(libc.so .data) 0xb76eba7c 0xb76ec000 ~(ld.so.cache) 0xb7703e5a 0xb7704000 ~(8K空间) 0xb7706000)
set_thread_area({entry_number:-1 -> 6, base_addr:0xb753d940, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
This call to set_thread_area tells the kernel to set up a TLS (Thread Local Storage) data area. Note that the address is within the memory area most recently allocated.
调用set_thread_area来设置线程私有数据(TLS)区域。注意到,这个区域是位于刚刚最后的mmap2申请的空间之内的。
mprotect(0xb76e6000, 8192, PROT_READ) = 0
mprotect(0x8049000, 4096, PROT_READ) = 0
mprotect(0xb7727000, 4096, PROT_READ) = 0
These mprotect statements change the memory protection to read-only for lib.so constants, the program’s constants, and ld.so’s own constants, respectively.
这三个mprotect分别改变libc.so、程序hello以及动态链接器中的只读数据区域为只读。
munmap(0xb76ec000, 97882) = 0
The memory area previously mapped from ld.so.cache is now removed from memory by munmap.
文件ld.so.cache已经不再需要,因此使用munmap来释放占用的内存空间。
At this point the shared libaries are mapped into memory. (In our case, there is only one, libc.so)
好了,到现在所有的共享库都已经映射到内存里了(因为我们的测试测试程序极其简单,只依赖于一个libc.so,因此只需映射一个共享库即可)。
Control returns from _ld_start to _start which falls through to _dl_user_start which continues with this code previously shown from the RTLD_START macro:
函数_ld_start返回到_start后,接着来到_dl_user_start部分:
# Pass our finalizer function to the user in %edx, as per ELF ABI.\n\
leal _dl_fini@GOTOFF(%ebx), %edx\n\
# Restore %esp _start expects.\n\
movl (%esp), %esp\n\
# Jump to the user’s entry point.\n\
jmp *%edi\n\
The jmp (jump) instruction at the end of the above code transfers to the entry point of our program. Our program’s entry point was passed from the kernel in the %eax register. It was previously moved to the %edi register by this code:
最后的jmp指令会将程序控制权交给我们的程序。刚刚调用的_dl_start的返回值就是我们程序的入口点,_dl_start返回后将这个入口点存储在了寄存器%edi中,因此这里jmp指令的操作数是*%edi。
# Save the user entry point address in %edi.\n\
movl %eax, %edi\n\
With the jmp instruction, our program now begins at a routine called _start. (Note this is not the same as the _start function in ld.so; each instance of _start is defined locally within its own module.)
我们程序的入口点是_start,注意它与动态链接器的入口_start仅仅是名字相同而已,实际的内存地址是不同的。
参考链接:《_dl_start_user源码分析(一)》
Beginning our Program Code — the C Run-time Library
通过C运行时库开始运行我们的程序
Upon entry to the program (at _start) the following information has been provided to the program:
在开始运行_start之前,可以肯定的有以下几点:
- The command-line arguments and environment variables are loaded into the top end of the stack memory area.
- The stack pointer is set just below the above data.
- argc and argv are then pushed onto the stack. These are the count and address of the command line arguments, respectively.
(The above three steps were done by the kernel as part of the execve processing.)
早在execve()阶段,运行动态链接器之前,内核已经将命令行参数和环境变量已经压入栈里,命令行参数的个数argc也压入栈里(位于栈顶的位置),栈指针指向栈顶。
Our program begins with:
以下是_start处的第一条指令:
0x8048ba8 <_start> xor %ebp,%ebp
The xor instruction shown above (the first instruction of the program) sets the %ebp register to zero. This register is used to keep track of stack frames used by C functions, and setting this value to zero means this is the end of the set of stack frames.
设置寄存器%ebp为值为0。%ebp寄存器是用来记录函数调用链的,这里设置为0,表示这里是函数调用链的开始处。
0x8048baa <_start+2> pop %esi
0x8048bab <_start+3> mov %esp,%ecx
The above instructions get argc and argv from the stack to the %esi and %ecx registers respectively.
这两条指令分别将argc和argv存储在寄存器%esi和%ecx中。
0x8048bad <_start+5> and $0xfffffff0,%esp
This makes sure the stack pointer is on a word boundary, i.e. on an address divisible by 16.
在接下来的给函数__libc_start_main压入参数之前,必须保证%esp是16字节对齐。
0x8048bb0 <_start+8> push %eax
0x8048bb1 <_start+9> push %esp <stack end>
0x8048bb2 <_start+10> push %edx <_dl_fini> [from ld.so]
0x8048bb3 <_start+11> push $0x8049340 <__libc_csu_fini>
0x8048bb8 <_start+16> push $0x80492a0 <__libc_csu_init>
0x8048bbd <_start+21> push %ecx <argv> [saved above]
0x8048bbe <_start+22> push %esi <argc> [saved above]
0x8048bbf <_start+23> push $0x8048ce0 <main>
0x8048bc4 <_start+28> call 0x8048d00 <__libc_start_main>
The above instructions push the arguments for the subsequent function call onto the stack, and then call __libc_start_main, the first C-language code in the program.
这一系列压栈操作是在为即将调用的C函数__libc_start_main传递参数,函数__libc_start_main是程序的第一个C函数。
0x8048bc9 <_start+33> hlt
This instruction would be executed if __libc_start_main returned to its caller, but that should never happen. If it did, hlt (halt) is a privileged instruction and will cause the program to fail.
__libc_start_main函数调用我们的main函数,然后调用exit结束进程。因此如果出错__libc_start_main返回的话,就会运行这里的hlt指令,但是我们知道hlt是特权级指令,因此在用户态会触发异常,导致程序挂掉。
The following code is from debug/glibc-2.15-a316c1f/csu/libc-start.c.
It shows the entry into __libc_start_main.
__libc_start_main函数位于csu/libc-start.c文件中:
/* Note: the fini parameter is ignored here for shared library. It
is registered with __cxa_atexit. This had the disadvantage that
finalizers were called in more than one place. */
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char *__unbounded *__unbounded ubp_av,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *__unbounded auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *__unbounded stack_end)
__libc_start_main (main=0x8048430 <main>, argc=1, ubp_av=0xbfffefa4,
init=0x8048450 <__libc_csu_init>, fini=0x80484c0 <__libc_csu_fini>,
rtld_fini=0x42bfaa90 <_dl_fini>, stack_end=0xbfffef9c) at libc-start.c:96
At this point a number of functions are called (not shown) that initialize the C run-time environment.
在__libc_start_main函数中,在调用我们的main函数之前,会做很多的初始化工作,为我们的程序运行准备好环境。
Beginning the main() Function 开始运行main()函数
Next we see the following code:
终于开始调用我们的main函数了:
/* Nothing fancy, just call the function. */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
#endif
exit (result);
The above call to main is where the user’s code is started. (All C programs begin with a function named “main”.)
At this point our program’s code, beginning at main() starts to execute at last. Our program does whatever it was programmed to do (in our case print a message as we saw at the top of this article).
调用main函数后,就可以运行我们的写的代码了,这里hello程序会在标准输出上打印一段信息。
Program exit — Back to the C Run-time Library main()函数结束,返回C运行库
The C program can terminate either by calling “exit” or by returning from the main function. Our example program (shown at the top of this article) does the latter. In that case the following code is executed from the run-time library after the return from main:
程序退出可以通过两种方式,第一是显示地调用exit函数,第二就是main()函数正常返回。我们的测试程序hello是通过第二种方式退出的。从main()函数返回后,其实是回到了C运行库中。
258 exit (result);
So either our program calls exit or the run-time library calls it for us after main returns.
可以看出,两种退出方式本质是一样的,都是调用exit()函数。
Many people think exit is a system call, but it is actually a C library function in debug/glibc-2.15-a316c1f/stdlib/exit.c:
很多人可能以为exit()是个系统调用,其实它是一个C标准库函数。
98 exit (int status)
99 {
100 __run_exit_handlers (status, &__exit_funcs, true);
101 }
As you can see, exit is very simple; it just calls __run_exit_handlers which looks like this:
正如你所见,exit()的实现极其简单,只是调用了函数__run_exit_handlers。
/* Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered
perform stdio cleanup, and terminate program execution with STATUS. */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit)
{
/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
...
_exit (status);
}
(The omitted code simply loops through calling a list of functions to handle process cleanup before it exits.)
函数__run_exit_handlers先遍历一个函数链表,分别调用函数做一些程序退出前的清理工作,最后调用_exit()系统调用退出。
The _exit() System Call — Back to the Kernel 调用_exit()返回内核
As you can see above, the last thing this function does is call _exit, which really is a system call, defined in debug/glibc-2.15-a316c1f/sysdeps/unix/sysv/linux/i386/_exit.S:
_exit()才是系统调用,它定义在文件sysdeps/unix/sysv/linux/i386/_exit.S中:
.text
.type _exit,@function
.global _exit
_exit:
movl 4(%esp), %ebx
/* Try the new syscall first. */
#ifdef __NR_exit_group
movl $__NR_exit_group, %eax
ENTER_KERNEL
#endif
/* Not available. Now the old one. */
movl $__NR_exit, %eax
/* Don't bother using ENTER_KERNEL here. If the exit_group
syscall is not available AT_SYSINFO isn't either. */
int $0x80
/* This must not fail. Be sure we don't return. */
hlt
.size _exit,.-_exit
This code places the intended system call number (NR_exit_group) into the %eax register to pass it to the kernel. It then invokes ENTER_KERNEL, which is an assembler macro that expands to this machine-language code:
先将数值NR_exit_group存储在寄存器%eax,目的是告诉内核本次系统调用是_exit()。接着调用ENTER_KERNEL。ENTER_KERNEL是一个宏,扩展开来是如下的指令:
0x42cc57ad <_exit+9> call *%gs:0x10
which calls this machine-language code, the typical sequence for calling a system service in the kernel.
下面是被call处的若干指令,是典型的陷入内核的指令。
0xb7fff414 <__kernel_vsyscall> push %ecx
0xb7fff415 <__kernel_vsyscall+1> push %edx
0xb7fff416 <__kernel_vsyscall+2> push %ebp
0xb7fff417 <__kernel_vsyscall+3> mov %esp,%ebp
0xb7fff419 <__kernel_vsyscall+5> sysenter
Usually the kernel returns to the user’s program when it is finished, but in the case of _exit the kernel does not return. Instead, it deletes the memory space occupied by our program. The process it was running in will be marked “defunct”.
跟前面提到的execve()的类似,_exit()也不会返回,不同的是,_exit()不再是将程序控制权传递给别的程序,而是内核通过_exit()删除进程的内核资源,例如删除进程所占据的内存空间等,并且将进程的状态设置为“僵尸”状态。
waitpid() — Back to the Shell SHELL通过waitpid()获得程序返回状态
The process will be completely deleted once its parent (usually the shell) gathers the defunct process’s completion status:
通过上面的分析我们知道,_exit()之后进程并没有完全消失,只有等到父进程通过waitpid()获取了子进程的退出状态之后,子进程才真正的从系统中消失了。
pid = waitpid (-1, &status, waitpid_flags);
Once waitpid is called by the process’s parent, both the child process and the program it was running are gone from memory.
父进程waitpid()之后,子进程的所有资源才真正意义上的彻底释放了。
Thus the program’s execution life cycle is ended.
至此,一个程序的生命周期就结束了。
Other Ways a Program May Terminate 程序结束的方式有很多
In addition to calling exit to terminate the process,, as was described above, there are alternative ways in which a program may terminate:
除了上文提到的调用exit()结束程序,其实程序结束的方式还有很多,例如:
- The process can be abnormally terminated due to an error or action by another program or by the user. (In that case the C library cleanup code by the exit() call will not be performed.)
程序由于异常退出。发生异常的原因有很多,有可能是程序内部出错导致的异常,也有可能是其他程序或者用户发送了终止信号。注意,异常退出时,跟exit()不同的是,退出之前不会调用相关的清理工作了。 - The program can issue another execve call which will start a new program in place of the current program.
程序可以调用execve()启动其他程序,那么原来的程序也就结束了。
Conclusion 文末总结
Here is a summary of the steps described in this article.
以下是一个程序完整生命周期每个阶段的简述:
- Frequently, but not always, some process (often a shell) forks a new process for the new program.
绝大多数情况下(例如init进程就是由内核启动的),想要运行一个新程序,会由一个进程(一般是shell程序)创建一个新进程。 - In the process that is to run the new program, the existing program calls execve.
在这个新的进程中调用execve()来加载和启动新的程序。 - The kernel releases the old program’s address space and begins building a new address space.
execve()会用创建一个全新的内存空间。 - The kernel maps the program into the new address space.
然后将新程序从磁盘映射到这块新的内存空间。 - If the program uses dynamic libraries, then the kernel maps ld.so into the new address space.
如果程序是动态链接的,那么内核首先会将ld.so映射到刚刚创建的内存空间。 - If the program uses dynamic libraries, the kernel gives control to ld.so within the process context of the new program. ld.so then causes any shared libraries to be mapped into memory.
然后内核将控制权交给动态链接器ld.so,ld.so在自我重定位之后,会将程序以来的所有动态库映射进内存。 - ld.so then transfers control to the new program for the first time at the label, _start.
之后,动态链接器会将控制权交给这个新程序,也就是开始运行_start符号处的指令。 - _start saves some input parameters from the system and then call _libc_start_main which initializes the C run time library.
_start首先从栈中获得参数,然后为即将调用的_libc_start_main准备参数,在调用main函数之前,_libc_start_main会做很多初始化工作。 - _libc_start_c_main calls main, the beginning of the application program code.
_libc_start_c_main最终会调用main函数,也就是应用程序的代码。 - The program runs until:
程序开始运行,直到发生以下任何一种情况,程序停止。(1)it terminates by calling exit or returning from the main function. (Continue to step 11.)
程序中显示调用exit()或者从main函数返回(跳转到步骤11)。 (2)it calls execve to begin a new program. (Go back to step 2.) - 程序调用execve()开始运行另一个新的程序(回到步骤2)。 (3)The process is abnormally terminated. (Skip to step 13.)
程序异常退出(跳转到步骤13)。 - The C run-time library cleans up.
程序在退出之前做相应的清理工作。 - The run-time library calls _exit, the system call that terminates the process.
最后调用_exit(),程序终止。 - The kernel releases the memory and other resources of the just-terminated process.
内核释放进程的相关资源。 - The parent process issues a wait call for the process that just terminated.
父进程通过wait函数获取子进程退出状态。 - The kernel releases the terminated process’s task structure, the last remnant of the program.
内核最后彻底释放进程的所有资源,至此程序不复从在。
Over ~~~
扩展阅读:
《Linux Programe/Dynamic Shared Library Entry/Exit Point && Glibc Entry Point/Function》