当前位置 博文首页 > astrotycoon:Life Cycle of a Linux Program

    astrotycoon:Life Cycle of a Linux Program

    作者:[db:作者] 时间:2021-08-06 13:07

    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?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”:

    事实上,提到“程序的生命周期”,我们至少需要考虑到两个方面:

    1. The development life cycle (requirements, design, code, test, deploy…)
      首先是程序的开发周期(提出需求,设计,编码,测试,部署......)
    2. 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:

    下一篇:没有了