execve系統調用 execve系統調用 我們前面提到了, fork, vfork等複製出來的進程是父進程的一個副本, 那麼如何我們想載入新的程式, 可以通過execve來載入和啟動新的程式。 x86架構下, 其實還實現了一個新的exec的系統調用叫做execveat(自linux 3.19後進入 ...
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 系統上出現後,被 UNIX 世界所廣泛接受,作為預設的二進位文件格式來使用。
但是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的數據結構,定義如下
linux_binfmt定義在include/linux/binfmts.h中
/*
* 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”的信號時被創建的, 其格式取決於被執行程式的可執行類型
當我們執行一個可執行程式的時候, 內核會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) |
arch/對應體繫結構/include/uapi/asm/unistd.h, line 265 |
入口函數聲明 | asmlinkage long sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp); |
include/linux/syscalls.h, line 843 |
系統調用實現 | asmlinkage long sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp); | fs/exec.v 1710 |
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的定義在fs/exec.c中,參見 http://lxr.free-electrons.com/source/fs/exec.c?v=4.5#L1628
更早期實現linux-2.4 | linux-3.18引入execveat之前do_execve實現 | linux-3.19~至今引入execveat之後do_execve實現 | do_execveat |
---|---|---|---|
代碼過長, 沒有經過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_common和do_execveat_common
早期linux-2.4中直接由do_execve實現程式的載入和運行
linux-3.18引入execveat之前do_execve調用do_execve_common來完成程式的載入和運行
linux-3.19~至今引入execveat之後do_execve調用do_execveat_common來完成程式的載入和運行
在Linux中提供了一系列的函數,這些函數能用可執行文件所描述的新上下文代替進程的上下文。這樣的函數名以首碼exec開始。所有的exec函數都是調用了execve()系統調用。
sys_execve接受參數:1.可執行文件的路徑 2.命令行參數字元串 3.環境變數字元串
sys_execve是調用do_execve實現的。do_execve則是調用do_execveat_common實現的,依次執行以下操作:
- 調用unshare_files()為進程複製一份文件表
- 調用kzalloc()分配一份struct linux_binprm結構體
- 調用open_exec()查找並打開二進位文件
- 調用sched_exec()找到最小負載的CPU,用來執行該二進位文件
- 根據獲取的信息,填充struct linux_binprm結構體中的file、filename、interp成員
- 調用bprm_mm_init()創建進程的記憶體地址空間,為新程式初始化記憶體管理.並調用init_new_context()檢查當前進程是否使用自定義的局部描述符表;如果是,那麼分配和準備一個新的LDT
- 填充struct linux_binprm結構體中的argc、envc成員
- 調用prepare_binprm()檢查該二進位文件的可執行許可權;最後,kernel_read()讀取二進位文件的頭128位元組(這些位元組用於識別二進位文件的格式及其他信息,後續會使用到)
- 調用copy_strings_kernel()從內核空間獲取二進位文件的路徑名稱
- 調用copy_string()從用戶空間拷貝環境變數和命令行參數
- 至此,二進位文件已經被打開,struct linux_binprm結構體中也記錄了重要信息, 內核開始調用exec_binprm執行可執行程式
- 釋放linux_binprm數據結構,返回從該文件可執行格式的load_binary中獲得的代碼
定義在fs/exec.c
/*
* 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(¤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;
// 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成員負責識別該二進位文件的格式;
內核使用鏈表組織這些struct linux_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是不一樣的,
定義在fs/exec.c
/*
* 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結構, 其中存儲了對應可執行程式格式載入函數等
格式 | linux_binfmt定義 | load_binary | load_shlib | core_dump |
---|---|---|---|---|
a.out | aout_format | load_aout_binary | load_aout_library | aout_core_dump |
flat style executables | flat_format | load_flat_binary | load_flat_shared_library | flat_core_dump |
script腳本 | script_format | load_script | 無 | 無 |
misc_format | misc_format | load_misc_binary | 無 | 無 |
em86 | em86_format | load_format | 無 | 無 |
elf_fdpic | elf_fdpic_format | load_elf_fdpic_binary | 無 | elf_fdpic_core_dump |
elf | elf_format | load_elf_binary | load_elf_binary | elf_core_dump |