1. 程式人生 > >Linux核心如何裝載和啟動一個可執行程式

Linux核心如何裝載和啟動一個可執行程式

一、預備知識

1.1 編譯連結的過程

預處理:(.c---->.cpp)(注意:這裡的CPP不是C Plus Plus的意思)

gcc -E -o hello.cpp hello.c -m32
編譯:(.cpp---->.s彙編)
gcc -x cpp-output -S -o hello.s hello.cpp -m32
編譯:(.s---->.o二進位制程式碼)
gcc -x assembler -c hello.s -o hello.o -m32
連結:(.o---->a.out)共享庫
gcc -o hello hello.o -m32
靜態編譯:
gcc -o hello.static hello.o -m32 -static
C語言main函式格式

int main(int argc, char *argv[])

int main(int argc, char *argv[], char *envp[])

execve函式格式

int execve(const char *filename, char *const argv[], char *const envp[])
系統呼叫過程:sys_execve -> do_execve -> do_execve_common -> exec_binprm

動態連結:

(1)裝載時動態連結

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

(2)執行時動態連結

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


1.2 ELF可執行檔案格式

ELF檔案有三種類型:

(1)可重定位檔案(relocatable),即通常所說的目標檔案(.o)

(2)可執行檔案(executable)

(3)共享檔案(object),即通常所說的庫檔案(.so)

ELF檔案的總體佈局:

ELF header(頭)

program header table(程式頭表)

segment 1(段1)

……

segment n(段n)

section header table(節頭表)(可選)

用readelf可以看可執行檔案的ELF資訊

ELF可執行檔案預設對映地址為:0x8048000

但實際的入口地址為:0x8048300


二、實驗過程
2.1 開啟shell終端,執行如下命令:

cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs
效果如下:




2.2 開啟除錯模式

關閉上面的qemu,在menu目錄下執行如下命令:

qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S
另開一個shell視窗,在menu目錄下開啟gdb
gdb
file ../linux-3.18.6/vmlinux
target remote:1234


這樣我們就進入除錯模式了。

2.3 設定斷點跟蹤除錯

b sys_execve
b do_execve
b search_binary_handler
b load_elf_binary
b start_thread


2.4 分析

裝載和啟動一個可執行程式依次呼叫以下函式:

