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

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

#linux核心分析學習筆記 ——第七章 可執行程式工作原理

學習目標:瞭解一個可執行程式是如何作為一個程序工作的。

ELF檔案

目標檔案:是指由彙編產生的(*.o)檔案和可執行檔案。 即 可執行或可連線的檔案目標檔案是已經適應某一種CPU體系結構上的二進位制指令。

目標檔案的格式可以分為:

  • a.out
  • COFF
  • PE(windows)和ELF(linux)

ELF就是可執行和可連線的格式,是一個目標檔案的標準格式。ELF是一種物件檔案格式,用於定義不同型別的物件檔案中都有什麼內容、以什麼樣的格式存放這些內容。

ELF檔案的三種類型:

  • 可重定位檔案:屬於中間檔案,需要繼續處理。由編譯器和彙編器建立。一個原始碼會生成一個可重定位檔案。用來和其他目標檔案一起來建立一個可執行檔案、靜態庫檔案或者共享目標檔案
    • 可重定位檔案字尾為.o ,最後所有.o檔案會連結為一個檔案。
  • 可執行檔案:由多個可重定位檔案結合生成,完成了所有重定位工作和符號解析的檔案。檔案中儲存著一個用來執行的程式。
  • 共享目標檔案:共享庫,是指被可執行檔案或其他庫檔案使用的目標檔案。其後綴為.so

ELF檔案的功能:

ELF檔案參與程式的連線(建立一個程式)和程式的執行(執行一個程式),所以可以從不同的角度來看待elf格式的檔案:

  • 如果用於編譯和連結(可重定位檔案),則編譯器和連結器將把elf檔案看作是節頭表描述的節的集合,程式頭表可選。
  • 如果用於載入執行(可執行檔案),則載入器則將把elf檔案看作是程式頭表描述的段的集合,一個段可能包含多個節,節頭表可選。
  • 如果是共享檔案,則兩者都含有。

ELF格式

ELF檔案由4部分組成,分別是ELF頭(ELF header)、程式頭表(Program header table)、節(Section)和節頭表(Section header table)。

ELF Header之後可能會有一個程式頭部表(Program Header Table),如果存在的話,告訴系統如何建立程序映像。用來構造程序映像的目標檔案必須具有程式頭部表,可重定位檔案不需要這個表。
節區頭部表(Section Heade Table)包含了描述檔案節區的資訊,每個節區在表中都有一項,每一項給出諸如節區名稱、節區大小這類資訊。用於連結的目標檔案必須包含節區頭部表,其他目標檔案可以有,也可以沒有這個表。
另外,Sections是檔案節區,它包含不同的節區,且節區沒有規定的順序。

ELF Header

ELF Header結構體定義:

  #define EI_NIDENT   16
  typedef struct {
      unsigned char   e_ident[EI_NIDENT];
      Elf32_Half  e_type;
      Elf32_Half  e_machine;
      Elf32_Word  e_version;
      Elf32_Addr  e_entry;
      Elf32_Off   e_phoff;
      Elf32_Off   e_shoff;
      Elf32_Word  e_flags;
      Elf32_Half  e_ehsize;
      Elf32_Half  e_phentsize;
      Elf32_Half  e_phnum;
      Elf32_Half  e_shentsize;
      Elf32_Half  e_shnum;
      Elf32_Half  e_shstrndx;
  } Elf32_Ehdr;

其中e_ident定義:

 e_ident[] Identification Indexes
  Name       Value       Purpose
  ====       =====       =======
  EI_MAG0     0      File identification
  EI_MAG1     1      File identification
  EI_MAG2     2      File identification
  EI_MAG3     3      File identification
  EI_CLASS    4      File class
  EI_DATA     5      Data encoding
  EI_VERSION  6      File version
  EI_PAD      7      Start of padding bytes
  EI_NIDENT   16     Size of e_ident[ ]

其中結構體e_ident[EI_NIDENT]前4個位元組叫做一個魔術數(magic number),用來確定該檔案是否為ELF的目標檔案,所有ELF檔案的魔數是相同的。其中 EI_VERSIONELF頭的版本號,目前只能設定為‘1’。

對於ELF Header的部分結構體成員:

  • e_machine該成員變數指出了執行該程式需要的體系結構。
  • e_version這個成員確定object檔案的版本。
  • e_entry 程式入口虛地址。
  • e_phoff 檔案頭偏移,表明檔案頭緊接在elf head後面。
  • e_shoff 節頭表文件偏移;
  • e_flags 處理器相關的標誌
  • e_ehsize 該成員儲存著ELF頭大小(以位元組計數)。
  • e_phentsize 該成員儲存著在檔案的程式頭表(program header table)中一個入口的大小(以位元組計數)。所有的入口都是同樣的大小。
  • e_phnum 該成員儲存著在程式頭表中入口的個數。
  • e_shentsize 該成員儲存著section頭的大小(以位元組計數)。
  • e_shnum 該成員儲存著在section header table中的入口數目.
  • e_shstrndx 該成員儲存著跟section名字字元表相關入口的section頭表(section header table)索引。

