Linux中程序是怎样启动的

2022/9/11 5:23:23

本文主要是介绍Linux中程序是怎样启动的,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Linux中程序是怎样启动的

前言

  • 新程序的启动往往是通过libc中exe()系列函数进行的, exe系列函数最终都可以归纳为execve这个系统调用
  • 系统层面
    • kernel会检查这个文件的类型
    • 确定是elf之后会为新进程分配页表, 文件描述符, task描述符等各种资源
    • 然后解析这个elf文件, 把text data bss等段都映射到内存中
    • 然后jmp到elf的入口点, 从而开始执行

ELF的入口点_start()

  • 由于现代ELF使用运行时重定位机制。即:运行钱各种libc库函数的地址,编译时不知道libc会被mmap到哪

  • 因此如果kernel发现需要运行时重定位, 那么就会转而jmp到这个elf指定的动态链接器(也就是常用的ld.so.2), 由ld去重定位elf中相关地址后再jmp到elf的入口点

  • 但是ld并不是直接执行main()函数, 因为有析构函数就必定有构造函数, 在进入main之前还需要进行参数设置, 申请流缓冲区等操作

    实际上ld会跳转到elf中的_start标号处, 这才是elf中第一个被执行的指令地址

  • _start标号处的程序由汇编编写, 对应libc中start.S文件,

    • _start做的工作很少, 只会为__libc_start_main()设置好参数, 然后调用
    • _start()会在编译的时候被链接入ELF文件中
    • libc_start_main()定义在libc中, _start()通过PLT+GOT调用到libc_start_main()
ENTRY (_start)    /* 编译时告诉链接器, 这里才是整个函数的入口点 */

    /* Clearing frame pointer is insufficient, use CFI.  */
    cfi_undefined (rip)

    /* 初始化栈底: %ebp=0 */
    xorl %ebp, %ebp

    /* 设置__libc_start_main的参数
       调用__libc_start_main的参数会通过如下寄存器传递, 因为linux才用cdecl函数调用约定:
    main:        %rdi
    argc:        %rsi
    argv:        %rdx
    init:        %rcx
    fini:        %r8
    rtld_fini:    %r9
    stack_end:    stack.    */

    mov %RDX_LP, %R9_LP    /* 设置参数rtld_fini */

#ifdef __ILP32__
    mov (%rsp), %esi    /* Simulate popping 4-byte argument count.  */
    add $4, %esp
#else
    popq %rsi        /* Pop argc */
#endif

    /* 设置参数argv */
    mov %RSP_LP, %RDX_LP
    /* rsp对齐 */
    and  $~15, %RSP_LP

    /* Push garbage because we push 8 more bytes.  */
    pushq %rax

    /* Provide the highest stack address to the user code (for stackswhich grow downwards).  */
    pushq %rsp

#ifdef SHARED
    /* 设置参数init和fini */
    mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP
    mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP

    /* 设置参数main函数地址 */
    mov main@GOTPCREL(%rip), %RDI_LP

    /*     调用__libc_start_main() 
        __libc_start_main()进行一些构造工作, 然后调用main()
        main() return到__libc_start_main之后 __libc_start_main会进行析构工作 */
    call __libc_start_main@PLT
#else
    /* Pass address of our own entry points to .fini and .init.  */
    mov $__libc_csu_fini, %R8_LP
    mov $__libc_csu_init, %RCX_LP

    mov $main, %RDI_LP

    /* Call the user's main function, and exit with its value.
       But let the libc call main.      */
    call __libc_start_main
#endif

    hlt            /* Crash if somehow `exit' does return.     */
END (_start)
  • 由此注册析构函数的关键点就在__libc_start_main()中

__libc_start_main

这个函数定义在libc源码的libc-start.c文件中, 由于比较复杂, 因此只分析关键部分

  • 首先是其参数列表也就是_start()传递的参数, 我们中重点注意下面三个
    • init: ELF文件 也就是main()的构造函数
    • fini: ELF文件 也就是main()的析构函数
    • rtld_fini: 动态链接器的析构函数
static int __libc_start_main(
                int (*main)(int, char **, char **MAIN_AUXVEC_DECL), //参数: main函数指针
                int argc, char **argv,                              //参数: argc argv
                ElfW(auxv_t) * auxvec,
                __typeof(main) init,     //参数: init ELF的构造函数
                void (*fini)(void),      //参数: fini ELF的析构函数
                void (*rtld_fini)(void), //参数: rtld_fini ld的析构函数
                void *stack_end         //参数: 栈顶
        )
{
    ...函数体;
}
  • 进入函数体, __libc_start_mian()主要做了以下几件事
    • 为libc保存一些关于main的参数, 比如__environ…
    • 通过atexit()注册fini 与 rtld_fini 这两个参数
    • 调用init为main()进行构造操作
    • 然后调用main()函数

