Linux内核如何装载和启动一个可执行程序

陈琛+ 原创作品转载请注明出处《Linux内核分析》MOOC课

1.理解编译链接的过程和ELF可执行文件格式

1)程序编译链接过程

编译->汇编->链接
1)预处理
gcc -E -o hello.cpp hello.c -m32

2)编译为汇编代码
gcc -x cpp-output -S -o hello.s hello.cpp -m32

3)汇编代码编译为目标代码
gcc -x assembler -c hello.s -o hello.o -m32

4)链接
gcc -o hello hello.o -m32
使用共享库的编译,libc, printf

5)
静态编译(所依赖的都放在hello.static内部)
gcc -o hello.static hello.o -m32 -static

2)ELF文件格式

a.out

COFF

PE

ELF(EXECUTABLE AND LINKABLE FORMAT)

三种目标文件:

  • 可重定位文件 .o文件
  • 可执行文件
  • 共享目标文件 .so文件
    用来被两个链接器链接:链接编辑器,可以和其他可重定位和共享文件object来创建其他object(静态链接); 动态链接器,联合一个可执行文件和其他共享object文件来创建一个进程映像 .

ELF文件加载内存(静态链接,所有代码放在一个段),形成进程,默认是加载到以0x8048000开始处.

image

gcc -shared shlibexample.c -o libshlibexample.so -m32
gcc -shared dllibexample.c -o libdllibexample.so -m32

gcc main.c -o main -L$PWD -lshlibexample -ldl -m32 //-ldl动态加载库
export LD_LIBRARY_PATH=$PWD  //当前目录加入到库搜索路径

2.编程使用exec*库函数加载一个可执行文件

可执行文件的装载

  • 执行一个程序的shell环境,直接使用execve系统调用
    • shell不限制命令行个数,取决于命令本身
    • shell调用execve将命令行参数和环境参数传递给可执行程序的main函数
    • int execve(const char *filename, char * const argv[], char * const envp[])
    • 库函数exec*是系统调用execve的封装例程
  • sys_execve会解析可执行文件格式
    • do_execve -> do_execve_common -> exec_binprm
    • search_binary_handler符合寻找文件格式对应的解析模块(根据文件头部信息寻找对应的文件格式处理模块)
     list_for_each_entry(fmt, &formats, lh) {
        if (!try_module_get(fmt->module))
            continue;
        read_unlock(&binfmt_lock);
        bprm->recursion_depth++;
        retval = fmt->load_binary(bprm);//解析elf文件格式的执行位置
        read_lock(&binfmt_lock);
    
    • 对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读
    • Linux内核是如何支持多种不同的可执行文件格式的?
    static struct linux_binfmt elf_format = {
        .module     = THIS_MODULE,
        .load_binary    = load_elf_binary,
        .load_shlib = load_elf_library,
        .core_dump  = elf_core_dump,
        .min_coredump   = ELF_EXEC_PAGESIZE,
    };
    
    static int __init init_elf_binfmt(void)
    {
        register_binfmt(&elf_format);
        return 0;
    }
    
    • 可执行文件开始执行的起点在哪里?如何才能让execve系统调用返回到用户态时执行新程序?
      • 庄生梦蝶 —— 醒来迷惑是庄周梦见了蝴蝶还是蝴蝶梦见了庄周?
      • 庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)
      • 修改int 0x80压入内核堆栈的EIP
      • load_elf_binary -> start_thread//通过修改内核堆栈中EIP的值作为新程序的起点
      • 动态链接的过程内核做了什么?可执行文件依赖的动态链接库(共享库)是由谁负责加载以及如何递归加载的?
execve 处理过程
linux/fs/exec.c

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);
}


int do_execve(struct filename *filename,
         const char __user *const __user *__argv,
         const char __user *const __user *__envp)   //__user 用户态指针
 {
         struct user_arg_ptr argv = { .ptr.native = __argv };//命令行参数变成结构
         struct user_arg_ptr envp = { .ptr.native = __envp };
         return do_execve_common(filename, argv, envp);
 }


static int do_execve_common(struct filename *filename,
                                 struct user_arg_ptr argv,
                                 struct user_arg_ptr envp)
 {
 
    struct linux_binprm *bprm; //保存要执行的文件相关的信息(include/linux/binfmts.h)
    ...
    file = do_open_exec(filename);//打开执行的可执行文件
    
    //填充bprm结构
    bprm->file = file;
    bprm->filename = bprm->interp = filename->name;
    ...
    retval = copy_strings(bprm->argc, argv, bprm);//命令行参数和环境变量copy到结构体里
    
    retval = exec_binprm(bprm);// 
 }

1405 static int exec_binprm(struct linux_binprm *bprm)
1406 {
1407         pid_t old_pid, old_vpid;
1408         int ret;
1409 
1410         /* Need to fetch pid before load_binary changes it */
1411         old_pid = current->pid;
1412         rcu_read_lock();
1413         old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
1414         rcu_read_unlock();
1415 
1416         ret = search_binary_handler(bprm);//寻找可执行文件的处理函数
1417         if (ret >= 0) {
1418                 audit_bprm(bprm);
1419                 trace_sched_process_exec(current, old_pid, bprm);
1420                 ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
1421                 proc_exec_connector(current);
1422         }
1423 
1424         return ret;
1425 }


