Linux系統(tǒng)中的init進(jìn)程(pid=1)是除了idle進(jìn)程(pid=0,也就是init_task)之外另一個(gè)比較特殊的進(jìn)程,它是Linux內(nèi)核開(kāi)始建立起進(jìn)程概念時(shí)第一個(gè)通過(guò)kernel_thread產(chǎn)生的進(jìn)程,其開(kāi)始在內(nèi)核態(tài)執(zhí)行,然后通過(guò)一個(gè)系統(tǒng)調(diào)用,開(kāi)始執(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)程派生出來(lái)(目前尚未閱讀過(guò)/sbin/init的源碼)...
目前我們至少知道在內(nèi)核空間執(zhí)行用戶空間的一段應(yīng)用程序有兩種方法:
1. call_usermodehelper
2. kernel_execve
它們最終都通過(guò)int $0x80在內(nèi)核空間發(fā)起一個(gè)系統(tǒng)調(diào)用來(lái)完成,這個(gè)過(guò)程我在《深入Linux設(shè)備驅(qū)動(dòng)程序內(nèi)核機(jī)制》第9章有過(guò)詳細(xì)的描述,對(duì)它的討論最終結(jié)束在 sys_execve函數(shù)那里,后者被用來(lái)執(zhí)行一個(gè)新的程序?,F(xiàn)在一個(gè)有趣的問(wèn)題是,在內(nèi)核空間發(fā)起的系統(tǒng)調(diào)用,最終通過(guò)sys_execve來(lái)執(zhí)行用戶 空間的一個(gè)程序,比如/sbin/myhotplug,那么該應(yīng)用程序執(zhí)行時(shí)是在內(nèi)核態(tài)呢還是用戶態(tài)呢?直覺(jué)上肯定是用戶態(tài),不過(guò)因?yàn)?a target="_blank">cpu在執(zhí)行 sys_execve時(shí)cs寄存器還是__KERNEL_CS,如果前面我們的猜測(cè)是真的話,必然會(huì)有個(gè)cs寄存器的值從__KERNEL_CS到 __USER_CS的轉(zhuǎn)變過(guò)程,這個(gè)過(guò)程是如何發(fā)生的呢?下面我以kernel_execve為例,來(lái)具體討論一下其間所發(fā)生的一些有趣的事情。?
start_kernel在其最后一個(gè)函數(shù)rest_init的調(diào)用中,會(huì)通過(guò)kernel_thread來(lái)生成一個(gè)內(nèi)核進(jìn)程,后者則會(huì)在新進(jìn)程環(huán)境下調(diào) 用kernel_init函數(shù),kernel_init一個(gè)讓人感興趣的地方在于它會(huì)調(diào)用run_init_process來(lái)執(zhí)行根文件系統(tǒng)下的 /sbin/init等程序:?
?
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.");
}
run_init_process的核心調(diào)用就是kernel_execve,后者的實(shí)現(xiàn)代碼是:
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;
}
里面是段內(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)移(具體過(guò)程可參考x86處理器中的特權(quán)級(jí)檢查及Linux系統(tǒng)調(diào)用的實(shí)現(xiàn)一帖). 此處用bx,cx以及dx來(lái)保存filename, argv以及envp參數(shù)是有講究的,它對(duì)應(yīng)著struct pt_regs中寄存器在棧中的布局,因?yàn)榻酉聛?lái)就會(huì)涉及從匯編到調(diào)用C函數(shù)過(guò)程,所以匯編程序在調(diào)用C之前,應(yīng)該把要傳遞給C的參數(shù)在棧中準(zhǔn)備好。
system_call是一段純匯編代碼:
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
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),尤其是接下來(lái)對(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最終通過(guò)iret指令返回,而后者會(huì)從當(dāng)前棧中彈出cs與ip,然后跳轉(zhuǎn)到cs:ip處執(zhí)行代碼。正常情況下,x86架構(gòu)上的int?n指 令會(huì)將其下條指令的cs:ip壓入堆棧,所以當(dāng)通過(guò)iret指令返回時(shí),原來(lái)的代碼將從int?n的下條指令繼續(xù)執(zhí)行,不過(guò)如果我們能在后續(xù)的C代碼中改變r(jià)egs->cs與regs->ip(也就是int?n執(zhí)行時(shí)壓入棧中的cs與ip),那么就可以控制下一步代碼執(zhí)行的走向,而 sys_execve函數(shù)的調(diào)用鏈正好利用了這一點(diǎn),接下來(lái)我們很快就會(huì)看到。SAVE_ALL宏的最后為將ds, es, fs都設(shè)置為_(kāi)_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ù)為:?
?
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
...
ptregs_execve其實(shí)就是sys_execve函數(shù):
#define ptregs_execve sys_execve
而sys_execve函數(shù)的代碼實(shí)現(xiàn)則是:
/*
*?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;
}
注意這里的參數(shù)傳遞機(jī)制!其中的核心調(diào)用是do_execve,后者調(diào)用do_execve_common來(lái)干執(zhí)行一個(gè)新程序的活,在我們這個(gè)例子中要執(zhí) 行的新程序來(lái)自/sbin/init,如果用file命令看一下會(huì)發(fā)現(xiàn)它其實(shí)是個(gè)ELF格式的動(dòng)態(tài)鏈接庫(kù),而不是那種普通的可執(zhí)行文件,所以 do_execve_common會(huì)負(fù)責(zé)打開(kāi)、解析這個(gè)文件并找到其可執(zhí)行入口點(diǎn),這個(gè)過(guò)程相當(dāng)繁瑣,我們不妨直接看那些跟我們問(wèn)題密切相關(guān)的代 碼,do_execve_common會(huì)調(diào)用search_binary_handler去查找所謂的binary formats handler,ELF顯然是最常見(jiàn)的一種格式:
?
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);
...
}
...
}
}
代碼中針對(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ù),直到其最后,我們才找到所需要的答案:
?
static?int?load_elf_binary(struct linux_binprm?*bprm,?struct pt_regs?*regs)
{
...
start_thread(regs,?elf_entry,?bprm->p);
...
}
上述代碼中的elf_entry即為/sbin/init中的執(zhí)行入口點(diǎn), bprm->p為應(yīng)用程序新棧(應(yīng)該已經(jīng)在用戶空間了),start_thread的實(shí)現(xiàn)為:
?
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);
}
在這里,我們看到了__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é)束通過(guò)iret指令返回時(shí),代碼將從這里的__USER_CS:elf_entry處開(kāi)始執(zhí) 行,也就是/sbin/init中的入口點(diǎn)。start_thread的代碼與kernel_thread非常神似,不過(guò)它不需要象 kernel_thread那樣在最后調(diào)用do_fork來(lái)產(chǎn)生一個(gè)task_struct實(shí)例出來(lái)了,因?yàn)槟壳爸恍枰诋?dāng)前進(jìn)程上下文中執(zhí)行代碼,而不是創(chuàng)建一個(gè)新進(jìn)程。關(guān)于kernel_thread,我在本版曾有一篇帖子分析過(guò),當(dāng)時(shí)基于的是ARM架構(gòu)。
所以我們看到,start_kernel在最后調(diào)用rest_init,而后者通過(guò)對(duì)kernel_thread的調(diào)用產(chǎn)生一個(gè)新進(jìn)程(pid=1),新進(jìn)程在其kernel_init()-->init_post()調(diào)用鏈中將通過(guò)run_init_process來(lái)執(zhí)行用戶空間的/sbin /init,run_init_process的核心是個(gè)系統(tǒng)調(diào)用,當(dāng)系統(tǒng)調(diào)用返回時(shí)代碼將從/sbin/init的入口點(diǎn)處開(kāi)始執(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");
但是只要比如/sbin/init被成功調(diào)用,run_init_process中的kernel_execve函數(shù)將無(wú)法返回,因?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í)必須確保用來(lái)改寫int $0x80壓入棧中的cs和ip的start_thread函數(shù)之后不會(huì)再有其他額外的代碼導(dǎo)致整個(gè)調(diào)用鏈的失敗,否則代碼將執(zhí)行非預(yù)期的指令,內(nèi)核進(jìn)入不穩(wěn)定狀態(tài)。
最后,我們來(lái)驗(yàn)證一下,所謂眼見(jiàn)為實(shí),耳聽(tīng)為虛。再者,如果驗(yàn)證達(dá)到預(yù)期,也是很鼓舞人好奇心的極佳方法。驗(yàn)證的方法我打算采用“Linux設(shè)備驅(qū)動(dòng)模型中的熱插拔機(jī)制及實(shí)驗(yàn)” 中的路線,通過(guò)call_usermodehelper來(lái)做,因?yàn)樗蚹ernel_execve本質(zhì)上都是一樣的。我們自己寫個(gè)應(yīng)用程序,在這個(gè)應(yīng)用程序里讀取cs寄存器的值,程序很簡(jiǎn)單:
?
#include?
#include?
#include?
#include?
int?main()
{
unsigned short ucs;
asm(
"movw %%cs, %0 "
:"=r"(ucs)
::"memory");
syslog(LOG_INFO,?"ucs = 0x%x ",?ucs);
return 0;
}
然后把這個(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)通過(guò)執(zhí)行用戶空間程序,期間通過(guò)cs的轉(zhuǎn)變(從__KERNEL_CS到__USER_CS)來(lái)達(dá)到特權(quán)級(jí)的更替。
?
評(píng)論
查看更多