Linux系統(tǒng)中的init進(jìn)程(pid=1)是除了idle進(jìn)程(pid=0,,也就是init_task)之外另一個(gè)比較特殊的進(jìn)程,,它是Linux內(nèi)核開始建立起進(jìn)程概念時(shí)第一個(gè)通過kernel_thread產(chǎn)生的進(jìn)程,,其開始在內(nèi)核態(tài)執(zhí)行,然后通過一個(gè)系統(tǒng)調(diào)用,,開始執(zhí)行用戶空間的/sbin/init程序,,期間Linux內(nèi)核也經(jīng)歷了從內(nèi)核態(tài)到用戶態(tài)的特權(quán)級(jí)轉(zhuǎn)變,/sbin/init極有可能產(chǎn)生出了shell,,然后所有的用戶進(jìn)程都有該進(jìn)程派生出來(目前尚未閱讀過/sbin/init的源碼)...
目前我們至少知道在內(nèi)核空間執(zhí)行用戶空間的一段應(yīng)用程序有兩種方法: 1. call_usermodehelper 2. kernel_execve
它們最終都通過int $0x80在內(nèi)核空間發(fā)起一個(gè)系統(tǒng)調(diào)用來完成,,這個(gè)過程我在《深入Linux設(shè)備驅(qū)動(dòng)程序內(nèi)核機(jī)制》第9章有過詳細(xì)的描述,對(duì)它的討論最終結(jié)束在 sys_execve函數(shù)那里,,后者被用來執(zhí)行一個(gè)新的程序?,F(xiàn)在一個(gè)有趣的問題是,在內(nèi)核空間發(fā)起的系統(tǒng)調(diào)用,,最終通過sys_execve來執(zhí)行用戶 空間的一個(gè)程序,,比如/sbin/myhotplug,那么該應(yīng)用程序執(zhí)行時(shí)是在內(nèi)核態(tài)呢還是用戶態(tài)呢,?直覺上肯定是用戶態(tài),,不過因?yàn)閏pu在執(zhí)行 sys_execve時(shí)cs寄存器還是__KERNEL_CS,,如果前面我們的猜測(cè)是真的話,必然會(huì)有個(gè)cs寄存器的值從__KERNEL_CS到 __USER_CS的轉(zhuǎn)變過程,,這個(gè)過程是如何發(fā)生的呢,?下面我以kernel_execve為例,來具體討論一下其間所發(fā)生的一些有趣的事情,。
start_kernel在其最后一個(gè)函數(shù)rest_init的調(diào)用中,,會(huì)通過kernel_thread來生成一個(gè)內(nèi)核進(jìn)程,后者則會(huì)在新進(jìn)程環(huán)境下調(diào) 用kernel_init函數(shù),,kernel_init一個(gè)讓人感興趣的地方在于它會(huì)調(diào)用run_init_process來執(zhí)行根文件系統(tǒng)下的 /sbin/init等程序:
<init/mian.c>
- static noinline int init_post(void)
- {
- ...
- run_init_process("/sbin/init");
- run_init_process("/etc/init");
- run_init_process("/bin/init");
- run_init_process("/bin/sh");
- panic("No init found. Try passing init= option to kernel. "
- "See Linux Documentation/init.txt for guidance.");
- }
復(fù)制代碼 run_init_process的核心調(diào)用就是kernel_execve,,后者的實(shí)現(xiàn)代碼是: <arch/x86/kernel/sys_i386_32.c>
- int kernel_execve(const char *filename,
- const char *const argv[],
- const char *const envp[])
- {
- long __res;
- asm volatile ("int $0x80"
- : "=a" (__res)
- : "0" (__NR_execve), "b" (filename), "c" (argv), "d" (envp) : "memory");
- return __res;
- }
復(fù)制代碼 里面是段內(nèi)嵌的匯編代碼,代碼相對(duì)比較簡(jiǎn)單,,核心代碼是int $0x80,,執(zhí)行系統(tǒng)調(diào)用,系統(tǒng)調(diào)用號(hào)__NR_execve放在AX里,,當(dāng)然系統(tǒng)調(diào)用的返回值也是在AX中,,要執(zhí)行的用戶空間應(yīng)用程序路徑名稱保存在 BX中。int $0x80的執(zhí)行導(dǎo)致代碼向__KERNEL_CS:system_call轉(zhuǎn)移(具體過程可參考x86處理器中的特權(quán)級(jí)檢查及Linux系統(tǒng)調(diào)用的實(shí)現(xiàn)一帖). 此處用bx,cx以及dx來保存filename, argv以及envp參數(shù)是有講究的,,它對(duì)應(yīng)著struct pt_regs中寄存器在棧中的布局,,因?yàn)榻酉聛砭蜁?huì)涉及從匯編到調(diào)用C函數(shù)過程,所以匯編程序在調(diào)用C之前,,應(yīng)該把要傳遞給C的參數(shù)在棧中準(zhǔn)備好,。
system_call是一段純匯編代碼:
- <arch/x86/kernel/entry_32.s>
- ENTRY(system_call)
- RING0_INT_FRAME # can't unwind into user space anyway
- pushl_cfi %eax # save orig_eax
- SAVE_ALL
- GET_THREAD_INFO(%ebp)
- # system call tracing in operation / emulation
- testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
- jnz syscall_trace_entry
- cmpl $(nr_syscalls), %eax
- jae syscall_badsys
- syscall_call:
- call *sys_call_table(,%eax,4)
- movl %eax,PT_EAX(%esp) # store the return value
- syscall_exit:
- ...
- restore_nocheck:
- RESTORE_REGS 4 # skip orig_eax/error_code
- irq_return:
- INTERRUPT_RETURN #iret instruction for x86_32
復(fù)制代碼 system_call首先會(huì)為后續(xù)的C函數(shù)的調(diào)用在當(dāng)前堆棧中建立參數(shù)傳遞的環(huán)境(x86_64的實(shí)現(xiàn)要相對(duì)復(fù)雜一點(diǎn),它會(huì)將系統(tǒng)調(diào)用切換到內(nèi)核棧 movq PER_CPU_VAR(kernel_stack),%rsp),,尤其是接下來對(duì)C函數(shù)sys_execve調(diào)用中的struct pt_regs *regs參數(shù),,我在上面代碼中同時(shí)列出了系統(tǒng)調(diào)用之后的后續(xù)操作syscall_exit,從代碼中可以看到系統(tǒng)調(diào)用int $0x80最終通過iret指令返回,,而后者會(huì)從當(dāng)前棧中彈出cs與ip,,然后跳轉(zhuǎn)到cs:ip處執(zhí)行代碼。正常情況下,,x86架構(gòu)上的int n指 令會(huì)將其下條指令的cs:ip壓入堆棧,,所以當(dāng)通過iret指令返回時(shí),原來的代碼將從int n的下條指令繼續(xù)執(zhí)行,,不過如果我們能在后續(xù)的C代碼中改變r(jià)egs->cs與regs->ip(也就是int n執(zhí)行時(shí)壓入棧中的cs與ip),,那么就可以控制下一步代碼執(zhí)行的走向,而 sys_execve函數(shù)的調(diào)用鏈正好利用了這一點(diǎn),,接下來我們很快就會(huì)看到,。SAVE_ALL宏的最后為將ds, es, fs都設(shè)置為__USER_DS,但是此時(shí)cs還是__KERNEL_CS.
核心的調(diào)用發(fā)生在call *sys_call_table(,%eax,4)這條指令上,sys_call_table是個(gè)系統(tǒng)調(diào)用表,,本質(zhì)上就是一個(gè)函數(shù)指針數(shù)組,,我們這里的系 統(tǒng)調(diào)用號(hào)是__NR_execve=11, 所以在sys_call_table中對(duì)應(yīng)的函數(shù)為:
<arch/x86/kernel/syscall_table_32.s>
- ENTRY(sys_call_table)
- .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
- .long sys_exit
- .long ptregs_fork
- .long sys_read
- .long sys_write
- .long sys_open /* 5 */
- .long sys_close
- ...
- .long sys_unlink /* 10 */
- .long ptregs_execve //__NR_execve
- ...
復(fù)制代碼 ptregs_execve其實(shí)就是sys_execve函數(shù): <arch/x86/um/sys_call_table_32.s>
- #define ptregs_execve sys_execve
復(fù)制代碼 而sys_execve函數(shù)的代碼實(shí)現(xiàn)則是: <arch/x86/kernel/process.c>
- /*
- * sys_execve() executes a new program.
- */
- long sys_execve(const char __user *name,
- const char __user *const __user *argv,
- const char __user *const __user *envp, struct pt_regs *regs)
- {
- long error;
- char *filename;
- filename = getname(name);
- error = PTR_ERR(filename);
- if (IS_ERR(filename))
- return error;
- error = do_execve(filename, argv, envp, regs);
- #ifdef CONFIG_X86_32
- if (error == 0) {
- /* Make sure we don't return using sysenter.. */
- set_thread_flag(TIF_IRET);
- }
- #endif
- putname(filename);
- return error;
- }
復(fù)制代碼 注意這里的參數(shù)傳遞機(jī)制!其中的核心調(diào)用是do_execve,后者調(diào)用do_execve_common來干執(zhí)行一個(gè)新程序的活,,在我們這個(gè)例子中要執(zhí) 行的新程序來自/sbin/init,,如果用file命令看一下會(huì)發(fā)現(xiàn)它其實(shí)是個(gè)ELF格式的動(dòng)態(tài)鏈接庫,而不是那種普通的可執(zhí)行文件,,所以 do_execve_common會(huì)負(fù)責(zé)打開,、解析這個(gè)文件并找到其可執(zhí)行入口點(diǎn),這個(gè)過程相當(dāng)繁瑣,,我們不妨直接看那些跟我們問題密切相關(guān)的代 碼,,do_execve_common會(huì)調(diào)用search_binary_handler去查找所謂的binary formats handler,ELF顯然是最常見的一種格式:
<fs/exec.c>
- int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
- {
- ...
- for (try=0; try<2; try++) {
- read_lock(&binfmt_lock);
- list_for_each_entry(fmt, &formats, lh) {
- int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
- ...
- retval = fn(bprm, regs);
- ...
- }
- ...
- }
- }
復(fù)制代碼 代碼中針對(duì)ELF格式的 fmt->load_binary即為load_elf_binary, 所以fn=load_elf_binary, 后續(xù)對(duì)fn的調(diào)用即是調(diào)用load_elf_binary,,這是個(gè)非常長(zhǎng)的函數(shù),,直到其最后,我們才找到所需要的答案:
<fs/binfmt_elf.c>
- static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
- {
- ...
- start_thread(regs, elf_entry, bprm->p);
- ...
- }
復(fù)制代碼 上述代碼中的elf_entry即為/sbin/init中的執(zhí)行入口點(diǎn),, bprm->p為應(yīng)用程序新棧(應(yīng)該已經(jīng)在用戶空間了),,start_thread的實(shí)現(xiàn)為:
<arch/x86/kernel/process_32.c>
- void
- start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
- {
- set_user_gs(regs, 0);
- regs->fs = 0;
- regs->ds = __USER_DS;
- regs->es = __USER_DS;
- regs->ss = __USER_DS;
- regs->cs = __USER_CS;
- regs->ip = new_ip;
- regs->sp = new_sp;
- /*
- * Free the old FP and other extended state
- */
- free_thread_xstate(current);
- }
復(fù)制代碼 在這里,我們看到了__USER_CS的身影,,在x86 64位系統(tǒng)架構(gòu)下,,該值為0x33. start_thread函數(shù)最關(guān)鍵的地方在于修改了regs->cs= __USER_CS, regs->ip= new_ip,其實(shí)就是人為地改變了系統(tǒng)調(diào)用int $0x80指令壓入堆棧的下條指令的地址,,這樣當(dāng)系統(tǒng)調(diào)用結(jié)束通過iret指令返回時(shí),,代碼將從這里的__USER_CS:elf_entry處開始執(zhí) 行,也就是/sbin/init中的入口點(diǎn),。start_thread的代碼與kernel_thread非常神似,不過它不需要象 kernel_thread那樣在最后調(diào)用do_fork來產(chǎn)生一個(gè)task_struct實(shí)例出來了,,因?yàn)槟壳爸恍枰诋?dāng)前進(jìn)程上下文中執(zhí)行代碼,,而不是創(chuàng)建一個(gè)新進(jìn)程。關(guān)于kernel_thread,,我在本版曾有一篇帖子分析過,,當(dāng)時(shí)基于的是ARM架構(gòu)。
所以我們看到,,start_kernel在最后調(diào)用rest_init,,而后者通過對(duì)kernel_thread的調(diào)用產(chǎn)生一個(gè)新進(jìn)程(pid=1),新進(jìn)程在其kernel_init()-->init_post()調(diào)用鏈中將通過run_init_process來執(zhí)行用戶空間的/sbin /init,,run_init_process的核心是個(gè)系統(tǒng)調(diào)用,,當(dāng)系統(tǒng)調(diào)用返回時(shí)代碼將從/sbin/init的入口點(diǎn)處開始執(zhí)行,所以雖然我們知道 post_init中有如下幾個(gè)run_init_process的調(diào)用:
- run_init_process("/sbin/init");
- run_init_process("/etc/init");
- run_init_process("/bin/init");
- run_init_process("/bin/sh");
復(fù)制代碼 但是只要比如/sbin/init被成功調(diào)用,run_init_process中的kernel_execve函數(shù)將無法返回,,因?yàn)樗鼒?zhí)行int $0x80時(shí)壓入堆棧中回家的路徑被后續(xù)的C函數(shù)調(diào)用鏈給改寫了,,這樣4個(gè)run_init_process只會(huì)有一個(gè)有機(jī)會(huì)被成功執(zhí)行,如果這4個(gè)函數(shù)都失敗 了,,那么內(nèi)核將會(huì)panic. 所以內(nèi)核設(shè)計(jì)時(shí)必須確保用來改寫int $0x80壓入棧中的cs和ip的start_thread函數(shù)之后不會(huì)再有其他額外的代碼導(dǎo)致整個(gè)調(diào)用鏈的失敗,,否則代碼將執(zhí)行非預(yù)期的指令,內(nèi)核進(jìn)入不穩(wěn)定狀態(tài),。
最后,,我們來驗(yàn)證一下,所謂眼見為實(shí),,耳聽為虛,。再者,如果驗(yàn)證達(dá)到預(yù)期,,也是很鼓舞人好奇心的極佳方法,。驗(yàn)證的方法我打算采用“Linux設(shè)備驅(qū)動(dòng)模型中的熱插拔機(jī)制及實(shí)驗(yàn)” 中的路線,通過call_usermodehelper來做,,因?yàn)樗蚹ernel_execve本質(zhì)上都是一樣的,。我們自己寫個(gè)應(yīng)用程序,在這個(gè)應(yīng)用程序里讀取cs寄存器的值,,程序很簡(jiǎn)單:
<main.c>
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <syslog.h>
- int main()
- {
- unsigned short ucs;
- asm(
- "movw %%cs, %0\n"
- :"=r"(ucs)
- ::"memory");
- syslog(LOG_INFO, "ucs = 0x%x\n", ucs);
- return 0;
- }
復(fù)制代碼 然后把這個(gè)程序打到/sys/kernel/uevent_help上面(參照Linux設(shè)備驅(qū)動(dòng)模型中的熱插拔機(jī)制及實(shí)驗(yàn)一文),,之后我們往電腦里插個(gè)U盤,然后到/var/log/syslog文件里看輸出(在某些distribution上,,syslog的輸出可能會(huì)到/var/log/messages中):
Mar 10 14:20:23 build-server main: ucs = 0x33
0x33正好就是x86 64位系統(tǒng)(我實(shí)驗(yàn)用的環(huán)境)下的__USER_CS.
所以第一個(gè)內(nèi)核進(jìn)程(pid=1)通過執(zhí)行用戶空間程序,,期間通過cs的轉(zhuǎn)變(從__KERNEL_CS到__USER_CS)來達(dá)到特權(quán)級(jí)的更替。
|