1. 程式人生 > >2018-2019-1 20189201 《LInux核心原理與分析》第八週作業

2018-2019-1 20189201 《LInux核心原理與分析》第八週作業

只有在天足夠黑的時候你才能看到星星。

圖片名稱

BY WAY GK 加油


一、書本第七章知識總結【可執行程式工作原理】

1. ELF目標檔案格式

  • ELF全稱Executable and Linkable Format,可執行連線格式,ELF格式的檔案用於儲存Linux程式。ELF檔案(目標檔案)格式主要三種:
    1)可重定向檔案:檔案儲存著程式碼和適當的資料,用來和其他的目標檔案一起來建立一個可執行檔案或者是一個共享目標檔案。(目標
    檔案或者靜態庫檔案,即linux通常字尾為.a和.o的檔案)
    2)可執行檔案:檔案儲存著一個用來執行的程式。(例如bash,gcc等)
    3)共享目標檔案:共享庫。檔案儲存著程式碼和合適的資料,用來被下連線編輯器和動態連結器連結。(linux下字尾為.so的檔案。)

  • 如下圖:一般的 ELF 檔案包括三個索引表:
    1)ELF header:在檔案的開始,儲存了路線圖,描述了該檔案的組織情況。
    2)Program header table:告訴系統如何建立程序映像。用來構造程序映像的目標檔案必須具有程式頭部表,可重定位檔案不需要這個表。
    3)Section header table :包含了描述檔案節區的資訊,每個節區在表中都有一項,每一項給出諸如節區名稱、節區大小這類資訊。用於連結
    的目標檔案必須包含節區頭部表,其他目標檔案可以有,也可以沒有這個表。
    圖片名稱

  • 分析ELF檔案頭(ELF header)
    進入終端輸入:cd /usr/include/elf.h,檢視ELF的檔案頭包含整個檔案的控制結構:
    圖片名稱

  • 取一段簡單的程式碼進行分析:

hello.c

#include<stdio.h>
void main()
{
        printf("hello");
}

輸入指令 readelf -h hello 得到ELF檔案頭資訊:
圖片名稱
由圖可看出ELF檔案頭大小為64位元組。

2. 預處理、編譯、彙編、連結【c語言】

  • 編譯器預處理、編譯成彙編程式碼、彙編器編譯成目的碼,然後連結成可執行檔案,再將可執行程式載入到記憶體中執行,的過程可以通過下圖展示(其中預處理已省略):
    圖片名稱
cd Code
vi hello.c
gcc –E –o hello.cpp hello.c –m32             //預處理,把include的檔案包含進來及巨集替換等工作
vi hello.cpp                                 //cpp為預處理的中間檔案
gcc -x cpp-output -S -o hello.s hello.cpp -m32  //編譯成彙編程式碼
vi hello.s
gcc -x assembler -c hello.s -o hello.o -m32     //編譯成目的碼
vi hello.o                                      //得到二進位制.o檔案,ELF格式
gcc -o hello hello.o -m32                    //連結成可執行檔案hello
vi hello                                     //也是二進位制檔案,ELF格式
gcc -o hello.static hello.o -m32 -static     //靜態編譯,佔用記憶體較大
ls -l

gcc –E hello.c -o hello.i //預處理
gcc -S hello.i -o hello.s -m32//編譯
gcc -c hello.s -o hello.o -m32 //彙編
gcc hello.o -o hello -m32 //連結

3. 可執行程式、共享庫和動態載入

  • 當elf檔案載入到記憶體的時候,他把程式碼的資料載入到一塊記憶體中來,其中有很多段程式碼。載入進來之後預設從0x8048000開始載入,前面是elf頭部的一些資訊,一般頭部的大小會有不同,載入的入口點的位置可能是0x8048300,即程式的實際入口。當啟動一個剛載入過可執行檔案的程序的時候,開始執行的入口點。檔案是一個elf的靜態連線檔案,連結的時候已經連結好了。從這(0x8048300)開始執行,壓棧出棧,從main函式到結束,所有的連結在靜態連結時候已經設定好了。正常需要用到共享庫或動態連結的時候,情況會更復雜一點。
  • 裝載可執行程式之前,先了解一下可執行程式的執行環境。一般我們執行一個程式的shell環境,它本身不限制命令列引數的個數,命令列引數的個數受限於命令自身,比如 int main(int argc,char *argv[]) ,shell會呼叫execve將命令列引數和環境引數傳遞給可執行程式的main函式。命令列引數和環境串都放在使用者態的堆疊中。Shell程式->execve -> sys_execve,然後在初始化新程式堆疊時拷貝進去。

  • 動態連結有可執行裝載時的動態連結和執行時的動態連結,下面演示了兩種動態連結:

1)共享庫shilibexample.c實現SharedLibApi()函式
shilibexample.h:

#ifndef _SH_LIB_EXAMPLE_H_
#define _SH_LIB_EXAMPLE_H_

#define SUCCESS 0
#define FAILURE (-1)

#ifdef __cplusplus
extern "C" {
#endif
/*
 * Shared Lib API Example
 * input    : none
 * output   : none
 * return   : SUCCESS(0)/FAILURE(-1)
 *
 */
int SharedLibApi();


#ifdef __cplusplus
}
#endif
#endif /* _SH_LIB_EXAMPLE_H_ */

SharedLibApi()函式:

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

/*
 * Shared Lib API Example
 * input    : none
 * output   : none
 * return   : SUCCESS(0)/FAILURE(-1)
 *
 */
int SharedLibApi()
{
printf("This is a shared libary!\n");
return SUCCESS;
}

通過gcc -shared shlibexaple.c -o libshlibexample.so -m32編譯成一個共享庫檔案,
下面是同樣使用 gcc -shared dllibexample.c -o libdllibexample.so -m32 得到動態載入共享庫。

