《深入理解Linux內核》 第二十章:深入理解 Linux 程序執行機制(Program Execution)
關鍵詞:exec 系列系統調用、可執行文件格式(ELF)、用戶地址空間、內存映射、動態鏈接、棧初始化、入口點、共享庫、內核態與用戶態切換
一、概述:程序是如何被執行的?
1.1 本質
Linux 中,“程序執行”是指某個已存在的進程調用 exec()
系列系統調用后,由內核將該進程的上下文替換為另一個程序的上下文,從而在相同 PID 下運行新的程序。
與 fork()
創建新進程不同,exec()
并不創建新進程,而是替換現有進程的地址空間。
1.2 系統調用家族
int execl(const char *path, const char *arg, ..., NULL);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ..., NULL, char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
內核中的入口點為:
SYSCALL_DEFINE3(execve, const char __user *, filename,const char __user *const __user *, argv,const char __user *const __user *, envp)
二、從用戶態到內核態
2.1 用戶空間的 execve()
程序調用 exec 函數族(如 execv()
)最終都會轉換為 execve()
系統調用。
其傳入參數包括:
- 程序路徑
filename
- 命令行參數
argv
- 環境變量
envp
2.2 內核入口
系統調用處理流程:
- 用戶態調用 execve;
- CPU 切換到內核態;
- 內核從系統調用表中查找
sys_execve()
; - 調用
do_execve()
→do_execveat_common()
; - 進入真正的程序替換邏輯。
三、execve 內核實現流程
程序執行的內核主干邏輯如下:
do_execveat_common()└── exec_binprm()└── search_binary_handler()└── load_elf_binary() 或其他格式
3.1 創建 binprm 結構
struct linux_binprm {char buf[BINPRM_BUF_SIZE];struct file *file;...
};
- 包含傳入參數、程序路徑;
- 讀取前128字節,判斷文件格式;
- 設置執行權限、清除信號等。
3.2 判斷可執行文件格式
執行 search_binary_handler()
:
- 檢查 ELF 標志(前4字節為 0x7f + ELF);
- 若是腳本(以
#!
開頭),調用load_script()
; - 若是 ELF 文件,調用
load_elf_binary()
。
四、加載 ELF 可執行文件
4.1 ELF 文件結構
ELF(Executable and Linkable Format)是 Linux 下標準的可執行文件格式。
主要結構:
ELF Header
|
+-- Program Header Table (段表)
|
+-- Section Header Table (僅編譯調試時使用)
|
+-- 數據段、代碼段、堆、符號表等
- ELF Header:描述整個文件;
- Program Header Table:決定哪些段需要加載;
- 每個段描述:起始地址、偏移、大小、屬性(可執行、可寫等)。
4.2 ELF 加載流程
load_elf_binary()
實現步驟:
- 驗證 ELF 魔數;
- 解析 Program Header Table;
- 使用
do_mmap()
映射段到用戶地址空間; - 設置
mm->start_code
、start_data
、brk
; - 初始化用戶棧;
- 設置
e_entry
(程序入口點); - 調用
start_thread()
設置寄存器(EIP / RIP); - 切換到用戶態開始執行。
五、構建新用戶地址空間
5.1 mm_struct 的替換
每個進程都有一個 mm_struct
:
struct mm_struct {struct vm_area_struct *mmap;struct pgd_t *pgd;...
};
當調用 execve 時,原有的地址空間會被釋放(mm_release()
),新的 mm_struct
被創建并綁定。
5.2 do_mmap 的作用
調用 do_mmap()
將 ELF 段映射到用戶空間:
.text
映射為可執行段;.data
映射為可寫段;.bss
用于初始化堆;- 其他段如
.rodata
也被映射;
mmap 映射區域都以 vm_area_struct
記錄,最終組成進程虛擬內存布局。
六、用戶棧與參數傳遞
6.1 參數與環境變量的拷貝
exec 調用時會將 argv
與 envp
拷貝到內核緩沖區,再構建用戶棧:
- 棧頂:參數數量 argc;
- 緊接著:argv 指針數組;
- 然后:envp 指針數組;
- 最后是 NULL terminator;
6.2 棧構建邏輯
setup_arg_pages()
create_elf_tables()
- 將參數字符串數據拷貝到用戶棧;
- 設置
AT_PHDR
、AT_ENTRY
等 auxv; - 創建適配動態鏈接器的數據結構(如
ld.so
);
七、動態鏈接與共享庫加載
7.1 動態鏈接器的作用
如果 ELF 是動態鏈接的,其 PT_INTERP
段指定了動態鏈接器(如 /lib/ld-linux.so.2
)。
流程:
-
加載主 ELF 文件;
-
加載動態鏈接器;
-
動態鏈接器運行在用戶態,負責:
- 加載所需
.so
文件; - 執行符號解析與重定位;
- 最終跳轉到
main()
函數。
- 加載所需
7.2 預加載庫
環境變量:
LD_PRELOAD=/lib/myhook.so ./app
可在程序啟動前注入庫,進行函數劫持等操作。
八、執行權限與文件檢查
8.1 權限驗證
- 檢查可執行文件是否有執行權限;
- 檢查是否可讀取;
- 判斷 SUID/SGID 是否生效;
- 對腳本文件,檢查
#!
指向的解釋器;
8.2 setuid 程序執行注意點
當可執行文件具有 SUID 權限時:
- 進程會提升有效 UID 為目標用戶;
- 內核需清除大部分內存內容,防止信息泄漏;
- 需要設置
secureexec
標志位,屏蔽某些危險變量(如LD_PRELOAD
);
九、執行結果與返回路徑
9.1 執行完成前的最后一步
- 在 execve 完成所有加載后,調用
start_thread()
設置 PC/SP; - 切換到用戶態入口點開始執行;
- 若失敗,則返回錯誤碼。
9.2 execve 成功永不返回
一旦 execve 成功,舊進程空間完全被新程序替換,除非失敗,調用永不返回。
十、文件格式支持機制(binfmt)
Linux 支持多種可執行文件格式(ELF, a.out, scripts, Java 等):
struct linux_binfmt {int (*load_binary)(struct linux_binprm *);int (*load_shlib)(struct file *);...
};
常見格式注冊:
register_binfmt(&elf_format);
register_binfmt(&script_format);
這些接口掛載在 search_binary_handler()
中被遍歷查找處理程序。
十一、源碼路徑與調試技巧
路徑 | 說明 |
---|---|
fs/exec.c | execve 核心邏輯 |
fs/binfmt_elf.c | ELF 加載實現 |
fs/binfmt_script.c | 腳本執行邏輯 |
arch/x86/kernel/process.c | 架構相關 start_thread() 實現 |
include/linux/binfmts.h | binfmt 接口定義 |
/proc/self/maps | 當前進程地址空間布局查看 |
調試工具:
strace ./a.out
:追蹤 execve 執行;readelf -a ./a.out
:查看 ELF 內容;lsof -p PID
:查看進程打開的文件;gdb
:調試執行流程與鏈接器行為。
十二、小結
- execve 系統調用是 Linux 執行程序的核心;
- 內核根據 ELF 或腳本格式加載目標程序;
- 構建新的用戶空間,包括棧、段映射、鏈接器加載;
- 支持 SUID、環境變量注入、安全過濾;
- 動態鏈接器負責完成后續 .so 加載與符號重定位;
- 內核中設計清晰,分層合理,是內核與用戶交互的關鍵橋梁。