linux init进程原理,Linux 系统下 init 进程的前世今生
原標(biāo)題:Linux 系統(tǒng)下 init 進(jìn)程的前世今生
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è)通過(guò) kernel_thread 產(chǎn)生的進(jìn)程,其開始在內(nèi)核態(tài)執(zhí)行,然后通過(guò)一個(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)程派生出來(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è)新的程序。現(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)?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 代碼中改變 regs->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è)置為__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
#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é)打開、解析這個(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 處開始執(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)處開始執(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í),耳聽為虛。再者,如果驗(yàn)證達(dá)到預(yù)期,也是很鼓舞人好奇心的極佳方法。驗(yàn)證的方法我打算采用 “Linux 設(shè)備驅(qū)動(dòng)模型中的熱插拔機(jī)制及實(shí)驗(yàn)” 中的路線,通過(guò) call_usermodehelper 來(lái)做,因?yàn)樗?kernel_execve 本質(zhì)上都是一樣的。我們自己寫個(gè)應(yīng)用程序,在這個(gè)應(yīng)用程序里讀取 cs 寄存器的值,程序很簡(jiǎn)單:
#include
#include
#include
#include
int main()
{
unsigned short ucs;
asm(
"movw %%cs, %0n"
:"=r"(ucs)
::"memory");
syslog(LOG_INFO, "ucs = 0x%xn", 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í)的更替。
文章來(lái)源:CU技術(shù)社區(qū)
《深入JVM內(nèi)核—原理、診斷與優(yōu)化》由葛一鳴老師親授!本課程為Java進(jìn)階課程,通過(guò)學(xué)習(xí)熟悉JVM的工作機(jī)制,了解Java虛擬機(jī)的工作原理,知道如何處理Java程序開發(fā)與運(yùn)行中出現(xiàn)各種問(wèn)題,故障診斷、以及調(diào)優(yōu)!返回搜狐,查看更多
責(zé)任編輯:
總結(jié)
以上是生活随笔為你收集整理的linux init进程原理,Linux 系统下 init 进程的前世今生的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 拦截器中addInterceptor和e
- 下一篇: 光盘隐藏文件夹 linux,linux常