其中,節頭表定義了整個ELF檔案的組成,段只是對節的重新組合,將多個節區描述為一段連續區域,對應到一段連續的記憶體地址中。

Section Header

節區頭是節區的索引,程式執行時先通過ELF Header找到Section Header,再通過這一索引找到對應的節區。

typedef struct {
    Elf32_Word  sh_name;
    Elf32_Word  sh_type;
    Elf32_Word  sh_flags;
    Elf32_Addr  sh_addr;
    Elf32_Off   sh_offset;
    Elf32_Word  sh_size;
    Elf32_Word  sh_link;
    Elf32_Word  sh_info;
    Elf32_Word  sh_addralign;
    Elf32_Word  sh_entsize;
} Elf32_Shdr;
  • sh_name 節名,是在字串中的索引
  • sh_type 節型別
  • sh_addr 該節對應的虛擬地址
  • sh_offset 該節在檔案中的位置
  • sh_size 該節的大小
  • sh_link 與該節連線的其他節
  • sh_addralign 對齊方式

Program Header

段頭表是和建立程序相關的,描述了連續的幾個節在檔案中的位置、大小以及它被放入記憶體後的位置和大小,告訴系統如何建立程序

/* Program Header */
typedef struct {
    Elf32_Word  p_type;   
    Elf32_Off   p_offset;   
    Elf32_Addr  p_vaddr;
    Elf32_Addr  p_paddr;
    Elf32_Word  p_filesz;
    Elf32_Word  p_memsz;
    Elf32_Word  p_flags;   
    Elf32_Word  p_align;
} Elf32_Phdr;
  • p_type 當前描述的段型別
  • p_offset 段在檔案中的偏移
  • p_vaddr 段在記憶體中的虛擬地址
  • p_paddr 在實體記憶體定位相關的系統中,此項為實體地址保留
  • p_filesz 段在檔案中的長度
  • p_memsz 段在記憶體中的長度
  • p_align 確定段在檔案及記憶體中如何對齊

程式編譯

程式從原始碼到可執行檔案經過以下步驟:

預處理、編譯、彙編、連結。

  • 預處理
    • gcc -E hello.c -o hello.i
    • 預處理的主要工作是
      • 刪除所有的註釋
      • 刪除所有#define,進行替換
      • 處理所有預編譯指令
      • 處理#include指令,將被包含的檔案插入預編譯指令的位置
      • 新增行號和檔名標識
    • 預處理完的檔案仍然是文字檔案,可以用任意文字編輯器檢視。
  • 編譯
    • gcc -S hello.i -o hello.s -m32
    • 編譯首先會檢查程式碼的規範性、語法錯誤等
    • 彙編結束的檔案是二進位制檔案,可以用任意編輯器檢視
  • 彙編
    • gcc -c hello.s -o hello.o -m32
    • 彙編結束後的檔案已經是ELF格式的檔案了。至少包含三個節區.text .data .bss
      • .text 程式碼段,通常用來存放程式執行程式碼的記憶體區域。
      • .data 資料段,通常用來存放程式中已經初始化的全域性變數的一塊記憶體區域,屬於靜態記憶體分配。
      • .bss 通常用來存放程式中未初始化的變數的記憶體區域,不佔用檔案空間。
  • 連結
    • gcc hello.o -o hello -m32 -static
    • 主要工作將有關的目標檔案彼此相連,使得所有目標檔案能夠成為一個能夠被作業系統裝入執行的統一整體。將各種程式碼和資料部分收集起來並組合成一個單一檔案的過程,這個檔案可以被載入或複製到記憶體中並執行。

連結與庫

  • 從過程上講,連結分為
    • 符號解析
    • 重定位
  • 連結的時機不同,可以分為
    • 靜態連結
    • 動態連結

對於連結過程,都是採用兩步連結的方法

  • 空間與地址分配

      掃描所有的輸入目標檔案,並且獲得它們的各個段的長度、屬性和位置,並且將輸入目標檔案中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全域性符號表中。

這一步中,連結器將能夠獲得所有輸入目標檔案的段長度,並且將它們合併,計算出輸出檔案中各個段合併後的長度和位置,並建立對映關係。

  • 符號解析與重定位

      使用上面第一步中收集的所有資訊,讀取輸入檔案中段的資料、重定位資訊(有一個重定位表Relocation Table),並且進行符號解析與重定位、調整程式碼中的地址(外部符號)等。

符號與符號解析

在連結中,我們將函式和變數統稱為符號,函式名或變數名就是符號名,函式或變數的地址就是符號值。

每一個目標檔案都有一個符號表,符號有以下幾種:

  • 定義在本目標檔案的全域性符號,可被其他目標檔案引用

      如:全域性變數,全域性函式
  • 在本目標檔案中引用的全域性符號,卻沒有定義在本目標檔案 -- 外部符號(External Symbol)

       如:extern變數,printf等庫函式,其他目標檔案中定義的函式
  • 段名,這種符號由編譯器產生,其值為該段的起始地址

       如:目標檔案的.text、.data等
  • 區域性符號,內部可見

符號表

