1. 程式人生 > >2017-2018-1 20179202《Linux內核原理與分析》第八周作業

2017-2018-1 20179202《Linux內核原理與分析》第八周作業

預測 rar 合並 數據 代碼分析 一個 設置 堆棧 linu

一 、可執行程序的裝載

1. 預處理、編譯、鏈接

gcc –e –o hello.cpp hello.c   //預處理
gcc -x cpp-output -S -o hello.s hello.cpp //編譯   
gcc -x assembler -c hello.s -o hello.o-m32  //匯編
gcc -o hello hello.o   //鏈接成可執行文件,使用共享庫

技術分享圖片

gcc -o hello.static hello.o -static靜態編譯出來的hello.static把C庫裏需要的東西也放到可執行文件裏了。用命令ls –l,可以看到hello只有7K,hello.static有大概700K。

2. ELF文件

ELF(Excutable and Linking Format)是一個文件格式的標準。通過readelf-h hello查看可執行文件hello的頭部(-a查看全部信息,-h只查看頭部信息),頭部裏面註明了目標文件類型ELF32。Entry point address是程序入口,地址為0x8048310,
即可執行文件加載到內存中開始執行的第一行代碼地址。頭部後還有一些代碼數據等等。可執行文件的格式和進程的地址空間有一個映射的關系,當程序要加載到內存中運行時,將ELF文件的代碼段和數據段加載到進程的地址空間。

技術分享圖片

ELF文件裏面三種目標文件:可重定位(relocatable)文件保存著代碼和適當的數據,用來和其它的object文件一起來創建一個可執行文件或者是一個共享文件(主要是.o文件);可執行(executable)文件保存著一個用來執行的程序,該文件指出了exec(BA_OS)如何來創建程序進程映象(操作系統怎麽樣把可執行文件加載起來並且從哪裏開始執行);共享object文件保存著代碼和合適的數據,用來被兩個鏈接器鏈接。第一個是鏈接編輯器(靜態鏈接),可以和其它的可重定位和共享object文件來創建其它的object。第二個是動態鏈接器,聯合一個可執行文件和其它的共享object文件來創建一個進程映象。

3. 動態鏈接

動態鏈接有可執行裝載時的動態鏈接(大多數)和運行時的動態鏈兩種方式。

(1)共享庫

