Linux內核分析(七)系統調用execve處理過程

本文的內容包括:

1. 用execve系統調用加載和執行一個可執行程序的代碼演示

2. 用gdb跟蹤系統調用execve的執行過程

3. execve系統調用處理過程分析

 

一、如何用execve系統調用加載一個可執行程序

下面的代碼可以展示如何用execlp函數啓動一個新的進程,execlp是對系統調用execve的一層封裝。

 其中第19行的輸出是故意加上的。執行結果如下,可以看到第19行的輸出根本沒有顯示出來,原因就是exec系列函數會用被加載的進程替換掉原來的進程。

 

在Linux幫助手冊中關於exec系列函數的說明也說明了這一點:

execve() does not return on success, and the text, data, bss, and stack of the calling process are overwritten by that of the program loaded. (execve函數執行成功後不會返回,而且代碼段,數據段,bss段和調用進程的棧會被被加載進來的程序覆蓋掉)

二、 用gdb跟蹤execve系統調用的實驗方法

    要用gdb調試execve系統調用,首先需要在我們的menuos中添加execve系統調用的入口,程序和上面的代碼差不多,只是我們的menuos中還沒有ls命令,所以我們需要做另外一個可執行程序讓execve系統調用來加載。代碼如下所示:

其中的載入的hello程序是我們準備的一個hello world程序,他做的事情就是簡單地輸出一行Hello Linux Kernel!的文字。我們用靜態編譯的方式構造這個hello程序,然後放到我們的根文件系統的根目錄下,使用的命令和執行效果如下所示:

 

要調試我們的exec程序,只需要重新使用qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s啓動,然後用另一箇中斷打開gdb遠程調試就可以了。不需要使用-S參數,因爲我們不需要調試啓動過程,只需要等系統啓動之後設置execve的斷點,然後執行exec命令就可以追蹤到了。

 

我們在下面幾個函數中添加斷點

1. do_execve

2. do_execve_common

3. exec_binprm

實驗截圖:

 

三、 execve系統調用處理過程分析

從上面的實驗可以看到,主要的處理過程都在do_execve_common() 函數中,我們來分析這個函數的代碼。 爲了更清楚的看到這個函數的結構,下面代碼刪掉了一些錯誤處理的部分。

1430 static int do_execve_common(struct filename *filename,

1431                                 struct user_arg_ptr argv,

1432                                 struct user_arg_ptr envp)

1433 {

1434         struct linux_binprm *bprm;  // 用於解析ELF文件的結構

1435         struct file *file;

1436         struct files_struct *displaced;

1437         int retval;

1456         current->flags &= ~PF_NPROC_EXCEEDED;  // 標記程序已被執行

1458         retval = unshare_files(&displaced);  // 拷貝當前運行進程的fd到displaced中

1463         bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);

1467         retval = prepare_bprm_creds(bprm);   // 創建一個新的憑證

1471         check_unsafe_exec(bprm);             // 必要的安全檢查

1472         current->in_execve = 1;

1474         file = do_open_exec(filename);       // 打開要執行的文件

1479         sched_exec(); // 下面是Linux代碼中對這個函數的解釋:

// execve() is a valuable balancing opportunity, because at

// this point the task has the smallest effective memory and cache footprint.

1481         bprm->file = file;

1482         bprm->filename = bprm->interp = filename->name;

1484         retval = bprm_mm_init(bprm);       // 爲ELF文件分配內存,其中的一些值還是

// 默認值,需要在後面的函數中修正

1488         bprm->argc = count(argv, MAX_ARG_STRINGS);

1492         bprm->envc = count(envp, MAX_ARG_STRINGS);

1496         retval = prepare_binprm(bprm);     // 從打開的可執行文件中讀取信息,填充bprm結構

     // 下面的4句是將運行參數和環境變量都拷貝到bprm結構的內存空間中

1500         retval = copy_strings_kernel(1, &bprm->filename, bprm);

1504         bprm->exec = bprm->p;

1505         retval = copy_strings(bprm->envc, envp, bprm);

1509         retval = copy_strings(bprm->argc, argv, bprm);

     // 開始執行加載到內存中的ELF文件

1513         retval = exec_binprm(bprm);

 

             /* 執行完成,清理並返回 */

1518         current->fs->in_exec = 0;

1519         current->in_execve = 0;

1520         acct_update_integrals(current);

1521         task_numa_free(current);

1522         free_bprm(bprm);

1523         putname(filename);

1524         if (displaced)

1525                 put_files_struct(displaced);

1526         return retval;

1547 }

 

    上面的過程中,最重要的莫過於1513行的exec_binprm()函數。下面來看exec_binprm的實現:

1405 static int exec_binprm(struct linux_binprm *bprm)

1406 {

1407         pid_t old_pid, old_vpid;

1408         int ret;

1409

1410         /* Need to fetch pid before load_binary changes it */

1411         old_pid = current->pid;

1412         rcu_read_lock();

1413         old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent))     ;

1414         rcu_read_unlock();

1415

1416         ret = search_binary_handler(bprm);

1417         if (ret >= 0) {

1418                 audit_bprm(bprm);

1419                 trace_sched_process_exec(current, old_pid, bprm);

1420                 ptrace_event(PTRACE_EVENT_EXEC, old_vpid);

1421                 proc_exec_connector(current);

1422         }

1423

1424         return ret;

1425 }

  其中,需要理解的就是search_binary_handler()函數,代碼如下:

1349 /*

1350  * cycle the list of binary formats handler, until one recognizes the image

1351  */

1352 int search_binary_handler(struct linux_binprm *bprm)

1353 {

1354         bool need_retry = IS_ENABLED(CONFIG_MODULES);

1355         struct linux_binfmt *fmt;

1356         int retval;

1357

1358         /* This allows 4 levels of binfmt rewrites before failing hard. */

1359         if (bprm->recursion_depth > 5)

1360                 return -ELOOP;

1361

1362         retval = security_bprm_check(bprm);   // 檢查用戶是否有權限運行該文件

1363         if (retval)

1364                 return retval;

1365

1366         retval = -ENOENT;

1367  retry:

1368         read_lock(&binfmt_lock);

1369         list_for_each_entry(fmt, &formats, lh) { // 嘗試每一種格式的解析函數,

// 支持的格式由__register_binfmt() 函數註冊進來

1370                 if (!try_module_get(fmt->module))

1371                         continue;

1372                 read_unlock(&binfmt_lock);

1373                 bprm->recursion_depth++;

1374                 retval = fmt->load_binary(bprm); // 關鍵步驟,調用合適格式的處理函數加載該可執行文件

// 對ELF文件來說,這個處理函數是 load_elf_binary

1375                 read_lock(&binfmt_lock);

1376                 put_binfmt(fmt);

1377                 bprm->recursion_depth--;

1378                 if (retval < 0 && !bprm->mm) {

1379                         /* we got to flush_old_exec() and failed after it */

1380                         read_unlock(&binfmt_lock);

1381                         force_sigsegv(SIGSEGV, current);

1382                         return retval;

1383                 }

1384                 if (retval != -ENOEXEC || !bprm->file) {

1385                         read_unlock(&binfmt_lock);

1386                         return retval;

1387                 }

1388         }

1389         read_unlock(&binfmt_lock);

1390

1391         if (need_retry) {

1392                 if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&

1393                     printable(bprm->buf[2]) && printable(bprm->buf[3]))

1394                         return retval;

1395                 if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) <      0)

1396                         return retval;

1397                 need_retry = false;

1398                 goto retry;

1399         }

1400

1401         return retval;

1402 }

 

在load_elf_binary()函數中,加載進來的可執行文件將把當前正在執行的進程的內存空間完全覆蓋掉,如果可執行文件是靜態鏈接的文件,進程的IP寄存器值將被設置爲main函數的入口地址,從而開始新的進程;而如果可執行文件是動態鏈接的,IP的值將被設置爲加載器ld的入口地址,是程序的運行由該加載器接管,ld會處理一些依賴的動態鏈接庫相關的處理工作,使程序繼續往下執行,而不管哪種執行方式,當前的進程都會被新加載進來的程序完全替換掉,這也是我們最早的那個程序中第19行的信息沒有在終端上顯示的原因。

 

四、總結

簡單總結一下execve系統調用的執行過程:

1. 陷入內核

2. 加載新的可執行文件並進行可執行性檢查

3. 將新的可執行文件映射到當前運行進程的進程空間中,並覆蓋原來的進程數據

4. 將EIP的值設置爲新的可執行程序的入口地址。如果可執行程序是靜態鏈接的程序,或不需要其他的動態鏈接庫,則新的入口地址就是新的可執行文件的main函數地址;如果可執行程序還需要其他的動態鏈接庫,則入口地址是加載器ld的入口地址

5. 返回用戶態,程序從新的EIP出開始繼續往下執行。至此,老進程的上下文已經被新的進程完全替代了,但是進程的PID還是原來的。從這個角度來看,新的運行進程中已經找不到原來的對execve調用的代碼了,所以execve函數的一個特別之處是他從來不會成功返回,而總是實現了一次完全的變身。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章