sys_execve -> do_execve -> do_execve_common -> exec_binprm ->search_binary_handler ->load_elf_binary -> start_thread

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)
{
   struct user_arg_ptr argv = { .ptr.native = __argv };
   struct user_arg_ptr envp = { .ptr.native = __envp };
   return do_execve_common(filename, argv, envp);
}
/*
* sys_execve() executes a new program.
*/
static int do_execve_common(struct filename *filename,
               struct user_arg_ptr argv,
               struct user_arg_ptr envp)
{
   struct linux_binprm *bprm;
   struct file *file;
   struct files_struct *displaced;
   int retval;
    if (IS_ERR(filename))
       return PTR_ERR(filename);
    /*
    * We move the actual failure in case of RLIMIT_NPROC excess from
    * set*uid() to execve() because too many poorly written programs
    * don't check setuid() return code.  Here we additionally recheck
    * whether NPROC limit is still exceeded.
    */
   if ((current->flags & PF_NPROC_EXCEEDED) &&
       atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) {
       retval = -EAGAIN;
       goto out_ret;
   }
    /* We're below the limit (still or again), so we don't want to make
    * further execve() calls fail. */
   current->flags &= ~PF_NPROC_EXCEEDED;
    retval = unshare_files(&displaced);
   if (retval)
       goto out_ret;
    retval = -ENOMEM;
   bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
   if (!bprm)
       goto out_files;
    retval = prepare_bprm_creds(bprm);
   if (retval)
       goto out_free;
    check_unsafe_exec(bprm);
   current->in_execve = 1;
    file = do_open_exec(filename);
   retval = PTR_ERR(file);
   if (IS_ERR(file))
       goto out_unmark;
    sched_exec();
    bprm->file = file;
   bprm->filename = bprm->interp = filename->name;
    retval = bprm_mm_init(bprm);
   if (retval)
       goto out_unmark;
    bprm->argc = count(argv, MAX_ARG_STRINGS);
   if ((retval = bprm->argc) < 0)
       goto out;
    bprm->envc = count(envp, MAX_ARG_STRINGS);
   if ((retval = bprm->envc) < 0)
       goto out;
    retval = prepare_binprm(bprm);
   if (retval < 0)
       goto out;
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
   if (retval < 0)
       goto out;
    bprm->exec = bprm->p;
   retval = copy_strings(bprm->envc, envp, bprm);
   if (retval < 0)
       goto out;
    retval = copy_strings(bprm->argc, argv, bprm);
   if (retval < 0)
       goto out;
    retval = exec_binprm(bprm);
   if (retval < 0)
       goto out;
    /* execve succeeded */
   current->fs->in_exec = 0;
   current->in_execve = 0;
   acct_update_integrals(current);
   task_numa_free(current);
   free_bprm(bprm);
   putname(filename);
   if (displaced)
       put_files_struct(displaced);
   return retval;
out:
   if (bprm->mm) {
       acct_arg_size(bprm, 0);
       mmput(bprm->mm);
   }
out_unmark:
   current->fs->in_exec = 0;
   current->in_execve = 0;
out_free:
   free_bprm(bprm);
out_files:
   if (displaced)
       reset_files_struct(displaced);
out_ret:
   putname(filename);
   return retval;
}
static int exec_binprm(struct linux_binprm *bprm)
{
   pid_t old_pid, old_vpid;
   int ret;
    /* Need to fetch pid before load_binary changes it */
   old_pid = current->pid;
   rcu_read_lock();
   old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
   rcu_read_unlock();
    ret = search_binary_handler(bprm);
   if (ret >= 0) {
       audit_bprm(bprm);
       trace_sched_process_exec(current, old_pid, bprm);
       ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
       proc_exec_connector(current);
   }
/*
* cycle the list of binary formats handler, until one recognizes the image
*/
int search_binary_handler(struct linux_binprm *bprm)
{
   bool need_retry = IS_ENABLED(CONFIG_MODULES);
   struct linux_binfmt *fmt;
   int retval;
    /* This allows 4 levels of binfmt rewrites before failing hard. */
   if (bprm->recursion_depth > 5)
       return -ELOOP;
    retval = security_bprm_check(bprm);
   if (retval)
       return retval;
    retval = -ENOENT;
retry:
   read_lock(&binfmt_lock);
   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);
       read_lock(&binfmt_lock);
       put_binfmt(fmt);
       bprm->recursion_depth--;
       if (retval < 0 && !bprm->mm) {
           /* we got to flush_old_exec() and failed after it */
           read_unlock(&binfmt_lock);
           force_sigsegv(SIGSEGV, current);
           return retval;
       }
       if (retval != -ENOEXEC || !bprm->file) {
           read_unlock(&binfmt_lock);
           return retval;
       }
   }
   read_unlock(&binfmt_lock);
    if (need_retry) {
       if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
           printable(bprm->buf[2]) && printable(bprm->buf[3]))
           return retval;
       if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
           return retval;
       need_retry = false;
       goto retry;
   }
    return retval;
}
EXPORT_SYMBOL(search_binary_handler);
對於ELF格式的可執行檔案fmt->load_binary(bprm);執行的應該是load_elf_binary其內部是和ELF檔案格式解析的部分需要和ELF檔案格式標準結合起來閱讀。load_elf_binary在/linux-3.18.6/fs/binfmt_elf.c中。
static struct linux_binfmt elf_format = {
    .module          = THIS_MODULE,
    .load_binary     = load_elf_binary,
    .load_dump       = elf_core_dump,
    .min_coredump    = ELF_EXEC_PAGESIZE,
};

當呼叫execve的可執行程式是,系統呼叫execve陷入核心,這時會建立一個新的使用者太堆疊,實際是把引數和環境變數通過指標的方式傳遞給系統呼叫核心處理函式,然後核心處理函式在建立可執行程式新的使用者態堆疊的時候,會把這些拷貝到使用者態堆疊初始化新的可執行程式的執行上下文環境(先函式呼叫引數傳遞,在系統呼叫引數傳遞)。這時就載入了新的可執行程式。系統呼叫execve返回使用者態的時候,就變成了被execve載入的可執行程式。

三、總結
    新的可執行程式是從new_ip開始執行,start_thread實際上是返回到使用者態的位置從Int 0x80的下一條指令,變成了新載入的可執行檔案的入口位置。當執行到execve系統呼叫時,陷入核心態,用execve載入的可執行檔案覆蓋當前程序的可執行程式,當execve系統呼叫返回時,返回新的可執行程式的執行起點(main函式位置),所以execve系統呼叫返回後新的可執行程式能順利執行。對於靜態連結的可執行程式和動態連結程式execve系統呼叫返回時,如果是靜態連結,elf_entry指向可執行檔案規定的頭部0x8048000;如果需要依賴動態連結庫,elf_entry指向動態聯結器的起點。