1. 程式人生 > >Linux程序啟動過程分析do_execve(可執行程式的載入和執行)---Linux程序的管理與排程(十一)

Linux程序啟動過程分析do_execve(可執行程式的載入和執行)---Linux程序的管理與排程(十一)

execve系統呼叫

execve系統呼叫

我們前面提到了, fork, vfork等複製出來的程序是父程序的一個副本, 那麼如何我們想載入新的程式, 可以通過execve來載入和啟動新的程式。

x86架構下, 其實還實現了一個新的exec的系統呼叫叫做execveat(自linux-3.19後進入核心)

exec()函式族

exec函式一共有六個,其中execve為核心級系統呼叫,其他(execl,execle,execlp,execv,execvp)都是呼叫execve的庫函式。

int execl(const char *path, const
char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]);

ELF檔案格式以及可執行程式的表示

ELF可執行檔案格式

Linux下標準的可執行檔案格式是ELF.ELF(Executable and Linking Format)是一種物件檔案的格式,用於定義不同型別的物件檔案(Object files)中都放了什麼東西、以及都以什麼樣的格式去放這些東西。它自最早在 System V 系統上出現後,被 xNIX 世界所廣泛接受,作為預設的二進位制檔案格式來使用。

但是linux也支援其他不同的可執行程式格式, 各個可執行程式的執行方式不盡相同, 因此linux核心每種被註冊的可執行程式格式都用linux_bin_fmt來儲存, 其中記錄了可執行程式的載入和執行函式

同時我們需要一種方法來儲存可執行程式的資訊, 比如可執行檔案的路徑, 執行的引數和環境變數等資訊,即linux_bin_prm結構

struct linux_bin_prm結構描述一個可執行程式

linux_binprm是定義在include/linux/binfmts.h中, 用來儲存要要執行的檔案相關的資訊, 包括可執行程式的路徑, 引數和環境變數的資訊

/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm { char buf[BINPRM_BUF_SIZE]; // 儲存可執行檔案的頭128位元組 #ifdef CONFIG_MMU struct vm_area_struct *vma; unsigned long vma_pages; #else # define MAX_ARG_PAGES 32 struct page *page[MAX_ARG_PAGES]; #endif struct mm_struct *mm; unsigned long p; /* current top of mem , 當前記憶體頁最高地址*/ unsigned int cred_prepared:1,/* true if creds already prepared (multiple * preps happen for interpreters) */ cap_effective:1;/* true if has elevated effective capabilities, * false if not; except for init which inherits * its parent's caps anyway */ #ifdef __alpha__ unsigned int taso:1; #endif unsigned int recursion_depth; /* only for search_binary_handler() */ struct file * file; /* 要執行的檔案 */ struct cred *cred; /* new credentials */ int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */ unsigned int per_clear; /* bits to clear in current->personality */ int argc, envc; /* 命令列引數和環境變數數目 */ const char * filename; /* Name of binary as seen by procps, 要執行的檔案的名稱 */ const char * interp; /* Name of the binary really executed. Most of the time same as filename, but could be different for binfmt_{misc,script} 要執行的檔案的真實名稱,通常和filename相同 */ unsigned interp_flags; unsigned interp_data; unsigned long loader, exec; };

struct linux_binfmt可執行程式的結構

linux支援其他不同格式的可執行程式, 在這種方式下, linux能執行其他作業系統所編譯的程式, 如MS-DOS程式, 活BSD Unix的COFF可執行格式, 因此linux核心用struct linux_binfmt來描述各種可執行程式。

linux核心對所支援的每種可執行的程式型別都有個struct linux_binfmt的資料結構,定義如下

/*
  * This structure defines the functions that are used to load the binary formats that
  * linux accepts.
  */
struct linux_binfmt {
    struct list_head lh;
    struct module *module;
    int (*load_binary)(struct linux_binprm *);
    int (*load_shlib)(struct file *);
    int (*core_dump)(struct coredump_params *cprm);
    unsigned long min_coredump;     /* minimal dump size */
 };

其提供了3種方法來載入和執行可執行程式

  • load_binary

    通過讀存放在可執行檔案中的資訊為當前程序建立一個新的執行環境

  • load_shlib

    用於動態的把一個共享庫捆綁到一個已經在執行的程序, 這是由uselib()系統呼叫啟用的

  • core_dump

    在名為core的檔案中, 存放當前程序的執行上下文. 這個檔案通常是在程序接收到一個預設操作為”dump”的訊號時被建立的, 其格式取決於被執行程式的可執行型別

所有的linux_binfmt物件都處於一個連結串列中, 第一個元素的地址存放在formats變數中, 可以通過呼叫register_binfmt()和unregister_binfmt()函式在連結串列中插入和刪除元素, 在系統啟動期間, 為每個編譯進核心的可執行格式都執行registre_fmt()函式. 當實現了一個新的可執行格式的模組正被裝載時, 也執行這個函式, 當模組被解除安裝時, 執行unregister_binfmt()函式.

當我們執行一個可執行程式的時候, 核心會list_for_each_entry遍歷所有註冊的linux_binfmt物件, 對其呼叫load_binrary方法來嘗試載入, 直到載入成功為止.

execve載入可執行程式的過程

核心中實際執行execv()或execve()系統呼叫的程式是do_execve(),這個函式先開啟目標映像檔案,並從目標檔案的頭部(第一個位元組開始)讀入若干(當前Linux核心中是128)位元組(實際上就是填充ELF檔案頭,下面的分析可以看到),然後呼叫另一個函式search_binary_handler(),在此函式裡面,它會搜尋我們上面提到的Linux支援的可執行檔案型別佇列,讓各種可執行程式的處理程式前來認領和處理。如果型別匹配,則呼叫load_binary函式指標所指向的處理函式來處理目標映像檔案。在ELF檔案格式中,處理函式是load_elf_binary函式,下面主要就是分析load_elf_binary函式的執行過程(說明:因為核心中實際的載入需要涉及到很多東西,這裡只關注跟ELF檔案的處理相關的程式碼):

sys_execve() > do_execve() > do_execveat_common > search_binary_handler() > load_elf_binary()

execve的入口函式sys_execve

描述 定義 連結
系統呼叫號(體系結構相關) 類似與如下的形式
#define __NR_execve 117
__SYSCALL(117, sys_execve, 3)
入口函式宣告 asmlinkage long sys_execve(const char __user *filename,
const char __user *const __user *argv,
const char __user *const __user *envp);
系統呼叫實現 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);
}

