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è)通過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)級轉(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ì)的描述,對它的討論最終結(jié)束在 sys_execve函數(shù)那里口蝠,后者被用來執(zhí)行一個(gè)新的程序∑髦樱現(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,如果前面我們的猜測是真的話寸五,必然會(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等程序:

1

2

3

4

5

6

7

8

9

10staticnoinlineintinit_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)代碼是:

1

2

3

4

5

6

7

8

9

10intkernel_execve(constchar*filename,

constchar*constargv[],

constchar*constenvp[])

{

long__res;

asm volatile("int $0x80"

:"=a"(__res)

:"0"(__NR_execve),"b"(filename),"c"(argv),"d"(envp):"memory");

return__res;

}

里面是段內(nèi)嵌的匯編代碼劲适,代碼相對比較簡單楷掉,核心代碼是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)級檢查及Linux系統(tǒng)調(diào)用的實(shí)現(xiàn)一帖). 此處用bx,cx以及dx來保存filename, argv以及envp參數(shù)是有講究的刊橘,它對應(yīng)著struct pt_regs中寄存器在棧中的布局鄙才,因?yàn)榻酉聛砭蜁?huì)涉及從匯編到調(diào)用C函數(shù)過程颂鸿,所以匯編程序在調(diào)用C之前,應(yīng)該把要傳遞給C的參數(shù)在棧中準(zhǔn)備好攒庵。

system_call是一段純匯編代碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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_REGS4# 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)要相對復(fù)雜一點(diǎn)嘴纺,它會(huì)將系統(tǒng)調(diào)用切換到內(nèi)核棧 movq PER_CPU_VAR(kernel_stack),%rsp),尤其是接下來對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中對應(yīng)的函數(shù)為:

1

2

3

4

5

6

7

8

9

10

11

12ENTRY(sys_call_table)

.longsys_restart_syscall/* 0 - old "setup()" system call, used for restarting */

.longsys_exit

.longptregs_fork

.longsys_read

.longsys_write

.longsys_open/* 5 */

.longsys_close

...

.longsys_unlink/* 10 */

.longptregs_execve//__NR_execve

...

ptregs_execve其實(shí)就是sys_execve函數(shù):

1

#define ptregs_execve sys_execve

而sys_execve函數(shù)的代碼實(shí)現(xiàn)則是:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26/*

* sys_execve() executes a new program.

*/

longsys_execve(constchar__user *name,

constchar__user *const__user *argv,

constchar__user *const__user *envp,structpt_regs *regs)

{

longerror;

char*filename;

filename=getname(name);

error=PTR_ERR(filename);

if(IS_ERR(filename))

returnerror;

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);

returnerror;

}

注意這里的參數(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顯然是最常見的一種格式:

1

2

3

4

5

6

7

8

9

10

11

12

13

14intsearch_binary_handler(structlinux_binprm *bprm,structpt_regs *regs)

{

...

for(try=0;try<2;try++){

read_lock(&binfmt_lock);

list_for_each_entry(fmt,&formats,lh){

int(*fn)(structlinux_binprm *,structpt_regs *)=fmt->load_binary;

...

retval=fn(bprm,regs);

...

}

...

}

}

代碼中針對ELF格式的 fmt->load_binary即為load_elf_binary, 所以fn=load_elf_binary, 后續(xù)對fn的調(diào)用即是調(diào)用load_elf_binary,這是個(gè)非常長的函數(shù)诲祸,直到其最后浊吏,我們才找到所需要的答案:

1

2

3

4

5

6staticintload_elf_binary(structlinux_binprm *bprm,structpt_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)為:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16void

start_thread(structpt_regs *regs,unsignedlongnew_ip,unsignedlongnew_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é)束通過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哲戚,而后者通過對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)用:

1

2

3

4run_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ù)將無法返回祈纯,因?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寄存器的值话浇,程序很簡單:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15#include

#include

#include

#include

intmain()

{

unsignedshortucs;

asm(

"movw %%cs, %0\n"

:"=r"(ucs)

::"memory");

syslog(LOG_INFO,"ucs = 0x%x\n",ucs);

return0;

}

然后把這個(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)級的更替。

官方微博:http://weibo.com/codingke

官方QQ群:148715490

官方QQ:2337862882

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末岖瑰,一起剝皮案震驚了整個(gè)濱河市叛买,隨后出現(xiàn)的幾起案子砂代,更是在濱河造成了極大的恐慌蹋订,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刻伊,死亡現(xiàn)場離奇詭異露戒,居然都是意外死亡椒功,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門智什,熙熙樓的掌柜王于貴愁眉苦臉地迎上來动漾,“玉大人,你說我怎么就攤上這事荠锭『得校” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵证九,是天一觀的道長删豺。 經(jīng)常有香客問我,道長愧怜,這世上最難降的妖魔是什么呀页? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮拥坛,結(jié)果婚禮上蓬蝶,老公的妹妹穿的比我還像新娘。我一直安慰自己猜惋,他們只是感情好丸氛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著著摔,像睡著了一般雪位。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上梨撞,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天雹洗,我揣著相機(jī)與錄音,去河邊找鬼卧波。 笑死时肿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的港粱。 我是一名探鬼主播螃成,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼查坪!你這毒婦竟也來了寸宏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤偿曙,失蹤者是張志新(化名)和其女友劉穎氮凝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體望忆,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡罩阵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年竿秆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稿壁。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡幽钢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出傅是,到底是詐尸還是另有隱情匪燕,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布喧笔,位于F島的核電站谎懦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏溃斋。R本人自食惡果不足惜界拦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梗劫。 院中可真熱鬧享甸,春花似錦、人聲如沸梳侨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽走哺。三九已至蚯嫌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丙躏,已是汗流浹背择示。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晒旅,地道東北人栅盲。 一個(gè)月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像废恋,于是被迫代替她去往敵國和親谈秫。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容