2) 共享庫dellibexample.c實現DynamicalLoadingLibApi()函式:
dellibexample.h:

#ifndef _DL_LIB_EXAMPLE_H_
#define _DL_LIB_EXAMPLE_H_



#ifdef __cplusplus
extern "C" {
#endif
/*
 * Dynamical Loading Lib API Example
 * input    : none
 * output   : none
 * return   : SUCCESS(0)/FAILURE(-1)
 *
 */
int DynamicalLoadingLibApi();


#ifdef __cplusplus
}
#endif
#endif /* _DL_LIB_EXAMPLE_H_ */

DynamicalLoadingLibApi()函式:

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

#define SUCCESS 0
#define FAILURE (-1)

/*
 * Dynamical Loading Lib API Example
 * input    : none
 * output   : none
 * return   : SUCCESS(0)/FAILURE(-1)
 *
 */
int DynamicalLoadingLibApi()
{
printf("This is a Dynamical Loading libary!\n");
return SUCCESS;
}

3)main.c()函式:

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

/*
 * Main program
 * input    : none
 * output   : none
 * return   : SUCCESS(0)/FAILURE(-1)
 *
 */
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;
}  

4)我們發現main函式中含有include "shlibexample.h" 以及include dlfcn,而沒有include dllibexample(動態載入共享庫)。當需要呼叫動態載入共享庫時,使用定義在dlfcn.h中的dlopen。
最後通過 gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
接下來編譯main()函式,注意這裡只提供shlibexample的-L(庫對應的介面標頭檔案所在目錄)和-l(庫名,如libshlibexample.so去掉lib和.so的部分),並沒有提供dllibexample的相關資訊,只是指明瞭-ldl,然後我們繼續執行:

$ export LD_LIBRARY_PATH=$PWD #將當前目錄加入預設路徑,否則main找不到依賴的庫檔案,當然也可以將庫檔案copy到預設路徑下。
$ ./main 

二、實驗部分【使用gdb跟蹤sys_execve核心函式的處理過程】

1)首先還是將menu目錄刪除,用git命令複製一個新的menu目錄,用test_exec.c將test.c覆蓋,然後重新編譯rootfs
圖片名稱

2)開啟test.c檔案,可以發現增加了一句,MenuConfig("exec","Execute a program",Exec)
圖片名稱
*********************************************
圖片名稱
3)看一下這段程式碼,和fork()函式類似,增加了一個fork,子程序增加了一個execlp("/hello","hello",NULL); 啟動hello,看一下hello.c
圖片名稱
4)看一下Makefile檔案,靜態的方式編譯了hello.c,並在生成根檔案系統時把init 和hello都放在rootfs裡面

圖片名稱

5)輸入命令 make rootfs ,在qemu視窗中輸入help 執行一下exec

圖片名稱

發現在MenuOS中使用help命令可以看到增加了exec命令,執行exec指令發現比fork指令增加了一行輸出“helloworld!”,實際上是新載入了一個可執行程式來輸出了一行語句。

6)先cd .. 返回到上一級,qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S啟動,水平分割,gdb。然後把符號表檔案載入進來,gdb伺服器使用預設埠號1234來連線,並通過gdb跟蹤除錯,設定斷點,可以先停在sys_execve然後再設定其他斷點。
圖片名稱
*********************************************
圖片名稱

7)除錯執行

  • 按c執行
    圖片名稱

  • 連續c執行三次,在menuos視窗輸入exec,發現執行到 This is child process! 停下
    圖片名稱
    *********************************************
    圖片名稱

  • 進入sys_execve系統呼叫,list列出來,跟蹤,接下來通過s進入sys_execve內部
    圖片名稱
    *********************************************
    圖片名稱

  • 按c繼續執行到load_elf_binary,list檢視;再按c執行,執行到start_thread,想知道new_ip到底指向哪裡,new_ip是返回到使用者態的第一條指令的地址。再水平分割一個控制檯出來,使用命令readelf -h hello,可以看到入口點地址和上面po new_ip所顯示的地址一樣:
    圖片名稱
  • 然後我們繼續執行s步驟,可以看到在進行修改核心堆疊的位置,發現原來壓棧的ip和sp都被改成了新的ip(程式hello的入口點地址)和新的sp,這樣在返回到使用者態的時候程式就有一個新的可執行上下文環境。最後按一下c,exec的執行結束:
    圖片名稱

三、實驗收穫

1. 關於execve

  • 當可執行程式在執行到execve的時候陷入到核心態,當前程序的可執行程式被execve的載入的可執行檔案覆蓋,當execve的系統呼叫返回時,返回的不是原來的可執行程式,而是新的可執行程式的起點(main函式)。shell環境會執行execve,把命令列引數和環境變數都載入進來,當系統呼叫陷入到核心裡面的時候,system call呼叫sys_execve。sys_execve中呼叫了do_execve。

2. fork和execve的區別

  • man exec就可以知到:
    The exec() family of functions replaces the current process image with a new process image
    exec是沒有建立新程序的,而是把當前程序對應的應用換成新的應用。因此,它裡頭當前不會去fork了。
    舉個例,如果PID=1000的程序A, 執行ecec B, 那就PID=1000的程序就會變為B,A的資源會被系統回收。對Exec函式來說,沒所謂父子程序,只有當前程序,當前執行exec函式的程序。新程序 是在使用者呼叫fork時生成的。
  • fork和exec不一樣,它的作用是複製一個程序,但兩個程序都執行相同的程式。task_struct也是fork的時間新建的。一般這兩函式是聯用的,先fork,再在子程序裡exec。在核心態並沒有相互呼叫關係。

3. gdb除錯若干問題

點選