shlibexample.h中定義了SharedLibApi()函數,shlibexample.c是對此函數的實現。用`gcc -shared shlibexample.c -o libshlibexample.so -m32(在64位環境下執行時加上-32)生成.so文件。這樣就生成了共享庫文件。

#include <stdio.h>
#include "shlibexample.h"

int SharedLibApi()
{
    printf("This is a shared libary!\n");
    return SUCCESS;
}

(2)動態加載共享庫

dllibexample.h定義了DynamicalLoadingLibApi()函數,dllibexample.c是對此函數的實現。同樣使用gcc -shared dllibexample.c -o libdllibexample.so得到動態加載共享庫。

#include <stdio.h>
#include "dllibexample.h"

#define SUCCESS 0
#define FAILURE (-1)

int DynamicalLoadingLibApi()
{
    printf("This is a Dynamical Loading libary!\n");
    return SUCCESS;
}

(3)main函數使用兩種動態鏈接庫。

#include <stdio.h>
#include "shlibexample.h" 
#include <dlfcn.h>

int main()
{
    printf("This is a Main program!\n");
    /* Use Shared Lib */
    printf("Calling SharedLibApi() function of libshlibexample.so!\n");
    SharedLibApi(); //直接調用共享庫
    /* Use Dynamical Loading Lib */
    void * handle = dlopen("libdllibexample.so",RTLD_NOW);//打開動態庫並將其加載到內存
    if(handle == NULL)
    {
        printf("Open Lib libdllibexample.so Error:%s\n",dlerror());
        return   FAILURE;
    }
    int (*func)(void);
    char * error;
    func = dlsym(handle,"DynamicalLoadingLibApi");
    if((error = dlerror()) != NULL)
    {
        printf("DynamicalLoadingLibApi not found:%s\n",error);
        return   FAILURE;
    }    
    printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");
    func();  
    dlclose(handle); //卸載庫  
    return SUCCESS;
}

可以看到main函數中只include了shlibexample(共享庫),沒有include dllibexample(動態加載共享庫),但是include了dlfcn。因為前面加了共享庫的接口文件,所以可以直接調用共享庫。但是如果要調用動態加載共享庫,就要使用定義在dlfcn.h中的dlopen。

gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32生成可執行文件。註意,這裏只提供shlibexample的-L,並沒有提供dllibexample的相關信息,只是指明了-ldl。-dl動態加載,編譯main.c的時候,沒有指明任何相關信息,只是在程序內部指明了。實驗截圖如下:

技術分享圖片

3. 代碼分析

當前的可執行程序在執行,執行到execve的時候陷入到內核態,用execve的加載的可執行文件把當前進程的可執行程序給覆蓋掉,當execve的系統調用返回的時候,已經返回的不是原來的那個可執行程序了,是新的可執行程序的起點(main函數)。shell環境會執行execve,把命令行參數和環境變量都加載進來,當系統調用陷入到內核裏面的時候,system call調用sys_execve。sys_execve中調用了do_execve。

//sys_execve
SYSCALL_DEFINE3(execve,
                const char __user *, filename,                //可執行程序的名稱
                const char __user *const __user *, argv,      //程序的參數
                const char __user *const __user *, envp)      //環境變量
{
    return do_execve(getname(filename), argv, envp);
}

//do_execve
int do_execve(struct filename *filename,
    const char __user *const __user *__argv,
    const char __user *const __user *__envp)
{
    struct user_arg_ptr argv = { .ptr.native = __argv };
    struct user_arg_ptr envp = { .ptr.native = __envp };
    return do_execve_common(filename, argv, envp);
}

很明顯,繼續分析其中調用的do_execve_common:

static int do_execve_common(struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp)
{
    struct linux_binprm *bprm; 
    struct file *file;             
    struct files_struct *displaced;
    int retval;
    ...
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);//在堆上分配一個linux_binprm結構體
    ...
    file = do_open_exec(filename);//打開需要加載的可執行文件,file中就包含了打開的可執行文件的信息
    ...
    bprm->file = file;                             //賦值file指針
    bprm->filename = bprm->interp = filename->name;//賦值文件名

    retval = bprm_mm_init(bprm);                   //創建進程的內存地址空間
    ...
    bprm->argc = count(argv, MAX_ARG_STRINGS);//賦值參數個數
    ...
    bprm->envc = count(envp, MAX_ARG_STRINGS);//賦值環境變量個數
    ...
    retval = copy_strings_kernel(1, &bprm->filename, bprm); //從內核空間獲取文件路徑;
    ...
    bprm->exec = bprm->p;                         //p為當前內存頁最高地址
    retval = copy_strings(bprm->envc, envp, bprm);//把環境變量拷貝到bprm中
    ...
    retval = copy_strings(bprm->argc, argv, bprm);//把命令行參數拷貝到bprm中
    ...
    retval = exec_binprm(bprm);//處理可執行文件
    ...
    return retval;
}

linux_binprm結構體用來保存要執行文件的相關信息, 如文件的頭128字節、文件名、命令行參數、環境變量、文件路徑、內存描述符信息等。exec_binprm函數保存當前的pid,其中ret = search_binary_handler(bprm); 調用 search_binary_handler 尋找可執行文件的相應處理函數。

int search_binary_handler(struct linux_binprm *bprm)
 {
    bool need_retry = IS_ENABLED(CONFIG_MODULES);
    struct linux_binfmt *fmt;
    int retval;
    ...
    read_lock(&binfmt_lock);
    list_for_each_entry(fmt, &formats, lh) {            //遍歷文件解析鏈表
           if (!try_module_get(fmt->module))
                   continue;
           read_unlock(&binfmt_lock);
           bprm->recursion_depth++;
                //解析elf格式執行的位置
           retval = fmt->load_binary(bprm);// 加載可執行文件的處理函數
           read_lock(&binfmt_lock);
           ...
        }
    return retval;

linux_binfmt結構體定義了一些函數指針,不同的Linux可接受的目標文件格式(如load_binary,load_shlib,core_dump)采用不同的函數來進行目標文件的裝載。每一個linux_binfmt結構體對應一種二進制程序處理方法。這些結構體實例會通過init_elf_binfmt以註冊的方式加入到內核對應的format鏈表中去,通過register_binfmt()unregister_binfmt()在鏈表中插入和刪除對象。

struct linux_binfmt {
    struct list_head lh;
    struct module *module;
    int (*load_binary)(struct linux_binprm *);//用於加載一個新的進程(通過讀取可執行文件中的信息)
    int (*load_shlib)(struct file *);         //用於動態加載共享庫
    int (*core_dump)(struct coredump_params *cprm);//在core文件中保存當前進程的上下文
    unsigned long min_coredump;     
 };

目標文件的格式是ELF,所以retval = fmt->load_binary(bprm);中load_binary實際上調用load_elf_binary完成ELF二進制映像的認領、裝入和啟動。load_elf_binary這個函數指針被包含在一個名為elf_format的結構體中:

static structlinux_binfmt elf_format = {  
        .module             =THIS_MODULE,  
        .load_binary     = load_elf_binary, //函數指針  
        .load_shlib        = load_elf_library,  
        .core_dump     = elf_core_dump,  
        .min_coredump        = ELF_EXEC_PAGESIZE,  
}; 

全局變量elf_format賦給了一個指針,在init_elf_binfmt裏把變量註冊註冊到文件解析鏈表中,就可以在鏈表裏找到相應的文件格式。繼續分析load_elf_binary:

 static int load_elf_binary(struct linux_binprm *bprm)
{
    ...
    if (elf_interpreter) {                        // 動態鏈接的處理  
         ... 
         } else {                                 // 靜態鏈接的處理  
                  elf_entry =loc->elf_ex.e_entry; 
                  ...
                  }  
         }  
    ...
    //將ELF文件映射到進程空間中,execve系統調用返回用戶態後進程就擁有了新的代碼段、數據段。
    current->mm->end_code = end_code;  
    current->mm->start_code =start_code;  
    current->mm->start_data =start_data;  
    current->mm->end_data = end_data;  
    current->mm->start_stack =bprm->p;  
    ...
    start_thread(regs, elf_entry, bprm->p);
}

ELF文件中的Entry point address字段指明了程序入口地址,這個地址一般是0x8048000(0x8048000以上的是內核段內存)。該入口地址被解析後存放在elf_ex.e_entry中,所以靜態鏈接程序的起始位置就是elf_entry。這個函數中還有一個關鍵點start_thread:

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;          
    regs->flags     = X86_EFLAGS_IF;

    set_thread_flag(TIF_NOTIFY_RESUME);
}

regs中為系統調用時SAVE_ALL宏壓入內核棧的部分。new_ip的值等於參數elf_entry的值,即把ELF文件中定義的main函數起始地址賦值給eip寄存器,進程返回到用戶態時的執行位置從原來的int 0x80的下一條指令變成了new_ip的位置。

總結一下,調用順序是sys_execve -> do_execve -> do_execve_common -> exec_binprm,當系統調用從內核態返回到用戶態時,eip直接跳轉到ELF程序的入口地址,CPU也得到新的用戶態堆棧(包含新程序的命令行參數和shell上下文環境)。這樣,新程序就開始執行了。

4.靜態鏈接可執行文件的調試

用test_exe.c覆蓋test.c,增加了一句MenuConfig()執行一個程序。

int Exec(int argc, char *argv[])
{
        int pid;
        /* fork another process */
        pid = fork();
        if (pid < 0)
        {
                /* error occurred */
                fprintf(stderr,"Fork Failed!");
                exit(-1);
        }
        else if (pid == 0)
        {
                /*       child process  */
        printf("This is Child Process!\n");
                execlp("/hello","hello",NULL);
        }
        else
        {
                /*      parent process   */
        printf("This is Parent Process!\n");
                /* parent will wait for the child to complete*/
                wait(NULL);
                printf("Child Complete!\n");
        }
}

makefile做了一些修改,編譯了hello.c,在生成根文件系統的時候,把init和hello都放到rootfs.img內。這樣在執行execve時就自動的加載hello可執行文件:

技術分享圖片

在前面分析的關鍵點設置斷點,一邊一句一句向下跟蹤,一邊對照執行過程。追蹤到start_thread,用po new_ip,得到的是0x804887f。

技術分享圖片

readelf –h hello可以看到這個可執行程序它的入口點地址也是0x804887f。

技術分享圖片

5.遇到的問題及解決

(1)看了視頻後對動態鏈接的第二種方式依然理解模糊,通過搜索資料解決。

??如果要調用動態加載共享庫,就要使用定義在dlfcn.h中的dlopen。給出文件名libdllibexample.so和標誌RTLD_NOW打開動態鏈接庫,返回handle句柄。dlsym函數與上面的dlopen函數配合使用,根據操作句柄(由dlopen打開動態鏈接後返回的指針)handle與符號(要求獲取的函數或全局變量的名稱)DynamicLoadingLibApi,返回符號對應的地址。使用此地址可以獲得庫中特定函數的地址,並且調用庫中的相應函數。這樣就可以使用動態加載共享庫裏面所定義的函數了。

(2)不理解調試中的po

po是print_object的縮寫,不僅僅可以輸出顯示定義的對象,也可以輸出表達式的結果。我嘗試了p、po、p/d、p\x,對比它們的執行結果:

技術分享圖片

可以發現p、p/d(10進制)、p\x(16進制)輸出值前都會有一個類似"$1="的前綴,它們是變量,在後面的表達式中可以使用,而po並不能把它的返回值存儲到變量裏。至於po還能在哪些地方看的不太清,以後遇到了再具體分析。

(3)在第六周實驗中,Rename函數實現把"hello.c"重命名為"newhello.c",在當前文件夾中放一個hello.c文件即可實現。但在MenuOS上,把hello.c文件嘗試放在menu文件夾下,執行rename命令顯示不成功:

所以我一直不知道該把hello.c文件放在哪裏才可以重命名成功。這周孟老師修改Makefile文件提醒了我,我修改了Makefile,把hello.c打包到鏡像文件中:

技術分享圖片

技術分享圖片

雖然顯示執行成功,不幸的是hello.c並沒有重命名為newhello.c:

技術分享圖片

我想,修改的應該是rootfs.img中的hello.c,所以這裏的hello.c才沒被修改(不知道思考的對不對,想打開rootfs.img,試了幾種方法都沒有解決)。

二 、課本筆記

虛擬文件系統

1.虛擬文件系統(VFS)是linux內核和存儲設備之間的抽象層。VFS中有四個主要的對象類型,分別是超級塊對象、索引節點對象、目錄項對象、文件對象。

2.超級塊主要存儲特定文件系統相關的信息,存儲在磁盤上,在使用時創建在內存中的。對於磁盤文件系統來說,這個對象通常對應磁盤上的一個文件系統控制塊(磁盤super block)。

3.索引節點包含內核在操作文件或目錄時需要的全部信息。一個索引節點代表文件系統中的一個文件(這裏的文件不僅是指我們平時所認為的普通的文件,還包括目錄,特殊設備文件等等)。索引節點存儲在磁盤上,當被應用程序訪問到時才會在內存中創建。

4.通過索引節點已經可以定位到指定的文件,但索引節點對象的屬性非常多,在查找,比較文件時,直接用索引節點效率不高,所以引入了目錄項(dentry)的概念。目錄項並不實際存在於磁盤上,在使用的時候在內存中創建目錄項對象。

5.在一個文件路徑中,路徑中的每一部分都被稱為目錄項。每個目錄項對象都有被使用,未使用和負狀態3種狀態。一個被使用的目錄項對應一個有效的索引節點,並且該對象由一個或多個使用者;一個未被使用的目錄項對應一個有效的索引節點,但是VFS當前並沒有使用這個目錄項;一個負狀態的目錄項沒有對應的有效索引節點。

6.在Linux中,除了普通文件,其他諸如目錄、設備、套接字等也以文件被對待即“一切皆文件”。文件對象表示進程已打開的文件,從用戶角度來看,我們在代碼中操作的就是一個文件對象。雖然一個文件對應的文件對象不是唯一的,但其對應的索引節點和目錄項對象卻是唯一的。

7.VFS中還有2個專門針對文件系統的2個對象,struct file_system_type用來描述各種特定文件系統類型(比如ext3,ext4或UDF),struct vfsmount 用來描述一個安裝文件系統的實例。被Linux支持的文件系統,都有且僅有一 個file_system_type結構而不管它有零個或多個實例被安裝到系統中。當文件系統被實際安裝時,會在安裝點創建一個vfsmount結構體。

8.以下3個結構體和進程緊密聯系在一起:

  • struct files_struct:由進程描述符中的 files 目錄項指向,所有與單個進程相關的信息(比如打開的文件和文件描述符)都包含在其中。
  • struct fs_struct:由進程描述符中的 fs 域指向,包含文件系統和進程相關的信息。
  • struct mmt_namespace:由進程描述符中的 mmt_namespace 域指向。

塊I/O層

1.I/O設備主要有字符設備和塊設備,相比字符設備的只能順序讀寫設備中的內容,塊設備能夠隨機讀寫設備中的內容。字符設備只能順序訪問,塊設備隨機訪問。

2.塊設備最小的可尋址單元是扇區。扇區的大小一般是2的整數倍,最常見的大小是512個字節。扇區是所有塊設備的基本單元,塊設備無法對比它還小的單元進行尋址和操作。雖然物理磁盤尋址是按照扇區級進行的,但是內核執行的所有磁盤操作都是按照塊進行的。為了便於文件系統管理,塊的大小一般是扇區的整數倍,並且小於等於頁的大小。

3.當一個塊被調入內存時,它要存儲在一個緩沖區中。每一個緩沖區與一個塊對應,它相當於是磁盤塊在內存中的表示。每個緩沖區都有一個對應的描述符,用buffer_head結構體表示,稱作緩沖區頭,包含了內和操作緩沖區所需要的全部信息。

4.bio結構體表示了一次I/O操作所涉及到的所有內存頁。通過用片段來描述緩沖區,即使一個緩沖區分散在內存的多個位置上,bio結構體也能對內核保證I/O操作的執行。

5.bio中對應的是內存中的一個個頁,而緩沖區頭對應的是磁盤中的一個塊。

6.塊設備將它們掛起的塊I/O請求保存在請求隊列中,該隊列由request_queue結構體表示。請求隊列表中的每一項都是一個單獨的請求,由reques結構體表示。因為一個請求可能要操作多個連續的磁盤塊,所有每個請求可有由多個bio結構體組成。

7.雖然磁盤上的塊必須連續,但是在內存中的這些塊並不一定要連續。

8.I/O調度程序的工作是管理塊設備的請求隊列。通過合並與排序減少磁盤尋址時間。

9.為了保證磁盤尋址的效率,一般會盡量讓磁頭向一個方向移動,等到頭了再反過來移動,這樣可以縮短所有請求的磁盤尋址總時間,I/O調度程序稱作電梯調度。

10.2.6內核中內置了4種I/O調度: 預測(as)、完全公正排隊(cfq)、最終期限(deadline)、空操作(noop)。通過命令行選項 elevator=xxx 來啟用其中的任何一種。

2017-2018-1 20179202《Linux內核原理與分析》第八周作業