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

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

全局函數 適應 odin def pad 預編譯 initrd 建立 進行

#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的值

技術分享圖片

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