execve系統呼叫的的入口點是體系結構相關的sys_execve, 該函式很快將工作委託給系統無關的do_execve函式

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

通過引數傳遞了寄存集合和可執行檔案的名稱(filename), 而且還傳遞了指向了程式的引數argv和環境變數envp的指標

引數 描述
filename 可執行程式的名稱
argv 程式的引數
envp 環境變數

指向程式引數argv和環境變數envp兩個陣列的指標以及陣列中所有的指標都位於虛擬地址空間的使用者空間部分。因此核心在當問使用者空間記憶體時, 需要多加小心, 而__user註釋則允許自動化工具來檢測時候所有相關事宜都處理得當

do_execve函式

程式碼過長, 沒有經過do_execve_common的封裝 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);
}
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);
}
int do_execveat(int fd, struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp,
int flags)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execveat_common(fd, filename, argv, envp, flags);
}

我們可以看到不同時期的演變, 早期的程式碼 do_execve就直接完成了自己的所有工作, 後來do_execve會呼叫更加底層的do_execve_common函式, 後來x86架構下引入了新的系統呼叫execveat, 為了使程式碼更加通用, do_execveat_common替代了原來的do_execve_common函式

早期的do_execve流程如下, 基本無差別, 可以作為參考

do_execve函式的流程

程式的載入do_execve_common和do_execveat_common

在Linux中提供了一系列的函式,這些函式能用可執行檔案所描述的新上下文代替程序的上下文。這樣的函式名以字首exec開始。所有的exec函式都是呼叫了execve()系統呼叫。

sys_execve接受引數:1.可執行檔案的路徑 2.命令列引數字串 3.環境變數字串

sys_execve是呼叫do_execve實現的。do_execve則是呼叫do_execveat_common實現的,依次執行以下操作:

  1. 呼叫unshare_files()為程序複製一份檔案表

  2. 呼叫kzalloc()分配一份structlinux_binprm結構體

  3. 呼叫open_exec()查詢並開啟二進位制檔案

  4. 呼叫sched_exec()找到最小負載的CPU,用來執行該二進位制檔案

  5. 根據獲取的資訊,填充structlinux_binprm結構體中的file、filename、interp成員

  6. 呼叫bprm_mm_init()建立程序的記憶體地址空間,為新程式初始化記憶體管理.並呼叫init_new_context()檢查當前程序是否使用自定義的區域性描述符表;如果是,那麼分配和準備一個新的LDT

  7. 填充structlinux_binprm結構體中的argc、envc成員

  8. 呼叫prepare_binprm()檢查該二進位制檔案的可執行許可權;最後,kernel_read()讀取二進位制檔案的頭128位元組(這些位元組用於識別二進位制檔案的格式及其他資訊,後續會使用到)

  9. 呼叫copy_strings_kernel()從核心空間獲取二進位制檔案的路徑名稱

  10. 呼叫copy_string()從使用者空間拷貝環境變數和命令列引數

  11. 至此,二進位制檔案已經被開啟,struct linux_binprm結構體中也記錄了重要資訊, 核心開始呼叫exec_binprm執行可執行程式

  12. 釋放linux_binprm資料結構,返回從該檔案可執行格式的load_binary中獲得的程式碼

