linux可執(zhí)行文件的加載和運行之一
三:可執(zhí)行文件的加載和運行
Execve系統(tǒng)調用可以調用一個可執(zhí)行文件完全代替當前的進程,它在libc中的封裝有幾個API:
int execl(const charp a t* h n a m e, const char a* rg 0, ... /* (char *) 0 */);
int execv(const charp a t* h n a m e, char *consta rgv [] );
int execle(const charp a t* h n a m e, const char a* rg 0, ...
/* (char *)0, char *cones nt v p [] */);
int execve(const charp a t* h n a m e, char *consta rgv [], char *consten vp [] );
int execlp(const charf i l e* n a m e, const char a* rg 0, ... /* (char *) 0 */);
int execvp(const charf i l e* n a m e, char *consta rgv [] );
我們深入內核代碼來研究一下可執(zhí)行文件的加載過程.execve()系統(tǒng)調用的入口是sys_execve().代碼如下:
asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;
//將用戶空間的第一個參數(也就是可執(zhí)行文件的路徑)復制到內核
filename = getname((char __user *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
error = do_execve(filename,
(char __user * __user *) regs.ecx,
(char __user * __user *) regs.edx,
s);
if (error == 0) {
task_lock(current);
current->ptrace &= ~PT_DTRACE;
task_unlock(current);
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
//釋放內存
putname(filename);
out:
return error;
}
系統(tǒng)調用的時候,把參數依次放在:ebx,ecx,edx,esi,edi,ebp寄存器.詳情請參閱本站 Linux中斷處理之系統(tǒng)調用>>.第一個參數為可執(zhí)行文件路徑,第二個參數為參數的個數,第三個參數為可執(zhí)行文件對應的參數.
do_execve()是這個系統(tǒng)調用的核心,它的代碼如下:
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs)
{
//linux_binprm:保存可執(zhí)行文件的一些參數
struct linux_binprm *bprm;
struct file *file;
unsigned long env_p;
int retval;
retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_ret;
//在內核中打開這個可執(zhí)行文件
file = open_exec(filename);
retval = PTR_ERR(file);
//如果打開失敗
if (IS_ERR(file))
goto out_kfree;
sched_exec();
bprm->file = file;
bprm->filename = filename;
bprm->interp = filename;
//bprm初始化,主要是初始化bprm->mm
retval = bprm_mm_init(bprm);
if (retval)
goto out_file;
//計算參數個數
bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc)
goto out_mm;
//環(huán)境變量個數
bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc)
goto out_mm;
retval = security_bprm_alloc(bprm);
if (retval)
goto out;
//把要加載文件的前128 讀入bprm->buf
retval = prepare_binprm(bprm);
if (retval
goto out;
//copy第一個參數filename
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval
goto out;
//bprm->exec:參數的起始地址(從上往下方向)
bprm->exec = bprm->p;
//copy環(huán)境變量
retval = copy_strings(bprm->envc, envp, bprm);
if (retval
goto out;
//環(huán)境變量存放的起始地址
env_p = bprm->p;
//copy可執(zhí)行文件所帶參數
retval = copy_strings(bprm->argc, argv, bprm);
if (retval
goto out;
//環(huán)境變量的長度
bprm->argv_len = env_p - bprm->p;
//到鏈表中尋找合適的加載模塊
retval = search_binary_handler(bprm,regs);
if (retval >= 0) {
/* execve success */
free_arg_pages(bprm);
security_bprm_free(bprm);
acct_update_integrals(current);
kfree(bprm);
return retval;
}
out:
free_arg_pages(bprm);
if (bprm->security)
security_bprm_free(bprm);
out_mm:
if (bprm->mm)
mmput (bprm->mm);
out_file:
if (bprm->file) {
allow_write_access(bprm->file);
fput(bprm->file);
}
out_kfree:
kfree(bprm);
out_ret:
return retval;
}
研究代碼之前,我們先考慮一下進程的空間安排結構.在本站的中的malloc機制分析>>曾經描述過.我們再次把進程的空間結構圖列出,如下如示:
用戶棧位于進程空間的最高部份.那進程初始化時,用戶棧存放的是什么呢?是參數.進程在執(zhí)行時會到棧中去取運行時所需的參數.這里所謂的參數包含了可執(zhí)行程序所帶的參數和環(huán)境變量.例如:在shell上執(zhí)行”echo hello,eric” .echo程序帶有二個參數.argv[0] = “echo”,argv[1] = “hello,eric”即第一個參數為程序名稱.其后的參數分別是運行進程所帶的參數.當然,在上面這個例子中沒有列出環(huán)境變量.一般的.在參數后面都跟了一個NULL.表示參數已經結束了,在上例中argv[1]后面的一個字節(jié)是NULL.如下圖所示:
這樣程序在運行的時候就可以方便的確定參數及環(huán)境變量的個數.
現在,我們可以分析代碼了.
bprm_mm_init()是bprm的初始化函數,我們跟蹤進去看它是怎么樣初始化的.
int bprm_mm_init(struct linux_binprm *bprm)
{
int err;
struct mm_struct *mm = NULL;
//分配一個mm
//mm_alloc我們在進程創(chuàng)建的時候已經分析過了,值得注意的是,它會調用mm_init()來為
//進程的用戶空間建立PGD->PMD映射
bprm->mm = mm = mm_alloc();
err = -ENOMEM;
if (!mm)
goto err;
err = init_new_context(current, mm);
if (err)
goto err;
//初始化bprm->mm
err = __bprm_mm_init(bprm);
if (err)
goto err;
return 0;
err:
if (mm) {
bprm->mm = NULL;
mmdrop(mm);
}
return err;
}
重點是在__bprm_mm_init():
static int __bprm_mm_init(struct linux_binprm *bprm)
{
int err = -ENOMEM;
struct vm_area_struct *vma = NULL;
struct mm_struct *mm = bprm->mm;
//分配一個VMA
bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma)
goto err;
down_write(&mm->mmap_sem);
vma->vm_mm = mm;
//STACK_TOP_MAX:進程用戶空間的最高值
//對應進程的棧頂
vma->vm_end = STACK_TOP_MAX;
vma->vm_start = vma->vm_end - PAGE_SIZE;
vma->vm_flags = VM_STACK_FLAGS;
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
//將VM插入mm表示的進程空間結構
err = insert_vm_struct(mm, vma);
if (err) {
up_write(&mm->mmap_sem);
goto err;
}
mm->stack_vm = mm->total_vm = 1;
up_write(&mm->mmap_sem);
//bprm->p:用戶棧的棧指針
bprm->p = vma->vm_end - sizeof(void *);
return 0;
err:
if (vma) {
bprm->vma = NULL;
kmem_cache_free(vm_area_cachep, vma);
}
return err;
}
上面的操作看起來比較隱晦,我們把它的操作用下面的圖表示:
在這里為bprm->mm的初始化下了這么多功夫是為什么呢?它跟進程的mm有什么關系?不急,繼續(xù)耐著性子看代碼,我們會看到它的用途的.
繼續(xù)分析do_execve()中所調用的子函數.
Count()來用計算可執(zhí)行文件的參數或者環(huán)境變量的個數.它的代碼如下:
static int count(char __user * __user * argv, int max)
{
int i = 0;
if (argv != NULL) {
for (;;) {
char __user * p;
//在內核空間中取argv的值
//取值失敗
if (get_user(p, argv))
return -EFAULT;
//如果為空。說明已經取到了NULL。結束了
if (!p)
break;
argv++;
//參數個數超過了允許的最大值
if(++i > max)
return -E2BIG;
cond_resched();
}
}
return i;
}
這個函數的原理是利用參數后面是以NULL結尾的,不懂的請回個頭去看下上面的分析.
疑問:在取參數個數的時候,會進行用戶空間到內核空間的copy.但是這里僅僅是得知它的個數,在后面的操作中,還會繼續(xù)去取參數值放到bprm->mm表示的空間中.這里有兩次拷copy.可不可把這兩個過程放在一起.省掉一次從用戶空間到內核空間的COPY呢?
prepare_binprm()會將文件的前128字節(jié)copy到bprm->buf.代碼片段如下所示:
int prepare_binprm(struct linux_binprm *bprm)
{
……
……
memset(bprm->buf,0,BINPRM_BUF_SIZE);
//#define BINPRM_BUF_SIZE 128
return kernel_read(bprm->file,0,bprm->buf,BINPRM_BUF_SIZE);
}
將具體的參數COPY到bprm->mm所表示的存儲空間中是由copy_strings()完成的.它的代碼有一點繁鎖.如下示:
/*
參數含義:
argc:參數個數
argv:參數數組
*/
static int copy_strings(int argc, char __user * __user * argv,
struct linux_binprm *bprm)
{
struct page *kmapped_page = NULL;
char *kaddr = NULL;
unsigned long kpos = 0;
int ret;
while (argc-- > 0) {
char __user *str;
int len;
unsigned long pos;
//取數組相應項,將其放至str中
//COPY失敗,或者參數長度非法
if (get_user(str, argv+argc) ||
!(len = strnlen_user(str, MAX_ARG_STRLEN))) {
ret = -EFAULT;
goto out;
}
//判斷參數長度是否超過允許的最大值
if (!valid_arg_len(bprm, len)) {
ret = -E2BIG;
goto out;
}
/* We're going to work our way backwords. */
//當前的位置
pos = bprm->p;
str += len;
bprm->p -= len;
while (len > 0) {
int offset, bytes_to_copy;
offset = pos % PAGE_SIZE;
if (offset == 0)
offset = PAGE_SIZE;
bytes_to_copy = offset;
if (bytes_to_copy > len)
bytes_to_copy = len;
offset -= bytes_to_copy;
pos -= bytes_to_copy;
str -= bytes_to_copy;
len -= bytes_to_copy;
if (!kmapped_page || kpos != (pos & PAGE_MASK)) {
struct page *page;
//根據映射關系得到pos地址在bprm->mm中所映射的頁面
page = get_arg_page(bprm, pos, 1);
if (!page) {
ret = -E2BIG;
goto out;
}
if (kmapped_page) {
flush_kernel_dcache_page(kmapped_page);
//斷開臨時映射
kunmap(kmapped_page);
//減少引用計數
put_arg_page(kmapped_page);
}
kmapped_page = page;
//將臨時映射到內核
kaddr = kmap(kmapped_page);
kpos = pos & PAGE_MASK;
flush_arg_page(bprm, kpos, kmapped_page);
}
//copy參數至剛才映射的頁面
if (copy_from_user(kaddr+offset, str, bytes_to_copy)) {
ret = -EFAULT;
goto out;
}
}
}
ret = 0;
out:
if (kmapped_page) {
flush_kernel_dcache_page(kmapped_page);
kunmap(kmapped_page);
put_arg_page(kmapped_page);
}
return ret;
}
我們在前面看到,并沒有給VM映射實際的內存,在這里COPY參數的時候,必然會引起缺頁異常,再由缺頁異常程序處理缺頁的情況.
經過上面的過程之后,bprm->mm表示的存儲空間如下所示:
經過一系統(tǒng)的初始化之后,可以尋找該文件的加載module了.這是由search_binary_handler()完成的.在深入到這段代碼之前.我們有必要討論一下linux可執(zhí)文件模塊的組織.
在linux內核,用linux_binfmt結構來表示每一個加載模塊.它的定義如下:
struct linux_binfmt {
//用來構成鏈表
struct list_head lh;
//所屬的module
struct module *module;
//加載可執(zhí)行文件
int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
//加載共享庫
int (*load_shlib)(struct file *);
int (*core_dump)(long signr, struct pt_regs *regs, struct file *file, unsigned long limit);
unsigned long min_coredump; /* minimal dump size */
int hasvdso;
}
結構中的lh將之組成一個鏈表,這個鏈表的表頭是formats.
為了說明,我們來看一下如何注冊一個可執(zhí)行文件的加載模塊.
int register_binfmt(struct linux_binfmt * fmt)
{
if (!fmt)
return -EINVAL;
write_lock(&binfmt_lock);
//將其添加之鏈表
list_add(&fmt->lh, &formats);
write_unlock(&binfmt_lock);
return 0;
}
所以,在加載可執(zhí)文件的時候,只要遍歷formats這個鏈表,然后依次按module加載這個可執(zhí)行文件.這正是search_binary_handler()所做的.代碼如下:
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
int try,retval;
struct linux_binfmt *fmt;
#ifdef __alpha__
/* handle /sbin/loader.. */
{
struct exec * eh = (struct exec *) bprm->buf;
if (!bprm->loader && eh->fh.f_magic == 0x183 &&
(eh->fh.f_flags & 0x3000) == 0x3000)
{
struct file * file;
unsigned long loader;
allow_write_access(bprm->file);
fput(bprm->file);
bprm->file = NULL;
loader = bprm->vma->vm_end - sizeof(void *);
file = open_exec("/sbin/loader");
retval = PTR_ERR(file);
if (IS_ERR(file))
return retval;
/* Remember if the application is TASO. */
bprm->sh_bang = eh->ah.entry
bprm->file = file;
bprm->loader = loader;
retval = prepare_binprm(bprm);
if (retval
return retval;
/* should call search_binary_handler recursively here,
but it does not matter */
}
}
#endif
retval = security_bprm_check(bprm);
if (retval)
return retval;
/* kernel module loader fixup */
/* so we don't try to load run modprobe in kernel space. */
set_fs(USER_DS);
retval = audit_bprm(bprm);
if (retval)
return retval;
retval = -ENOENT;
//這里會循環(huán)兩次.待模塊加載之后再遍歷一次
for (try=0; try
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
//加載函數
&