链接与装载---Linux下c语言main函数调用原理

目录

main函数调用过程说明

其他说明


 

main函数调用过程说明

在学习完链接过程控制之链接脚本这一节之后,我们知道,linux环境下可执行文件并不是从main开始执行的, 入口是在默认链接脚本中使用ENTRY()指定的, 通过执行命令'ld -verbose'可以看出,在linux下编译的应用程序,可执行文件入口为_start,它其实是定义在glibc库中的一个函数。

当我们在shell下执行./a.out的时候,

$ ./a.out
$

shell会首先调用fork系统调用来新建一个子进程( shell本身也是一个进程),然后该子进程会调用execve系统调用把目标程序elf文件加载到内存中,寻找到目标程序的入口地址,然后执行,整个过程如下:


(如果直接在命令行中执行:exec ./a.out,当a.out程序执行结束之后,执行命令的teminal也会关闭掉,因为该命令将shell进程替换掉了)

各个函数的核心代码如下:

SYSCALL_DEFINE3(execve,
                const char __user *, filename,
                const char __user *const __user *, argv,
                const char __user *const __user *, envp)
{
        return do_execve(getname(filename), argv, envp);
}

do_execve调用了do_execveat_common(), 在这个过程中, 函数中会检查文件是否存在,调用prepare_binprm()函数完成对文件前128字节的读取,目的是为了判断可执行文件的格式,每种可执行文件的开头几个字节都是很重要的,尤其是前四个字节魔数字(Magic Number),通过对前四个字节的判断,可以确定文件的类型和格式, 例如, ELF可执行文件的头4个字节为(分别为0X7f、0X45、0X4c、0X46,第一个对应ASCII字符里面的DEL控制符,后面3个字节是ELF这3个字母的ASCII码),我们可以用命令 readelf -h a.out 查看,如果被执行的是shell脚本或者python等解释型语言的脚本,那么第一行通常是(十六进制内容。。。),”#!/bin/sh”或”#!/usr/bin/python”,这时候前两个字节”#”和”!”构成构成了魔数,系统一旦判断到这两个字节,就对后面的字符串解析,以确定具体的解释程序的路径。

