本文的內容包括:
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函數的一個特別之處是他從來不會成功返回,而總是實現了一次完全的變身。