/*
1350  * cycle the list of binary formats handler, until one recognizes the image
1351  */
1352 int search_binary_handler(struct linux_binprm *bprm)
1353 {
         struct linux_binfmt *fmt;
        ...
        //循环寻找能够解析当前可执行文件的代码
1369         list_for_each_entry(fmt, &formats, lh) {
1370                 if (!try_module_get(fmt->module))
1371                         continue;
1372                 read_unlock(&binfmt_lock);
1373                 bprm->recursion_depth++;
1374                 retval = fmt->load_binary(bprm);//加载可执行文件的处理函数,函数指针,实际调用load_elf_binary(linux/fs/binfmt_elf.c)
1375                 read_lock(&binfmt_lock);
1376                 put_binfmt(fmt);
1377                 bprm->recursion_depth--;
1378                 if (retval < 0 && !bprm->mm) {
1379                         /* we got to flush_old_exec() and failed after it */
1380                         read_unlock(&binfmt_lock);
1381                         force_sigsegv(SIGSEGV, current);
1382                         return retval;
1383                 }
1384                 if (retval != -ENOEXEC || !bprm->file) {
1385                         read_unlock(&binfmt_lock);
1386                         return retval;
1387                 }
1388         }
1389         read_unlock(&binfmt_lock);

     ...
     }
// include/linux/binfmts.h
 14 struct linux_binprm {
 15         char buf[BINPRM_BUF_SIZE];
 16 #ifdef CONFIG_MMU
 17         struct vm_area_struct *vma;
 18         unsigned long vma_pages;
 19 #else
 20 # define MAX_ARG_PAGES  32
 21         struct page *page[MAX_ARG_PAGES];
 22 #endif
 23         struct mm_struct *mm;
 24         unsigned long p; /* current top of mem */
 25         unsigned int
 26                 cred_prepared:1,/* true if creds already prepared (multiple
 27                                  * preps happen for interpreters) */
 28                 cap_effective:1;/* true if has elevated effective capabilities,
 29                                  * false if not; except for init which inherits
 30                                  * its parent's caps anyway */
 31 #ifdef __alpha__
 32         unsigned int taso:1;
 33 #endif
 34         unsigned int recursion_depth; /* only for search_binary_handler() */
 35         struct file * file;
 36         struct cred *cred;      /* new credentials */
 37         int unsafe;             /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
 38         unsigned int per_clear; /* bits to clear in current->personality */
 39         int argc, envc;
 40         const char * filename;  /* Name of binary as seen by procps */
 41         const char * interp;    /* Name of the binary really executed. Most
 42                                    of the time same as filename, but could be
 43                                    different for binfmt_{misc,script} */
 44         unsigned interp_flags;
 45         unsigned interp_data;
 46         unsigned long loader, exec;
 47 };

//include/linux/binfmts.h
//linux_binfmt
 66 /*
 67  * This structure defines the functions that are used to load the binary formats that
 68  * linux accepts.
 69  */
 70 struct linux_binfmt {
 71         struct list_head lh;
 72         struct module *module;
 73         int (*load_binary)(struct linux_binprm *);
 74         int (*load_shlib)(struct file *);
 75         int (*core_dump)(struct coredump_params *cprm);
 76         unsigned long min_coredump;     /* minimal dump size */
 77 };
//linux/fs/binfmt_elf.c
//结构体变量, 赋值(加载到一个链表里面,观察者模式)
 82 static struct linux_binfmt elf_format = {
 83         .module         = THIS_MODULE,
 84         .load_binary    = load_elf_binary, 
 85         .load_shlib     = load_elf_library,
 86         .core_dump      = elf_core_dump,
 87         .min_coredump   = ELF_EXEC_PAGESIZE,
 88 };
 
2198 static int __init init_elf_binfmt(void)
2199 {
2200         register_binfmt(&elf_format); //efl_format注册到一个结构体链表里
2201         return 0;
2202 }


//按照elf格式严格解析elf文件.将文件映射到进程空间
//对于elf格式文件总被映射到0x8048000地址
//需要动态链接的可执行文件先加载连接器ld
571 static int load_elf_binary(struct linux_binprm *bprm)
572 {
        ...
626         retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
627                              (char *)elf_phdata, size);

975         start_thread(regs, elf_entry, bprm->p);
        ...
    }

3 使用gdb跟踪sys_execve内核函数的处理过程

 git clone https://github.com/mengning/menu.git
 
 mv test_exec.c test.c
 
 make rootfs
 
 qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -S -s
 
 // new terminal 
 gdb
 file ../linux-3.18.6/vmlinux
 target remote:1234
 
 //断点
 b sys_execve
 b load_elf_binary
 b start_thread
 
 // 执行
 c
 
 //列出断点代码
 list
 
 //单步
 s
 
 //next
 n

主要代码:shell(庄子)执行过程中加载了hello(蝴蝶)程序:
image

image

image

image

new_ip是返回到用户态的第一条指令的地址

po new_ip
image

readelf -h hello

image

可执行文件hello的elf信息显示入口地址为0x8040d0a,而new_ip的值正好为这个地址

动态链接库文件的加载呢?

依赖动态连接器ld解析这个elf文件

// linux/fs/binfmt_elf.c
// static int load_elf_binary(struct linux_binprm *bprm)
...
if(elf_interpreter){
    unsigned long inter_map_addr = 0;
    elf_entry = load_elf_interp(&loc->interp_elf_ex,
                                interpreter,
                                &interp_map_addr,
                                load_bias);
                            
}


...

elf_entry返回的 不再是elf文件的入口地址,而是动态链接器的起点.

动态链接库的装载过程是一个图的广度遍历,装载和链接之后ld将CPU的控制权交给可执行程序。

可执行程序加载的两种情况:

  • 静态链接库

    直接进入可执行程序入口;

  • 动态链接

    由ld动态链接,加载完成,控制权移交给可执行程序入口。所以动态加载不是由内核完成的,而是由ld(libc,用户态)完成

posted @ 2017-04-09 22:27  tb1over  阅读(461)  评论(0编辑  收藏  举报