int do_execve(struct filename *filename,
        const char __user *const __user *__argv,
        const char __user *const __user *__envp)
{
        struct user_arg_ptr argv = { .ptr.native = __argv };
        struct user_arg_ptr envp = { .ptr.native = __envp };
        return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

do_execveat_common调用了__do_execve_file

// fs/exec.c
static int do_execveat_common(int fd, struct filename *filename,
                              struct user_arg_ptr argv,
                              struct user_arg_ptr envp,
                              int flags)
{
        return __do_execve_file(fd, filename, argv, envp, flags, NULL);
}

__do_execve_file调用了exec_binprm

// fs/exec.c
static int __do_execve_file(int fd, struct filename *filename,
                            struct user_arg_ptr argv,
                            struct user_arg_ptr envp,
                            int flags, struct file *file)
{
        ...
        struct linux_binprm *bprm;
        ...
        bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
        ...
        if (!file)
                file = do_open_execat(fd, filename, flags);
        ...
        bprm->file = file;
        if (!filename) {
                ...
        } else if (fd == AT_FDCWD || filename->name[0] == '/') {
                bprm->filename = filename->name;
        } else {
                ...
        }
        ...
        retval = bprm_mm_init(bprm);
        ...
        retval = prepare_binprm(bprm);
        ...
        retval = copy_strings_kernel(1, &bprm->filename, bprm);
        ...
        retval = copy_strings(bprm->envc, envp, bprm);
        ...
        retval = copy_strings(bprm->argc, argv, bprm);
        ...
        retval = exec_binprm(bprm);
        ...
        return retval;
        ...
}

exec_binprm调用了search_binary_handler()

// fs/exec.c
static int exec_binprm(struct linux_binprm *bprm)
{
        ...
        ret = search_binary_handler(bprm);
        ...
        return ret;
}

在search_binary_handler()函数中,会根据前面获取的文件魔数得到可执行文件的格式类型,然后搜索与可执行文件格式相匹配的装载程序。Linux支持的可执行文件格式都有相应的装载处理程序,ELF可执行文件格式的装载处理程序是load_elf_binary(), shell可执行脚本的装载处理程序是load_script()。search_binary_handler调用了load_binary,load_binary是函数指针,针对elf的函数实现是load_elf_binary();

// fs/exec.c
int search_binary_handler(struct linux_binprm *bprm)
{
        ...
        struct linux_binfmt *fmt;
        ...
        list_for_each_entry(fmt, &formats, lh) {
                ...
                retval = fmt->load_binary(bprm);
                ...
        }
        ...
        return retval;
}
EXPORT_SYMBOL(search_binary_handler);

 

// fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
        struct file *interpreter = NULL; /* to shut gcc up */
        ...
        struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
        ...
        unsigned long elf_entry;
        ...
        struct {
                struct elfhdr elf_ex;
                struct elfhdr interp_elf_ex;
        } *loc;
        ...
        loc = kmalloc(sizeof(*loc), GFP_KERNEL);
        ...
        // 将之前从file中读出来的buf的内容,转成elf的header
        loc->elf_ex = *((struct elfhdr *)bprm->buf);
        ...
        // 从程序文件中读取elf的program header
        elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
        ...
        // 遍历program header,找到其中的interpreter,读取.interp表对应”segment”中的内容,即,动态连接器的地址 
        elf_ppnt = elf_phdata;
        for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
                char *elf_interpreter;
                loff_t pos;

                if (elf_ppnt->p_type != PT_INTERP)
                        continue;
                ...
                // elf_interpreter存放的是动态链接器的地址
                elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
                ...
                pos = elf_ppnt->p_offset;
                // 从程序文件中读取interpreter的路径,一般为 /lib64/ld-linux-x86-64.so.2
                // 有关interpreter的信息,请看 http://man7.org/linux/man-pages/man8/ld.so.8.html
                retval = kernel_read(bprm->file, elf_interpreter,
                                     elf_ppnt->p_filesz, &pos);
                ...
                // 打开interpreter文件
                interpreter = open_exec(elf_interpreter);
                ...
                pos = 0;
                // 读取interpreter的elf header
                retval = kernel_read(interpreter, &loc->interp_elf_ex,
                                     sizeof(loc->interp_elf_ex), &pos);
                ...
                break;
                ...
        }
        ...
        // 关闭当前进程使用的资源,比如线程、内存、文件等
        retval = flush_old_exec(bprm);
        ...
        // 设置新程序的各种信息
        setup_new_exec(bprm);
        ...
        // 重新设置当前堆栈的位置及大小
        retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                                 executable_stack);
        ...
        // 初始化
        elf_bss = 0;
        elf_brk = 0;

        start_code = ~0UL;
        end_code = 0;
        start_data = 0;
        end_data = 0;

        // 遍历program header,将程序文件中的代码段、data段等映射到内存
        for(i = 0, elf_ppnt = elf_phdata;
            i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
                ...
                if (elf_ppnt->p_type != PT_LOAD)
                        continue;
                ...        
                vaddr = elf_ppnt->p_vaddr;
                ...
                // 映射程序代码等信息到内存的虚拟地址,类似于mmap系统调用
                //该操作会在进程的struct mm_struct实例中添加一个struct vm_area_struct实例
                error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                                elf_prot, elf_flags, total_size);
                ...
                // 设置程序的各个segment位置信息
                k = elf_ppnt->p_vaddr;
                if (k < start_code)
                        start_code = k;
                if (start_data < k)
                        start_data = k;
                ...
                k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;

                if (k > elf_bss)
                        elf_bss = k;
                if ((elf_ppnt->p_flags & PF_X) && end_code < k)
                        end_code = k;
                if (end_data < k)
                        end_data = k;
                k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
                if (k > elf_brk) {
                        bss_prot = elf_prot;
                        elf_brk = k;
                }
        }
        // 调整程序各个segment的具体位置
        loc->elf_ex.e_entry += load_bias;
        elf_bss += load_bias;
        elf_brk += load_bias;
        start_code += load_bias;
        end_code += load_bias;
        start_data += load_bias;
        end_data += load_bias;
        ...
        // 设置堆的地址
        retval = set_brk(elf_bss, elf_brk, bss_prot);
        ...

        if (interpreter) {
                ...
                // 加载interpreter的入口地址
                elf_entry = load_elf_interp(&loc->interp_elf_ex,
                                            interpreter,
                                            &interp_map_addr,
                                            load_bias, interp_elf_phdata);
                if (!IS_ERR((void *)elf_entry)) {
                        ...
                        interp_load_addr = elf_entry;
                        elf_entry += loc->interp_elf_ex.e_entry;
                }
                ...
        } else {
                // 如果该程序没有interpreter,则使用程序自己的入口地址
                elf_entry = loc->elf_ex.e_entry;
                ...
        }
        ...
        // 进一步设置堆栈的各种信息,比如 auxiliary vector、环境变量、程序参数等
        retval = create_elf_tables(bprm, &loc->elf_ex,
                          load_addr, interp_load_addr);
        ...
        // 设置程序各个segment的地址
        current->mm->end_code = end_code;
        current->mm->start_code = start_code;
        current->mm->start_data = start_data;
        current->mm->end_data = end_data;
        current->mm->start_stack = bprm->p;

        if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
                ...
                current->mm->brk = current->mm->start_brk =
                        arch_randomize_brk(current->mm);
                ...
        }
        ...
        // 开始执行elf_entry指向的代码
        // 如果该程序有interpreter,则是执行interpreter中的入口地址
        // 如果没有,则是执行程序自己的入口地址
        // interpreter会检查该程序依赖的动态链接库,加载这些库,并解析相应的函数地址
        // 之后再调用源程序自己的入口函数,这样,也就对应到文章开始提到的
        // main函数是如何被调用的那篇文章了。
        start_thread(regs, elf_entry, bprm->p);
        ...
        return retval;
        ...
}

           elf_entry即为Entry point address(0x8048xxx),代表着应用程序的执行入口地址(在elf文件中, Entry point address事实上就是_start函数的地址)。

           load_elf_binary的主要功能是:

(1) 检查ELF可执行文件格式的有效性, 比如魔数、 程序头表中段(Segment) 的数量。
(2) 寻找动态链接的“.interp”段, 设置动态链接器路径(与动态链接有关) 。
(3) 根据ELF可执行文件的程序头表的描述, 对ELF文件进行映射, 比如代码、 数据、 只读数据。
(4) 初始化ELF进程环境, 比如进程启动时EDX寄存器的地址应该是DT_FINI的地址 。
(5) 将系统调用的返回地址修改成ELF可执行文件的入口点, 这个入口点取决于程序的链接方式, 对于静态链接的ELF可执行文件, 这个程序入口就是ELF文件的文件头中e_entry所指的地址; 对于动态链接的ELF可执行文件, 程序入口点是动态链接器。

当load_elf_binary()执行完毕, 返回至do_execve()再返回至sys_execve()时, 上面的第5步中已经把系统调用的返回地址改成了被装载的ELF程序的入口地址了。 所以当sys_execve()系统调用从内核态返回到用户态时, EIP寄存器(对应ARM寄存器PC)直接跳转到了ELF程序的入口地址, 于是新的程序开始执行, ELF可执行文件装载完成。

 load_elf_binary调用start_thread来启动应用程序

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
    ...
    regs->ip = new_ip;
    regs->sp = new_sp;
    ...
    set_thread_flag(TIF_NOTIFY_RESUME);

}
EXPORT_SYMBOL_GPL(start_thread);

start_thread会将cpu权限交给glibc的__start, 由汇编代码实现, 而应用程序的main函数则由__start调用

glibc-1.09.1\sysdeps\unix\bsd\ultrix4\mips\start.S

ENTRY(__start)
  .set noreorder

  /* The first thing on the stack is argc.  */
  lw t0, 0(sp)
  nop

  /* Set up the global pointer.  */
  la gp, _gp

  /* Then set up argv.  */
  addiu t1, sp, 4

  /* To compute where envp is, first we have to jump ahead four
     bytes from what argv was.  This will bring us ahead, so we don't
     need to compute the NULL at the end of argv later.  */
  addiu v1, t1, 4

  /* Now, compute the space to skip given the number of arguments
     we've got.  We do this by multiplying argc by 4.  */
  sll v0, t0, 2

  /* Now, add (argv+4) with the space to skip...that's envp.  */
  addu v1, v1, v0
  move t2, v1

  /* __environ = envp; */
  sw t2, __environ

  addiu sp, sp, -24

  /* __libc_init (argc, argv, envp); */
  move a0, t0
  move a1, t1
  move a2, t2
  jal __libc_init
  nop

  /* errno = 0; */
  sw zero, errno

  /* exit (main (argc, argv, envp)); */
  move a0, t0
  move a1, t1
  move a2, t2
  jal main
  nop

  /* Make the value returned by main be the argument to exit.  */
  jal exit
  move a0, v0

__start汇编实现代码中有一句注释 /*exit (main (argc, argv, envp)); */, 这也很好的说明了应用程序main函数结束后,glibc会调用exit(result)作为该程序的返回值。

 

其他说明

elf可执行文件入口地址: 

在elf header中包含elf入口地址Entry point address, 通过readelf -h a.out可以查看。

执行 objdump -t a.out | sort,有以下结果:

可以看出, elf执行入口地址是_start的地址,也是.text的起始位置。

另外,在学习完链接过程控制之链接脚本这一章之后,我们知道,可执行文件的入口是在链接脚本中使用ENTRY()指定的, 通过执行命令'ld -verbose'可以看出,在linux下编译的应用程序,可执行文件入口为_start,它其实是定义在glibc库中的一个函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值