通過gdb跟蹤Linux核心裝載和啟動可執行程式過程

來源:互聯網
上載者:User

標籤:

吳樂 山東師範大學

《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000

實驗目的:通過對一個簡單的可執行程式用gdb進行代碼的跟蹤,剖析linux核心是如何動態和靜態裝載和啟動程式的,進而總結linux核心可執行程式載入的過程。

一、實驗過程

1、編寫一個簡單的Exec的建立進程的函數

2、開啟gdb,並設定好如下斷點

3、開始跟蹤,找到第一個斷點。

(主程式還未建立子進程)

4、繼續在此斷點處逐步跟蹤

5、找到設定的第二個斷點,並列出

6、跟蹤到裝載new_ip處,查看其地址

7、明顯看到,此處載入的IP地址與程式入口地址相同

8、結束跟蹤,觀察其他斷點方法類似。

二、可執行檔的載入和運行

1、execve()系統調用的入口是sys_execve().代碼如下:

int sys_execve(struct pt_regs regs){    int error;    char * filename;    //將使用者空間的第一個參數(也就是可執行檔的路徑)複製到核心    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,            &regs);    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;}
由此可見進行系統調用時,把參數依次放在ebx,ecx,edx,esi,edi,ebp寄存器.
注意其中第一個參數為可執行檔路徑,第二個參數為參數的個數,第三個參數為可執行檔對應的參數.

2、do_execve()是這個系統調用的主要部分,它的代碼如下:

int do_execve(char * filename,    char __user *__user *argv,    char __user *__user *envp,    struct pt_regs * regs){    //linux_binprm:儲存可執行檔的一些參數    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;    //在核心中開啟這個可執行檔    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;    //環境變數個數    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環境變數    retval = copy_strings(bprm->envc, envp, bprm);    if (retval         goto out;    //環境變數存放的起始地址    env_p = bprm->p;    //copy可執行檔所帶參數    retval = copy_strings(bprm->argc, argv, bprm);    if (retval         goto out;    //環境變數的長度    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;}

3、在載入可執檔案的時候,需要遍曆formats這個鏈表,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;         //這裡會迴圈兩次.待模組載入之後再遍曆一次         for (try=0; try                   read_lock(&binfmt_lock);                   list_for_each_entry(fmt, &formats, lh) {                            //載入函數                            int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;                            if (!fn)                                     continue;                            if (!try_module_get(fmt->module))                                     continue;                            read_unlock(&binfmt_lock);                            //運行載入函數,如果載入末成功,則繼續遍曆                            retval = fn(bprm, regs);                            //載入成功了                            if (retval >= 0) {                                     put_binfmt(fmt);                                     allow_write_access(bprm->file);                                     if (bprm->file)                                               fput(bprm->file);                                     bprm->file = NULL;                                     current->did_exec = 1;                                     proc_exec_connector(current);                                     return retval;                            }                            read_lock(&binfmt_lock);                            put_binfmt(fmt);                            if (retval != -ENOEXEC || bprm->mm == NULL)                                     break;                            if (!bprm->file) {                                     read_unlock(&binfmt_lock);                                     return retval;                            }                   }                   read_unlock(&binfmt_lock);                   //所有模組載入這個可執行檔失敗,則載入其它模組再試一次                   if (retval != -ENOEXEC || bprm->mm == NULL) {                            break;                            //CONFIG_KMOD:動態載入模組標誌#ifdef CONFIG_KMOD                   }else{#define printable(c) (((c)==‘\t‘) || ((c)==‘\n‘) || (0x20                            if (printable(bprm->buf[0]) &&                                printable(bprm->buf[1]) &&                                printable(bprm->buf[2]) &&                                printable(bprm->buf[3]))                                     break; /* -ENOEXEC */                            request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));#endif                   }         }         return retval;}

4、喚醒父進程的過程以及棧空間的布局代碼如下.

static int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs){     ……     ……     current->mm->start_stack =         (unsigned long) create_aout_tables((char __user *) bprm->p, bprm);#ifdef __alpha__     regs->gp = ex.a_gpvalue;#endif     start_thread(regs, ex.a_entry, current->mm->start_stack);     ……}Creat_aout_tables()代碼如下:static unsigned long __user *create_aout_tables(char __user *p, struct linux_binprm * bprm){    char __user * __user *argv;    char __user * __user *envp;    unsigned long __user *sp;    //可執行檔的參數個數    int argc = bprm->argc;    //環境變數的個數    int envc = bprm->envc;    //sp初始化成p,也即bprm->p    sp = (void __user *)((-(unsigned long)sizeof(char *)) & (unsigned long) p);#ifdef __sparc__    /* This imposes the proper stack alignment for a new process. */    sp = (void __user *) (((unsigned long) sp) & ~7);    if ((envc+argc+3)&1) --sp;#endif#ifdef __alpha__/* whee.. test-programs are so much fun. */    put_user(0, --sp);    put_user(0, --sp);    if (bprm->loader) {        put_user(0, --sp);        put_user(0x3eb, --sp);        put_user(bprm->loader, --sp);        put_user(0x3ea, --sp);    }    put_user(bprm->exec, --sp);    put_user(0x3e9, --sp);#endif    sp -= envc+1;    envp = (char __user * __user *) sp;    sp -= argc+1;    argv = (char __user * __user *) sp;#if defined(__i386__) || defined(__mc68000__) || defined(__arm__) || defined(__arch_um__)    put_user((unsigned long) envp,--sp);    put_user((unsigned long) argv,--sp);#endif    put_user(argc,--sp);    current->mm->arg_start = (unsigned long) p;        while (argc-->0) {        char c;        put_user(p,argv++);        do {            get_user(c,p++);        } while (c);    }    put_user(NULL,argv);    current->mm->arg_end = current->mm->env_start = (unsigned long) p;    while (envc-->0) {        char c;        put_user(p,envp++);        do {            get_user(c,p++);        } while (c);    }    put_user(NULL,envp);    current->mm->env_end = (unsigned long) p;    return sp;}


ip這裡已經指向main函數入口地址了,此後的工作都由start_thread()函數完成。具體過程可參見我的另一片部落格:

http://www.cnblogs.com/wule/p/4404504.html

三、總結linux核心可執行程式載入的過程

  首先建立父進程,然後通過調用fork()系統調用建立一個新的進程,然後新的進程調用execve()系統調用執行指定的ELF檔案。 主進程繼續返回等待新進程執行結束,然後重新等待使用者輸入命令。execve()系統調用被定義在unistd.h,它的原型如下:
    int execve(const char *filenarne, char *const argv[], char *const envp[]);
    它的三個參數分別是被執行的程式檔案名稱、執行參數和環境變最。Glibc對execvp()系統調用進行了封裝,提供了execl(), execlp(), execle(), execv()和execvp()等5個不同形式的exec系列API,它們只是在調用的參數形式上有所區別,但最終都會調用到execve()這個系統中。

    調用execve()系統調用之後,再調用核心的入口sys_execve()。 sys_execve()進行一些參數的檢查複製之後,調用do_execve()。 因為可執行檔不止ELF一種,還有java程式和以“#!”開始的指令碼程式等, 所以do_execve()會首先檢查被執行檔案,讀取前128個位元組,特別是開頭4個位元組的魔數,用以判斷可執行檔的格式。 如果是解釋型語言的指令碼,前兩個位元組“#!"就構成了魔數,系統一旦判斷到這兩個位元組,就對後面的字串進行解析,以確定程式解譯器的路徑。

    當do_execve()讀取了這128個位元組的檔案頭部之後,然後調用search_binary_handle()去搜尋和匹配合適的可執行檔裝載處理過程。Linux中所有被支援的可執行檔格式都有相應的裝載處理過程,search_binary_handle()會通過判斷檔案頭部的魔數確定檔案的格式,並且調用相應的裝載處理過程。如ELF用load_elf_binary(),a.out用load_aout_binary(),指令碼用load_script()。其中ELF裝載過程的主要步驟是:
    ①檢查ELF可執行檔格式的有效性,比如魔數、程式頭表中段(Segment)的數量。
    ②尋找動態連結的”.interp”段(該段儲存可執行檔所需要的動態連結器的路徑),設定動態連結器路徑。
    ③根據ELF可執行檔的程式頭表的描述,對ELF檔案進行映射,比如代碼、資料、唯讀資料。
    ④初始化ELF進程環境,比如進程啟動時EDX寄存器的地址應該是DT_FINI的地址(結束代碼地址)。
    ⑤將系統調用的返回地址修改成ELF可執行檔的進入點,這個進入點取決於程式的連結方式,對於靜態連結的ELF可執行檔,這個程式入口就是ELF檔案的檔案頭中e_enEry所指的地址;對於動態連結的ELF可執行檔,程式進入點是動態連結器。
    當ELF被load_elf_binary()裝載完成後,函數返回至do_execve()在返回至sys_execve()。在load_elf_binary()中(第5步)系統調用的返回地址已經被改成ELF程式的入口地址了。 所以當sys_execve()系統調用從核心態返回到使用者態時,EIP寄存器直接跳轉到了ELF程式的入口地址,於是新的程式開始執行,ELF可執行檔裝載完成。

 

通過gdb跟蹤Linux核心裝載和啟動可執行程式過程

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.