/*
 * sys_execve() executes a new program.
 */
static int do_execveat_common(int fd, struct filename *filename,
                          struct user_arg_ptr argv,
                          struct user_arg_ptr envp,
                          int flags)
{
    char *pathbuf = NULL;
    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(&current_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;

    //  1.  呼叫unshare_files()為程序複製一份檔案表;
    retval = unshare_files(&displaced);
    if (retval)
            goto out_ret;

    retval = -ENOMEM;

    //  2、呼叫kzalloc()在堆上分配一份structlinux_binprm結構體;
    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;

    //  3、呼叫open_exec()查詢並開啟二進位制檔案;
    file = do_open_execat(fd, filename, flags);
    retval = PTR_ERR(file);
    if (IS_ERR(file))
            goto out_unmark;

    //  4、呼叫sched_exec()找到最小負載的CPU,用來執行該二進位制檔案;
    sched_exec();

    //  5、根據獲取的資訊,填充structlinux_binprm結構體中的file、filename、interp成員;
    bprm->file = file;
    if (fd == AT_FDCWD || filename->name[0] == '/') {
            bprm->filename = filename->name;
    } else {
            if (filename->name[0] == '\0')
                    pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
            else
                    pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
                                        fd, filename->name);
            if (!pathbuf) {
                    retval = -ENOMEM;
                    goto out_unmark;
            }
            /*
             * Record that a name derived from an O_CLOEXEC fd will be
             * inaccessible after exec. Relies on having exclusive access to
             * current->files (due to unshare_files above).
             */
            if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
                    bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
            bprm->filename = pathbuf;
    }
    bprm->interp = bprm->filename;

    //  6、呼叫bprm_mm_init()建立程序的記憶體地址空間,並呼叫init_new_context()檢查當前程序是否使用自定義的區域性描述符表;如果是,那麼分配和準備一個新的LDT;
    retval = bprm_mm_init(bprm);
    if (retval)
            goto out_unmark;

    //  7、填充structlinux_binprm結構體中的命令列引數argv,環境變數envp
    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;

    //  8、呼叫prepare_binprm()檢查該二進位制檔案的可執行許可權;最後,kernel_read()讀取二進位制檔案的頭128位元組(這些位元組用於識別二進位制檔案的格式及其他資訊,後續會使用到);
    retval = prepare_binprm(bprm);
    if (retval < 0)
            goto out;

    //  9、呼叫copy_strings_kernel()從核心空間獲取二進位制檔案的路徑名稱;
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval < 0)
            goto out;

    bprm->exec = bprm->p;

    //  10.1、呼叫copy_string()從使用者空間拷貝環境變數
    retval = copy_strings(bprm->envc, envp, bprm);
    if (retval < 0)
            goto out;

    //  10.2、呼叫copy_string()從使用者空間拷貝命令列引數;
    retval = copy_strings(bprm->argc, argv, bprm);
    if (retval < 0)
            goto out;
    /*
        至此,二進位制檔案已經被開啟,struct linux_binprm結構體中也記錄了重要資訊;

        下面需要識別該二進位制檔案的格式並最終執行該檔案
    */
    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);
    kfree(pathbuf);
    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);
    kfree(pathbuf);

out_files:
    if (displaced)
            reset_files_struct(displaced);
out_ret:
    putname(filename);
    return retval;
}

exec_binprm識別並載入二程序程式

每種格式的二進位制檔案對應一個struct linux_binprm結構體,load_binary成員負責識別該二進位制檔案的格式;

核心使用連結串列組織這些structlinux_binfmt結構體,連結串列頭是formats。

接著do_execveat_common()繼續往下看:

呼叫search_binary_handler()函式對linux_binprm的formats連結串列進行掃描,並嘗試每個load_binary函式,如果成功載入了檔案的執行格式,對formats的掃描終止。

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

    return ret;
}

search_binary_handler識別二程序程式

這裡需要說明的是,這裡的fmt變數的型別是struct linux_binfmt *, 但是這一個型別與之前在do_execveat_common()中的bprm是不一樣的,

/*
* 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);

//  遍歷formats連結串列
list_for_each_entry(fmt, &formats, lh) {
        if (!try_module_get(fmt->module))
                continue;
        read_unlock(&binfmt_lock);
        bprm->recursion_depth++;

        // 遍歷formats連結串列
        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;

}

load_binary載入可執行程式

我們前面提到了,linux核心支援多種可執行程式格式, 每種格式都被註冊為一個linux_binfmt結構, 其中儲存了對應可執行程式格式載入函式等