天天看點

Linux系統下init程序的前世今生

Linux系統中的init程序(pid=1)是除了idle程序(pid=0,也就是init_task)之外另一個比較特殊的程序,它是Linux核心開始建立起程序概念時第一個通過kernel_thread産生的程序,其開始在核心态執行,然後通過一個系統調用,開始執行使用者空間的/sbin/init程式,期間Linux核心也經曆了從核心态到使用者态的特權級轉變,/sbin/init極有可能産生出了shell,然後所有的使用者程序都有該程序派生出來(目前尚未閱讀過/sbin/init的源碼)...

目前我們至少知道在核心空間執行使用者空間的一段應用程式有兩種方法:

1. call_usermodehelper

2. kernel_execve

它們最終都通過int $0x80在核心空間發起一個系統調用來完成,這個過程我在《深入Linux裝置驅動程式核心機制》第9章有過詳細的描述,對它的讨論最終結束在 sys_execve函數那裡,後者被用來執行一個新的程式。現在一個有趣的問題是,在核心空間發起的系統調用,最終通過sys_execve來執行使用者 空間的一個程式,比如/sbin/myhotplug,那麼該應用程式執行時是在核心态呢還是使用者态呢?直覺上肯定是使用者态,不過因為cpu在執行 sys_execve時cs寄存器還是__KERNEL_CS,如果前面我們的猜測是真的話,必然會有個cs寄存器的值從__KERNEL_CS到 __USER_CS的轉變過程,這個過程是如何發生的呢?下面我以kernel_execve為例,來具體讨論一下其間所發生的一些有趣的事情。

start_kernel在其最後一個函數rest_init的調用中,會通過kernel_thread來生成一個核心程序,後者則會在新程序環境下調 用kernel_init函數,kernel_init一個讓人感興趣的地方在于它會調用run_init_process來執行根檔案系統下的 /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的核心調用就是kernel_execve,後者的實作代碼是:

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;

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

system_call首先會為後續的C函數的調用在目前堆棧中建立參數傳遞的環境(x86_64的實作要相對複雜一點,它會将系統調用切換到核心棧 movq PER_CPU_VAR(kernel_stack),%rsp),尤其是接下來對C函數sys_execve調用中的struct pt_regs *regs參數,我在上面代碼中同時列出了系統調用之後的後續操作syscall_exit,從代碼中可以看到系統調用int $0x80最終通過iret指令傳回,而後者會從目前棧中彈出cs與ip,然後跳轉到cs:ip處執行代碼。正常情況下,x86架構上的int n指 令會将其下條指令的cs:ip壓入堆棧,是以當通過iret指令傳回時,原來的代碼将從int n的下條指令繼續執行,不過如果我們能在後續的C代碼中改變regs->cs與regs->ip(也就是int n執行時壓入棧中的cs與ip),那麼就可以控制下一步代碼執行的走向,而 sys_execve函數的調用鍊正好利用了這一點,接下來我們很快就會看到。SAVE_ALL宏的最後為将ds, es, fs都設定為__USER_DS,但是此時cs還是__KERNEL_CS.

核心的調用發生在call *sys_call_table(,%eax,4)這條指令上,sys_call_table是個系統調用表,本質上就是一個函數指針數組,我們這裡的系 統調用号是__NR_execve=11, 是以在sys_call_table中對應的函數為:

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其實就是sys_execve函數:

#define ptregs_execve sys_execve

而sys_execve函數的代碼實作則是:

/*

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

注意這裡的參數傳遞機制!其中的核心調用是do_execve,後者調用do_execve_common來幹執行一個新程式的活,在我們這個例子中要執 行的新程式來自/sbin/init,如果用file指令看一下會發現它其實是個ELF格式的動态連結庫,而不是那種普通的可執行檔案,是以 do_execve_common會負責打開、解析這個檔案并找到其可執行入口點,這個過程相當繁瑣,我們不妨直接看那些跟我們問題密切相關的代 碼,do_execve_common會調用search_binary_handler去查找所謂的binary formats handler,ELF顯然是最常見的一種格式:

int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)

       ...

       for (try=0; try2; 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);

               }

               ...

       }

代碼中針對ELF格式的 fmt->load_binary即為load_elf_binary, 是以fn=load_elf_binary, 後續對fn的調用即是調用load_elf_binary,這是個非常長的函數,直到其最後,我們才找到所需要的答案:

static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)

        start_thread(regs, elf_entry, bprm->p);

上述代碼中的elf_entry即為/sbin/init中的執行入口點, bprm->p為應用程式新棧(應該已經在使用者空間了),start_thread的實作為:

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位系統架構下,該值為0x33. start_thread函數最關鍵的地方在于修改了regs->cs= __USER_CS, regs->ip= new_ip,其實就是人為地改變了系統調用int $0x80指令壓入堆棧的下條指令的位址,這樣當系統調用結束通過iret指令傳回時,代碼将從這裡的__USER_CS:elf_entry處開始執 行,也就是/sbin/init中的入口點。start_thread的代碼與kernel_thread非常神似,不過它不需要象 kernel_thread那樣在最後調用do_fork來産生一個task_struct執行個體出來了,因為目前隻需要在目前程序上下文中執行代碼,而不是建立一個新程序。關于kernel_thread,我在本版曾有一篇文章分析過,當時基于的是ARM架構。

是以我們看到,start_kernel在最後調用rest_init,而後者通過對kernel_thread的調用産生一個新程序(pid=1),新程序在其kernel_init()-->init_post()調用鍊中将通過run_init_process來執行使用者空間的/sbin /init,run_init_process的核心是個系統調用,當系統調用傳回時代碼将從/sbin/init的入口點處開始執行,是以雖然我們知道 post_init中有如下幾個run_init_process的調用:

run_init_process("/sbin/init");

run_init_process("/etc/init");

run_init_process("/bin/init");

run_init_process("/bin/sh");

但是隻要比如/sbin/init被成功調用,run_init_process中的kernel_execve函數将無法傳回,因為它執行int $0x80時壓入堆棧中回家的路徑被後續的C函數調用鍊給改寫了,這樣4個run_init_process隻會有一個有機會被成功執行,如果這4個函數都失敗 了,那麼核心将會panic. 是以核心設計時必須確定用來改寫int $0x80壓入棧中的cs和ip的start_thread函數之後不會再有其他額外的代碼導緻整個調用鍊的失敗,否則代碼将執行非預期的指令,核心進入不穩定狀态。

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

Mar 10 14:20:23 build-server main: ucs = 0x33

0x33正好就是x86 64位系統(我實驗用的環境)下的__USER_CS.

是以第一個核心程序(pid=1)通過執行使用者空間程式,期間通過cs的轉變(從__KERNEL_CS到__USER_CS)來達到特權級的更替。

繼續閱讀