1. 程式人生 > >利用GDB跟蹤分析linux核心啟動

利用GDB跟蹤分析linux核心啟動

1. 實驗準備

1. 下載程式碼3.18.6到linux環境下面中,

cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar
cd linux-3.18.6
make i386_defconfig

2. 修改linux編譯選項,並編譯

  1. 執行命令:make menuconfig
    這裡寫圖片描述
  2. 彈出如下視窗:
    這裡寫圖片描述
    利用上下箭頭,選擇kernel hacking,並進入
    這裡寫圖片描述
    選擇“Compile-time checks and compiler options ”這個選項,並進入
    這裡寫圖片描述

    選擇”Compile the kernel with debug info”, 按鍵盤上的Y鍵可以選中它
    這裡寫圖片描述
    選Save儲存。 執行make命令,這一過程很長。

3.製作根檔案系統

cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static -lpthread
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
(命令都是都從孟寧老師的課件中拷貝過來的)
注:編譯的時候,需要注意這裡使用的是靜態編譯, 連線的是libpthread.a

4. 構造gdb跟蹤

1) 首先在一個shell視窗中執行命令:
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd linux-3.18.6/rootfs.img -s -S
2) 在啟動另一個視窗
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb介面中targe remote之前載入符號表
(gdb)target remote:1234 # 建立gdb和gdbserver之間的連線,按c 讓qemu上的Linux繼續執行
(gdb)break start_kernel # 斷點的設定可以在target remote之前,也可以在之後

2. 分析過程

整個linux的啟動過程都是main.c中的start_kernel函式


asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

   ... ... 
    set_task_stack_end_magic(&init_task);
     ... ... 

    /* Do the rest non-__init'ed, we're now alive */
    rest_init();
}

對於start_kernel,需要重點關注上面這兩句。
第一句

    set_task_stack_end_magic(&init_task);

用於啟動0號程序。它的主體動作都是是init_task中進行的,其核心棧通過靜態方式分配的。重點分析rest_init,觀察1號程序的啟動過程.
對於1號程序,我們可以把它為三個過程:初始化、排程過程以及執行過程

static noinline void __init_refok rest_init(void)
{
    int pid;
    ... ... 
    //初始化過程 
    kernel_thread(kernel_init, NULL, CLONE_FS);

    ... ... 
    //準備排程
    schedule_preempt_disabled();
    /* Call into cpu_idle with preempt disabled */
    cpu_startup_entry(CPUHP_ONLINE);
}

而執行是在kernel_init中進行的。 首先分析初始化的過程

1)1號程序的初始化

1號程序的初始化是在kernel_thread中進行的,它會將kernel_init的地址傳入。其具體工作在kernel/fork.c中的copy_process()中進行的。

static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
   .... 


    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;
    //將當前的程序複製, 這個current對應於include/asm/current.h中的get_current()
    //此時第一個執行緒根據當前執行緒資訊被創建出來
    p = dup_task_struct(current);

  //設定一些執行緒的許可權
    retval = copy_creds(p, clone_flags);
  //接下來設定namespace, mm, cgroup,等資訊

    if (retval)
        goto bad_fork_cleanup_namespaces;

  //把kernel_init的入口地址寫入到p中
    retval = copy_thread(clone_flags, stack_start, stack_size, p);
     ... ... 

        //這裡會給pid賦值,就是我們使用top檢視的到的pid值
        __this_cpu_inc(process_counts);

  ... .... 

}

這裡的start_stack對應的值:
這裡寫圖片描述
這裡sp的值為3245760928,換算成十六進位制即為: 0xc17661a0 ,檢視kernel_init的地址:
這裡寫圖片描述
從上面的函式中,可以看出來,0號程序會將自己複製一份作為新程序。接著將kernel_init的入口地址設定到新程序中,接著設定pid的值。而這個pid的值設定是根據巨集:

#define __this_cpu_inc(pcp)     __this_cpu_add(pcp, 1)

從這裡可以看到,linux保證了pid的不重複。
1號程序 的記憶體資訊建立完成後,linux會將儲存在一個list中,此時kernel_thread的工作就完成了。但是此時1號程序並沒有執行起來。

2) 1號程序的排程過程

函式的排程是通過函式schedule_preempt_disabled()來啟動,在kernel_init處打上斷點,然後檢視其堆疊資訊:
這裡寫圖片描述
而:

asmlinkage __visible void __sched schedule(void)
{
    struct task_struct *tsk = current;

    sched_submit_work(tsk);
    __schedule();  //2870行
}

而2870行的程式碼是__schedule(),因此我們可以斷定所有的排程工作都是通過__schedule()來進行的。最終系統會呼叫entry_32.S的組合語言來執行到程式中(怎麼調到彙編中沒有看明白。)

3) 1號程序的執行

當kernel_init被呼叫之後,就開始執行其中的程式碼

static int __ref kernel_init(void *unused)
{
   ... ... 
    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
            pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }
    ... ... 
}

只需要重點關注上面這句話即可。檢視ramdisk_execute_command的值:
這裡寫圖片描述
從這裡可以很清楚看到它會執行/init,而init就是我們之前編譯出來的程序。因此我們可以看到0號程序會呼叫execute()命令將init作為一個可執行程式執行起來。
這裡寫圖片描述

3. 總結

  1. 0號是通過直接給定記憶體地址來啟動的。因此它是一個很特殊的程序
  2. 1號程序是第一個使用者態程序,它是由0號程序來建立,建立過程是0號程序先將自己的記憶體空間複製一份,然後再將1號程序的資訊寫入
  3. 1號程序的實際啟動是通過kernel_init來執行。