This page looks best with JavaScript enabled

elf

 ·  ☕ 14 min read
接下來以linux v5.10 64bit Elf64作範例

簡介

elf_arch

Executable and Linking Format (ELF)這個格式包含了執行與連結用的檔案,可以搭配文件交叉看

ELF header

利用指令查看

1
readelf -h 你的檔名

原始碼elf.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct elf64_hdr {
	unsigned char e_ident[16];	/* ELF "magic number" */
	Elf64_Half e_type;
	Elf64_Half e_machine;
	Elf64_Word e_version;
	Elf64_Addr e_entry;	/* Entry point virtual address */
	Elf64_Off e_phoff;	/* Program header table file offset */
	Elf64_Off e_shoff;	/* Section header table file offset */
	Elf64_Word e_flags;
	Elf64_Half e_ehsize;
	Elf64_Half e_phentsize;
	Elf64_Half e_phnum;
	Elf64_Half e_shentsize;
	Elf64_Half e_shnum;
	Elf64_Half e_shstrndx;
} Elf64_Ehdr;

e_ident

Namee_ident第幾個bytesPurpose
EI_MAG000x7f
EI_MAG11‘E’
EI_MAG22‘L’
EI_MAG33‘F’
EI_CLASS4ELFCLASSNONE=0
ELFCLASS32=1
ELFCLASS64=2
EI_DATA5ELFDATANONE=0
ELFDATA2LSB=1 又稱little endian
ELFDATA2MSB=2 又稱big endian
EI_VERSION6原始的ELF specification規範這裡固定是1,除非你要設計自己客製擴展ELF的規範不然e_versionEI_VERSION都是固定1
EI_OSABI7ELFOSABI_NONE/ELFOSABI_SYSV 0 UNIX System V ABI
ELFOSABI_HPUX 1 Hewlett-Packard HP-UX
ELFOSABI_NETBSD 2 NetBSD
ELFOSABI_LINUX 3 Linux
ELFOSABI_SOLARIS 6 Sun Solaris
ELFOSABI_AIX 7 AIX
ELFOSABI_IRIX 8 IRIX
ELFOSABI_FREEBSD 9 FreeBSD
ELFOSABI_TRU64 10 Compaq TRU64 UNIX
ELFOSABI_MODESTO 11 Novell Modesto
ELFOSABI_OPENBSD 12 Open BSD
ELFOSABI_OPENVMS 13 Open VMS
ELFOSABI_NSK 14 Hewlett-Packard Non-Stop Kernel
EI_ABIVERSION8ABI version
EI_PAD9Start of padding bytes 從這裡開始就補0直到最後 關於padding請看(你所不知道的 C 語言:記憶體管理、對齊及硬體特性)[https://hackmd.io/@sysprog/c-memory#data-alignment]
EI_NIDENT16Size of e_ident[]
嘗試利用vscode的Hex Editor修改執行檔並且利用readelf觀察

e_type

這邊描述elf檔案是什麼型態包含了ET_REL,ET_EXEC,ET_DYN,ET_CORE,接下來簡單

e_type數值(unsigned short)用途
ET_NONE0No file type 未知格式(我在linux核心找不到有啥特別用途)
ET_REL1Relocatable file 靜態連結 .o(object file) .a(靜態連結庫) .ko(kernel module)
ET_EXEC2Executable file 可以執行的檔案
ET_DYN3Shared object file 動態連結檔案.so,或是position-independent executable
ET_CORE4Core file 當發生core dumped如果有設定linux會輸出這個type的elf檔案讓你追蹤為什麼程式崩潰了
ET_LOPROC0xff00Processor-specific ET_LOPROC~ET_HIPROC範圍的數值
ET_HIPROC0xffffProcessor-specific 同上

ET_REL

object file

ET_REL型態的elf最主要是object file
下載ET_REL範例,裡面有2個檔案function.cmain.c

  1. 產生 function.c object file
    gcc -fPIC -c function.c
    
  2. 產生 main.c object file
    gcc -fPIC -c main.c
    
  3. 觀察object file的elf type
    readelf -h function.o
    

    ``

kernel module

下載kernel_module範例

編譯

make

載入kernel module

sudo insmod hello.ko

查詢kernel module已經載入了哪些

sudo lsmod | grep "hello"

刪除kernel module

sudo rmmod hello.ko

查詢printk輸出什麼

dmesg

我們來觀察kernel module的elf type

readelf -h hello.ko

ET_EXEC

這種類型的執行檔載入到固定虛擬記憶體位置,所以e_entry的位置就是固定的位置

1
2
3
4
5
#include<stdio.h>
int main(){
	printf("Hello World\n");
	return 0;
}

編譯

gcc -no-pie main.c

觀察型態

readelf-h a.out 

ET_DYN

position-independent executable

這種類型的執行檔才可以被作業系隨機載入到不同虛擬記憶體位置

1
2
3
4
5
#include<stdio.h>
int main(){
	printf("Hello World\n");
	return 0;
}

編譯

gcc -pie main.c

觀察型態

readelf-h a.out 
share libary

接下來繼續用剛才生成a.out來查詢share libary是哪種elf型態

查詢a.out需要動態連結哪些share libary

ldd a.out

我的電腦就出現/lib/x86_64-linux-gnu/libc.so.6

linux-vdso.so.1 (0x00007ffcb0cba000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5f6cd3a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5f6cf4e000)

分析看看是什麼型態

readelf -h /lib/x86_64-linux-gnu/libc.so.6

ET_CORE

這種型態的elf是當你的程式發生core dumped,作業系統就會吐出來ET_CORE的elf讓你用gdb來分析

設定core dump檔案的大小不限制

ulimit -c unlimited

我們故意寫一個會出錯的程式

1
2
3
4
5
6
#include<stdio.h>
int main(){
        int *a=0;
        printf("%d",*a);
        return 0;
}

編譯

gcc -g main.c

執行a.out

./a.out

發生Segmentation fault (core dumped)並且看到core這個檔案

我們來觀察core的type

readelf -h core

我們來利用gdb與core來幫助我們分析哪裡出錯了

gdb a.out core

e_machine

這個欄位是可以知道此elf檔案是屬於哪個架構的

細節請看elf-em.h

其中比較特別的是近幾年比較紅的RISC-V,BPF(linux核心裡面的虛擬機,指令就是虛擬機專用的bytecode)

1
2
#define EM_RISCV	243	/* RISC-V */
#define EM_BPF		247	/* Linux BPF - in-kernel virtual machine */

簡單bpf xdp

這裡小範例讓大家體會什麼是XDP

安裝
1
2
3
sudo apt-get install git
sudo apt-get install clang
git clone https://github.com/libbpf/libbpf.git
創建檔案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <linux/if_ether.h>//ethhdr
#include <linux/ip.h>//iphdr
#include <linux/in.h>//IPPROTO_ICMP
#include <linux/icmp.h>//icmphdr
#include <linux/bpf.h>
#include "libbpf/src/bpf_helpers.h"
SEC("show_icmp")
int xdp_show_icmp(struct xdp_md *ctx)
{	//ctx->data代表收到封包的起始記憶體位置
	//ctx->data_end代表收到封包的結束記憶體位置
    void *data = (void *)(long)ctx->data;
	//ctx->data起始記憶體位置+EtherType大小+IPv4 header大小+ICMP header大小希望小於等於結束記憶體位置
	if (ctx->data_end >= ctx->data + sizeof(struct ethhdr)+sizeof(struct iphdr)+sizeof(struct icmphdr)){
		struct iphdr *ip_header = data+sizeof(struct ethhdr);
		//確認IPv4 header的protocol是否有icmp
		if(ip_header->protocol==IPPROTO_ICMP){
		bpf_printk("I Get The ICMP\n");
		}
	} 
    return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
編譯bpf
clang -isystem /usr/include/x86_64-linux-gnu -O2 -target bpf -c xdp_show_icmp.c -o xdp_show_icmp.o
觀察架構

觀察此elf的指令架構

readelf -h xdp_show_icmp.o
設定XDP
sudo ip link set dev lo xdpgeneric obj xdp_show_icmp.o sec show_icmp
開始實驗

開啟終端機1

sudo cat /sys/kernel/debug/tracing/trace_pipe

開啟終端機2

ping 127.0.0.1

e_version

e_versionEI_VERSION都是固定1

The value 1 signifies the original file format; extensions will create new versions with higher numbers. The value of EV_CURRENT changes as necessary to reflect the current version number.

e_entry

只有e_typeET_EXEC的執行檔他的e_entry才值得參考
e_typeET_DYN的執行檔載入的虛擬記憶體位置會變動

實驗e_entry

先從ET_EXEC準備好環境

找一下Entry point address的數值並且確認e_typeET_EXEC

readelf -h a.out

進入gdb

gdb a.out

中斷在Entry point address

b *0x1234

開始執行

r

e_phoff

紀錄program header table位置在哪,如果沒有就0

e_shoff

紀錄section header table位置在哪,如果沒有就0

e_flags

processor-specific這裡定義硬體的特徵,細節的定義在不同的指令架構的規格書裡面,如果是RISC-V可以參考[RISC-V ELF psABI specification][https://github.com/riscv/riscv-elf-psabi-doc/blob/master/riscv-elf.md#-file-header]裡面關於elf的規範

e_ehsize

ELF header有多少bytes.

e_phentsize

每一個program header大小有多少bytes

e_phnum

有多少個program header,如果沒有就為0

e_shentsize

每一個section header大小有多少bytes

e_shnum

有多少個section header,如果沒有就為0

e_shstrndx

e_shstrndx的數值代表,第幾個section存放.shstrtab這個section,.shstrtab很特殊負責存放所有section人看的懂的名稱

e_shstrndx介紹

實驗看看

準備一個elf檔案

觀察Section header string table index數值(e_shstrndx)

readelf -h a.out

確認.shstrtab的位置是不是與e_shstrndx標示的一樣

readelf -S a.out

直接看.shstrtab內容,可以發現裡存放很多字串

readelf -x .shstrtab a.out

Program Headers

名詞注意:一個segment是由一個到多個section組成

利用指令查看

1
readelf -l 你的檔名

只有executableshared object filesProgram Headers才有特別的用途

Program headers are meaningful only for executable and shared object files

一個elf有0或多個Program Header,每個Program Header專門處理一個segment載入記憶體的規劃,如果你的elf檔案或是某個section不是拿來載入記憶體執行用途可能就沒有這個header,如下範例

先準備好ET_REL的執行檔案
利用readelf -l function.o指令觀察e_typeREL (Relocatable file)的elf有沒有program headers?

每一個Program Header的結構如下圖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct elf64_phdr {
  Elf64_Word p_type;
  Elf64_Word p_flags;
  Elf64_Off p_offset;		/* Segment file offset */
  Elf64_Addr p_vaddr;		/* Segment virtual address */
  Elf64_Addr p_paddr;		/* Segment physical address */
  Elf64_Xword p_filesz;		/* Segment size in file */
  Elf64_Xword p_memsz;		/* Segment size in memory */
  Elf64_Xword p_align;		/* Segment alignment, file & memory */
} E

p_type

Program Header的型態

p_typeValue用途
PT_NULL0沒有在使用, program header table會忽略PT_NULL型態的segment,FIXME: 細節還需要在研究.
PT_LOAD1單純載入segment到虛擬記憶體,如果載入到記憶體(p_memsz)時比在檔案(p_filesz)的時候還大,多出來的地方補0,可能是為了對齊記憶體
PT_DYNAMIC2載入這個segment裡面都是Dynamic Section去虛擬記憶體
PT_INTERP3當程式需要動態連結 就需要載入這個PT_INTERP型態的segment去記憶體 並且裡面包含dynamic linker/loader的路徑
PT_NOTE4載入這個segment裡面都是Note Section去虛擬記憶體
PT_SHLIB5這個保留尚未定義
PT_PHDR6這個型態代表載入整個Program Header Table去虛擬記憶體,裡面有1個以上Program Header
PT_TLS7載入記憶體是以PT_TLS型態,這個segment是存放Thread-local storage變數初始數值
PT_LOOS0x60000000從0x60000000~0x6fffffff的數值保留給作業系統使用
PT_HIOS0x6fffffff同上
PT_LOPROC0x70000000從0x70000000~0x7fffffff的數值保留給處理器使用
PT_HIPROC0x7fffffff同上

PT_INTERP實驗

  1. 觀察Program Header PT_INTERP型態的segment

    readelf -l a.out
    
  2. 編譯此範例程式再觀察一次,可以發現PT_INTERP型態的segment不見了,go語言也是屬於這種不需要動態連結的程式,好處是有很好的可攜性,缺點是執行檔比較大,並且e_type屬於ET_EXEC代表每次都載入到固定虛擬記憶體可能有安全疑慮

  3. 修改ld-linux(dynamic linker )名稱會發生什麼事?

    此實驗會毀損linux執行的過程謹慎練習
    實驗中我的Requesting program interpreter/lib64/ld-linux-x86-64.so.2
    如果我把/lib64/ld-linux-x86-64.so.2名稱改掉會怎樣?
    是不是只有實驗2的程式才能跑?

PT_TLS實驗

參考Linux Thread Local Storage

如下範例可以知道TLS(Thread Local Storage)的作法

每開起一個線程會從TLS_data(elf會定義這個變數存放的segment載入記憶體時p_typePT_TLS)拿出變數的初始數值存放每個線程內部TLS_data並且每個函數都可以直接使用TLS_data.

好處可以看到TLS版本不用傳遞參數給函數就可以直接使用,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define  NUMTHREADS   2 
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void show_tls(int local_data) {
    printf("線程 %.16lx: TLS data=%d\n",pthread_self(), local_data);
}
void *thread_run(int i)
{
   int local_data = i;
   show_tls(local_data);
   return NULL;
}
int main(int argc, char **argv)
{
  pthread_t             thread[NUMTHREADS];
  int                   rc=0;
  printf("開始執行\n");
  for (int i=0; i < NUMTHREADS; i++) { 
     rc = pthread_create(&thread[i], NULL, &thread_run, i);
  }
  printf("執行中..\n");
  for (int i=0; i < NUMTHREADS; i++) {
     rc = pthread_join(thread[i], NULL);  
  }
  printf("執行完成\n");
  return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define  NUMTHREADS   2 
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
__thread int TLS_data=87;
void show_tls() {
    printf("線程 %.16lx: TLS data=%d\n",pthread_self(), TLS_data);
}
void *thread_run(int i)
{
   TLS_data = i;
   show_tls();
   return NULL;
}
int main(int argc, char **argv)
{
  pthread_t             thread[NUMTHREADS];
  int                   rc=0;
  printf("開始執行\n");
  for (int i=0; i < NUMTHREADS; i++) { 
     rc = pthread_create(&thread[i], NULL, &thread_run, i);
  }
  printf("執行中..\n");
  for (int i=0; i < NUMTHREADS; i++) {
     rc = pthread_join(thread[i], NULL);  
  }
  printf("執行完成\n");
  return 0;
}

編譯

gcc t.c -lpthread

p_flags

定義這個區塊的segment在記憶體的權限 最後3個bit定義 讀/寫/執行

舉例
可讀/不可寫/可執行
0x00000000000000000000000000000101(32bit)

可讀/可寫/可執行
0x00000000000000000000000000000111(32bit)

p_offset

定義這個segment在檔案哪個位置開始

p_vaddr

定義這個segment載入到虛擬記憶體的哪個位置,只有沒有經過ASLR(Address space layout randomization)載入的執行檔(e_typeET_EXEC的執行檔),這個數值才有參考價值

p_paddr

定義這個segment載入到實體記憶體的哪個位置,因為實體記憶體被作業系統保護所以這個數值不準確.

p_filesz

定義這個segment在檔案的大小多少bytes,可為0

p_memsz

定義這個segment在虛擬記憶體的大小多少bytes,可為0

p_align

Section Headers

利用指令查看

1
readelf -S 你的檔名
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct elf64_shdr {
  Elf64_Word sh_name;		/* Section name, index in string tbl */
  Elf64_Word sh_type;		/* Type of section */
  Elf64_Xword sh_flags;		/* Miscellaneous section attributes */
  Elf64_Addr sh_addr;		/* Section virtual addr at execution */
  Elf64_Off sh_offset;		/* Section file offset */
  Elf64_Xword sh_size;		/* Size of section in bytes */
  Elf64_Word sh_link;		/* Index of another section */
  Elf64_Word sh_info;		/* Additional section information */
  Elf64_Xword sh_addralign;	/* Section alignment */
  Elf64_Xword sh_entsize;	/* Entry size if section holds table */
} Elf64_Shdr;

sh_name

sh_name存放名稱的index

section的名稱字串存在.shstrtab

sh_type

NameValue用途
SHT_NULL0這個型態代表這個section header無效
SHT_PROGBITS1這裡面的section放程式執行用到的東西
SHT_SYMTAB2這裡面的section放symbol table FIXME Symbol Table
SHT_STRTAB3這裡面的section放滿人可以看得懂的字串,例如可以拿來表示section的名稱,
SHT_RELA4給object file重定位資訊用的(with explicit addends) FIXME:Relocation
SHT_HASH5這個section給symbol table使用
SHT_DYNAMIC6這裡放動態連結的訊息 FIXME:Dynamic Section
SHT_NOTE7這裡檔案的訊息 FIXME:Note Section
SHT_NOBITS8代表這個section在檔案裡面沒有佔用任何大小,它只有section header而已
SHT_REL9給object file重定位資訊用的(without explicit addends) FIXME:Relocation
SHT_SHLIB10這個保留尚未定義
SHT_DYNSYM11這裡面的section放symbol table
SHT_INIT_ARRAY14裡面有陣列記憶體位置,指向主程式開始前的函數FIXME:Initialization and Termination Functions
SHT_FINI_ARRAY15裡面有陣列記憶體位置,指向主程式結束後要執行的函數,atexit FIXME:Initialization and Termination Functions
SHT_PREINIT_ARRAY16裡面有陣列記憶體位置指向函數,這個函數會在SHT_INIT_ARRAY裡面所有函數執行之前執行
SHT_GROUP17
SHT_SYMTAB_SHNDX18
SHT_LOOS0x60000000從0x60000000~0x6fffffff的數值保留給作業系統使用
SHT_HIOS0x6fffffff同上
SHT_LOPROC0x70000000從0x70000000~0x7fffffff的數值保留給處理器使用
SHT_HIPROC0x7fffffff同上
SHT_LOUSER0x80000000從0x80000000~0xffffffff的數值保留給應用程式使用
SHT_HIUSER0xffffffff同上

Initialization and Termination Functions

尚未完成
這裡深入探討SHT_PREINIT_ARRAY,SHT_FINI_ARRAY,SHT_INIT_ARRAY

sh_flags

NameValue用途
SHF_WRITE0x1程式執行時這個section可寫
SHF_ALLOC0x2程式執行時這個section會佔用記憶體
SHF_EXECINSTR0x4這個section包含機械碼
SHF_MERGE0x10
SHF_STRINGS0x20包含以零結尾的字符串組成
SHF_INFO_LINK0x40
SHF_LINK_ORDER0x80
SHF_OS_NONCONFORMING0x100
SHF_GROUP0x200
SHF_TLS0x400
SHF_MASKOS0x0ff00000保留給作業系統使用
SHF_MASKPROC0xf0000000保留給處理器使用

sh_addr

此section在執行時的記憶體哪個位置
0代表沒有在記憶體中

sh_offset

此section在檔案時哪個位置

sh_size

代表section在檔案的大小

如果sh_typeSHT_NOBITS,雖然檔案沒有存放,但是sh_size可能不為0
舉例
c語言還沒初始化的變數放在.bss section屬於SHT_NOBITS在檔案沒佔空間(只有section header)但載入記憶體後要佔空間

sh_info

sh_addralign

此section要對齊記憶體
必須讓sh_addr數值剛好,sh_addr/sh_addralign整除才能對齊
記憶體對齊可以增加效能,或是對齊cache避免False_sharing

sh_entsize

Special Sections

這個部份負責探討特殊Sections在幹麻
這個部份尚未完成

Relocation

重定位,用在靜態連結動態連結

現在流行可攜性高的程式很多程式golange

重定位方式效能檔案大小
靜態連結
動態連結
動態載入

Types

關於Relocation Types (Processor-Specific)是屬於每個指令架構abi所定義,而我們目前只討論AMD64的細節,文件System V Application Binary Interface AMD64 Architecture Processor Supplement有定義elf-Processor-Specific的規範

procedure linkage table

進階練習

學完elf後,如何從這個範例刪減elf執行檔的大小又可以執行?

可以作答在最下面的討論區

可以參考A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux

lief工具修改

參考

[操作系统真象还原-5.3.3 elf 格式的二进制文件]
[Learning Linux Binary Analysis]
The Curious Case of Position Independent Executables
APP漏洞扫描器之未使用地址空间随机化
ELF之學習心得02 - ELF Header(e_ident篇)

ELF Header

Share on

呂奕珣
WRITTEN BY
呂奕珣
SDN ML FINTECH HFT