符號表是用來供編譯器用於儲存有關源程式構造的各種資訊的資料結構,這些資訊在編譯器的分析階段被逐步收集並放入符號表,在綜合階段用於生成目標檔案。

符號表的功能是找未知函式在其他庫檔案中的程式碼段的具體位置。

檢視方法:objdump -t xxx.o 或 readlef -s xxx.o

  • Ndx 該符號對應區節的編號

其中,可以看到,在連結前main函式沒有地址,而在連線後,main函式分配了記憶體地址。其他屬性未改變,因為main函式本身就在hello.o檔案中。

由此可見符號表中的Ndx欄位會顯示函式表示符號在段在表中的下標,如果是未定義的函式,顯示UND;未初始化的全域性變數則顯示COMMON

重定位

重定位就是把程式的邏輯地址空間變換成記憶體中的實際實體地址空間的過程,也就是說在裝入時對目標程式中指令和資料的修改過程。它是實現多道程式在記憶體中同時執行的基礎。

上圖可以看到在0x11處有一個地址,需要被替換為puts將來的記憶體地址

通過反彙編後可以看到,call指令之後的fc ff ff ff在連結之後,就會被替換為puts在連結後的地址。

由此可見符號表記錄了目標檔案中所有全域性函式及其地址;重定位表中記錄了所有呼叫這些函式的程式碼位置

靜態連結與動態連結

靜態連結

連結器將函式的程式碼從其所在地(目標檔案或靜態連結庫中)拷貝到最終的可執行程式中。這樣該程式在被執行時這些程式碼將被裝入到該程序的虛擬地址空間中。靜態連結庫實際上是一個目標檔案的集合,其中的每個檔案含有庫中的一個或者一組相關函式的程式碼。

為建立可執行檔案,連結器必須要完成的主要任務:

符號解析:把目標檔案中符號的定義和引用聯絡起來;

重定位:把符號定義和記憶體地址對應起來,然後修改所有對符號的引用。

動態連結

在編譯時不直接複製可執行程式碼,通過記錄一系列的引數和符號,在程式執行或者載入時將這些資訊傳遞給作業系統。

作業系統將需要的動態庫載入到記憶體中,程式在執行到指定程式碼時,去共享執行記憶體中已經載入的動態庫去執行程式碼。

動態連結分為

  • 裝載時動態連結
    • 只需要在程式碼中呼叫對應的庫函式,在編譯時,將動態庫的標頭檔案路徑標明
  • 執行時動態連結
    • 執行時動態連結的本質就是程式設計師自己控制整個過程。

程式裝載

執行環境上下文

在Shell中輸入 ls -l/usr/bin實際上相當於執行了可執行程式ls,後面帶了兩個引數。

shell本身不限制引數的個數,命令列引數受限於命令自身。

shell程式的工作方式:fork出一個子程序,在子程序中呼叫execlp來載入可執行程式。

如果僅僅載入一個靜態連結可執行程式,只需要傳遞一些命令列引數和環境變數就可以正常工作。但是動態連結程式從核心態返回時,首先會執行.interp節區所指向的動態連結器。

fork和execve核心處理過程

execve執行概述

系統呼叫sys_execve()被用來執行一個可執行檔案,整體呼叫關係為:

sys_execve -> do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()
系統呼叫核心處理過程

該系統呼叫通過巨集定義在獲得可執行檔案的檔名後,直接呼叫do_execve並傳遞引數。

呼叫do_execve只是對引數進行了型別轉換,並傳遞給do_execve_commom

首先建立了一個結構體,將環境變數和命令列引數複製到結構體中,在exec_binprm是準備交給真正的可執行檔案載入器。

呼叫函式search_binary_handler(bprm)根據檔案的頭部,尋找可執行檔案的處理函式。

search_binary_handler(bprm)中呼叫了指標load_binary實際上對應的是load_elf_binary

load_elf_binary用來裝載可執行檔案,根據靜態連結和動態連結的不同,設定不同的elf_entry

  • 呼叫了start_thread函式,來建立新的程序堆疊,更重要的是修改了中斷現場中儲存的EIP暫存器。
    • 靜態連結:elf_entry指向可執行檔案的頭部,是新程式執行的起點。
    • 動態連結:elf_entry指向ld(動態聯結器)的起點load_elf_interp

最後就是start_thread在這個設定new_ip即對應的elf_entry等該程序返回使用者態時,轉而執行elf_entry指向的程式碼。

execve和fork的區別

簡單的來說,就是execve是變身,fork是分身

利用gdb跟蹤除錯過程

cd LinuxKernel
rm menu -rf
git clone http://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs

重新編譯後,使用qemu命令凍結系統執行,進行除錯

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

水平分割一個視窗,啟動gdb載入核心,連線到target 1234

 gdb
(gdb) file linux-3.18.6/vmlinux
(gdb) target remote:1234

新增斷點sys_execve和load_elf_binary和start_thread

b sys_execve
b load_elf_binary
b start_thread

停在了第一個斷點sys_execve

進入第二個斷點

進入第3個斷點,即start_thread處,繼續執行可以看到修改了eip的值