_dl_fini()

  • 我们把进程空间中的一个单独文件, 称之为模块
  • ld.so.2会通过dl_open()把所需文件到进程空间中, 他会把所有映射的文件都记录在结构体_rtld_global中
  • 当一个进程终止, ld.so.2自然需要卸载所映射的模块, 这需要调用每一个非共享模块的fini_arrary段中的析构函数
  • 一言以蔽之: _dl_fini()的功能就是调用进程空间中所有模块的析构函数

_dl_fini的任务:

  • 遍历rtld_global中所有的命名空间
  • 遍历命名空间中所有的模块
  • 找到这个模块的fini_array段, 并调用其中的所有函数指针
  • 找到这个模块的fini段, 调用
void internal_function _dl_fini(void)
{
#ifdef SHARED
    int do_audit = 0;
again:
#endif
    for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) //遍历_rtld_global中的所有非共享模块: _dl_ns[DL_NNS]
    {
        __rtld_lock_lock_recursive(GL(dl_load_lock)); //对rtld_global上锁

        unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
        /* 如果这个NameSapce没加载模块, 或者不需要释放, 就不需要做任何事, 就直接调用rtld中的函数指针释放锁 */
        if (nloaded == 0 || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit)
            __rtld_lock_unlock_recursive(GL(dl_load_lock));
        else //否则遍历模块
        {
            /* 把这个命名空间中的所有模块指针, 都复制到maps数组中  */
            struct link_map *maps[nloaded];
            unsigned int i;
            struct link_map *l;
            assert(nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
            for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next) //遍历链表
                if (l == l->l_real)                                                /* Do not handle ld.so in secondary namespaces.  */
                {
                    assert(i < nloaded);

                    maps[i] = l;
                    l->l_idx = i;
                    ++i;

                    /* Bump l_direct_opencount of all objects so that they are not dlclose()ed from underneath us.  */
                    ++l->l_direct_opencount;
                }
            ...;
            unsigned int nmaps = i;    //多少个模块

            /* 对maps进行排序, 确定析构顺序 */
            _dl_sort_fini(maps, nmaps, NULL, ns);

            //释放锁
            __rtld_lock_unlock_recursive(GL(dl_load_lock));    

            /* 从前往后, 析构maps中的每一个模块 */
            for (i = 0; i < nmaps; ++i)
            {
                struct link_map *l = maps[i];

                if (l->l_init_called)
                {
                    /* Make sure nothing happens if we are called twice.  */
                    l->l_init_called = 0;

                    /* 是否包含fini_array节, 或者fini节 */
                    if (l->l_info[DT_FINI_ARRAY] != NULL || l->l_info[DT_FINI] != NULL)
                    {
                        /* debug时打印下相关信息 */
                        if (__builtin_expect(GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0))
                            _dl_debug_printf("\ncalling fini: %s [%lu]\n\n",DSO_FILENAME(l->l_name),ns);

                        /* 如果有fini_array节的话 */
                        if (l->l_info[DT_FINI_ARRAY] != NULL)
                        {
                            /*
                                l->l_addr: 模块l的加载基地址
                                l->l_info[DT_FINI_ARRAY]: 模块l中fini_array节的描述符
                                l->l_info[DT_FINI_ARRAY]->d_un.d_ptr: 模块l中fini_arrary节的偏移
                                array: 为模块l的fini_array节的内存地址
                            */
                            ElfW(Addr) *array = (ElfW(Addr) *)(l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);

                            /* 
                                ELF中 fini_arraysz节用来记录fini_array节的大小
                                l->l_info[DT_FINI_ARRAYSZ]: 模块l中fini_arraysz节描述符
                                l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val: 就是fini_array节的大小, 以B为单位
                                i: fini_array节的大小/一个指针大小, 即fini_array中有多少个析构函数
                            */
                            unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof(ElfW(Addr)));
                            while (i-- > 0)    //从后往前, 调用fini_array中的每一个析构函数
                                ((fini_t)array[i])();
                        }

                        /* 调用fini段中的函数 */
                        if (l->l_info[DT_FINI] != NULL)
                            DL_CALL_DT_FINI(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
                    }

                    ...;
                }

                /* Correct the previous increment.  */
                --l->l_direct_opencount;
            }
        }
    }

    ...;
}


这篇关于Linux中程序是怎样启动的的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程