成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

程序員內功修煉:五分鐘徹底搞懂 Linux ELF 文件 !

系統
ELF 文件不僅僅是一個格式,它是 Linux 世界中程序的"靈魂容器",承載著程序從編譯到執行的整個生命周期。?

大家好啊,我是小康。

今天咱們來聊一個看起來高深,實際上理解起來其實挺簡單的話題—— ELF 文件。

不知道你有沒有想過:我們敲下./program命令的那一刻,計算機是怎么把這個文件變成一個活蹦亂跳的進程的?這背后的"黑魔法"到底是什么?

沒錯,答案就是今天的主角:ELF(Executable and Linkable Format)可執行與可鏈接格式。你可以把它理解為 Linux 世界里程序的"靈魂容器"!

一、什么是 ELF 文件?給個痛快話!

簡單來說,ELF 是 Linux 下的可執行文件格式,就像 Windows 下的 .exe 一樣。但別被這個簡單的解釋騙了,ELF 可比 .exe 復雜得多,也強大得多!

ELF 文件可以是:

  • 可執行文件(比如你的./program)
  • 目標文件(編譯后但還沒鏈接的 .o 文件)
  • 共享庫文件(就是 .so 文件,類似 Windows 下的 .dll)
  • 核心轉儲文件(程序崩潰時的那個core dump)

本質上,ELF 就是一個容器,里面裝著代碼、數據以及程序運行所需的各種信息,按照特定的格式組織起來。

二、初見 ELF:第一印象很重要

想知道一個文件是不是 ELF 格式的?超簡單:

$ file /bin/ls
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, ...

看到沒?只要文件輸出信息的開頭是"ELF",那它就是 ELF 格式的!

再來點兒硬核的,我們直接看一下 ELF 文件的前幾個字節:

$ hexdump -C -n 16 /bin/ls
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|

這里最開始的7f 45 4c 46就是 ELF 文件的"魔數"(Magic Number)。其中 45 4c 46 是 ASCII 碼中的 "ELF" 三個字母,前面的 7f 是一個特殊字符。這四個字節就是 ELF 文件的"身份證",操作系統首先會檢查這四個字節,確認它是不是一個 ELF 文件。

三、ELF 文件的內部結構:化繁為簡

很多教程一上來就給你畫個復雜的結構圖,看得人頭暈眼花。咱們先別急,我用一個簡單的類比來幫你理解:

把 ELF 文件想象成一本"程序說明書",這本書有三部分組成:

  • 文件頭(ELF Header):相當于書的封面和目錄,告訴你這本書有什么內容,怎么看
  • 程序頭表(Program Header Table):相當于給"閱讀器"(操作系統)看的指南,告訴它怎么把這本書變成一個活的程序
  • 節區頭表(Section Header Table):相當于給"編輯器"(鏈接器、調試器)看的指南,告訴它這本書的內部結構

然后,書的主體內容就是各種節區(Sections)或段(Segments),里面裝著代碼、數據等實際內容。

直觀一點,用圖來表示就是:

+------------------+
|    ELF Header    | <-- 文件開始處的標識信息和總體布局
+------------------+
|     程序頭表      | <-- 告訴操作系統如何加載
| Program Header 1 |
| Program Header 2 |
|       ...        |
+------------------+
|    Section 1     | <-- 實際內容,如代碼、數據等
|    Section 2     |
|       ...        |
+------------------+
|    節區頭表       |  <-- 描述每個Section的信息
| Section Header 1 | 
| Section Header 2 |
|       ...        |
+------------------+

哎,你可能會問:什么是節區(Section)?什么又是段(Segment)?它們有什么區別?

簡單來說:

  • 節區(Section):是 ELF 文件存儲的基本單位,針對鏈接器
  • 段(Segment):是運行時內存的基本單位,針對加載器

一個段通常包含多個功能相似的節區。比如,包含代碼的所有節區會被歸入到一個叫做"TEXT"的段中。

四、深入解剖 ELF文件:逐層剝開

1. ELF頭(ELF Header)

ELF 頭是整個文件的"門面",包含了文件的基本信息和指向其他部分的指針。用readelf -h命令可以查看:

$ readelf -h /bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x5850
  Start of program headers:          64 (bytes into file)
  Start of section headers:          136912 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

這里面最重要的信息是:

  • Entry point address:程序執行的起點地址
  • Start of program headers:程序頭表的位置
  • Start of section headers:節區頭表的位置

2. 程序頭表(Program Header Table)

程序頭表告訴操作系統如何創建進程映像,用readelf -l命令查看:

$ readelf -l /bin/ls

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000004428 0x0000000000004428  R      0x1000
  LOAD           0x0000000000005000 0x0000000000005000 0x0000000000005000
                 0x0000000000012d1c 0x0000000000012d1c  R E    0x1000
  LOAD           0x0000000000018000 0x0000000000018000 0x0000000000018000
                 0x0000000000004d40 0x0000000000004d40  R      0x1000
  LOAD           0x000000000001d520 0x000000000001e520 0x000000000001e520
                 0x0000000000001640 0x0000000000002270  RW     0x1000
...

最重要的是那些類型為LOAD的段,它們會被加載到內存中。

注意看Flags:

  • R表示可讀(Read)
  • W表示可寫(Write)
  • E表示可執行(Execute)

這就是為什么有的內存區域可執行,有的只能讀不能寫,這些權限在 ELF 文件里就定義好了!

3. 節區頭表(Section Header Table)

節區頭表描述了文件中各個節區的信息,用readelf -S查看:

$ readelf -S /bin/ls
There are 30 section headers, starting at offset 0x21730:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.build-i NOTE             0000000000000338  00000338
       0000000000000024  0000000000000000   A       0     0     4
...
  [11] .text             PROGBITS         00000000000052a0  000052a0
       0000000000012a7c  0000000000000000  AX       0     0     16
...
  [23] .data             PROGBITS         000000000001e520  0001d520
       0000000000000e60  0000000000000000  WA       0     0     32
  [24] .bss              NOBITS           000000000001f380  0001e380
       0000000000001410  0000000000000000  WA       0     0     32
...

常見的重要節區包括:

  • .text:存放程序的機器代碼
  • .data:已初始化的全局變量和靜態變量
  • .bss:未初始化的全局變量和靜態變量(不占用文件空間)
  • .rodata:只讀數據(如字符串常量)
  • .symtab:符號表,存儲程序中定義和引用的函數、變量
  • .strtab:字符串表,通常存儲符號名
  • .dynamic:動態鏈接信息

五、ELF 文件的生命周期:從編譯到執行

為了徹底搞懂 ELF 文件,我們需要了解它的整個生命周期:

源代碼(.c) --編譯--> 目標文件(.o) --鏈接--> 可執行文件 --加載--> 進程

1. 編譯階段:生成目標文件(.o)

當你寫完 C 代碼,運行gcc -c hello.c時,會得到一個hello.o的目標文件。這個文件已經是 ELF 格式的了,但它還不能直接執行,因為里面有很多"坑"等著被填上。

這些"坑"在 ELF 文件中表現為"重定位表",用readelf -r可以看到:

$ readelf -r hello.o

Relocation section '.rela.text' at offset 0x2d0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000013  000a00000004 R_X86_64_PLT32    0000000000000000 printf - 4
000000000023  000b00000004 R_X86_64_PLT32    0000000000000000 exit - 4

這表示代碼中調用了printf和exit函數,但編譯器不知道它們在哪兒,所以留了個"坑"等著鏈接器來填。

2. 符號表:程序的"通訊錄"

說到這些函數(printf 、exit),咱們不得不提 ELF 文件中的"符號表"。簡單來說,符號表就像是程序的"通訊錄",記錄了程序中所有函數和變量的名字和位置。

來看看符號表長啥樣:

$ readelf -s hello.o

Symbol table '.symtab' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
    ...
     9: 0000000000000000    41 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND exit

瞧,這里面有main函數(我們自己定義的),還有printf和exit(外部函數)。注意它們的Ndx(索引)列:main是1,表示在第1個節區;而printf和exit是UND,表示"未定義",這就是前面說的"坑"。

這個目標文件的符號表就像一張"半成品通訊錄",只記錄了自己有什么函數,以及自己需要哪些外部函數,但還不知道那些外部函數在哪里。所以它還不能獨立工作,需要鏈接器來幫忙找到這些外部函數。

3. 動態鏈接:程序的"即插即用"

說到外部函數,就不得不提 ELF 的一個超強功能:動態鏈接。

還記得 Windows 上安裝軟件時經常冒出的"DLL缺失"錯誤嗎?Linux 上也有類似概念,不過實現得更優雅,這就是動態鏈接庫(.so文件)。

動態鏈接的好處簡直不要太多:

  • 節省內存:多個程序共享同一個庫
  • 節省磁盤:不用把所有代碼都打包進可執行文件
  • 方便升級:庫更新后,程序自動用上新版本,不用重新編譯

那么問題來了:程序怎么知道自己需要哪些庫?又是如何找到這些庫的呢?

ELF 文件中有一個特殊的.dynamic節區,專門記錄這些信息:

$ readelf -d /bin/ls | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libselinux.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

這告訴我們,ls命令依賴于這兩個共享庫。如果你想更直觀地看到所有依賴及它們的實際位置,可以用ldd命令:

$ ldd /bin/ls
 linux-vdso.so.1 (0x00007ffc961cd000)
 libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f27f989e000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f27f96b3000)
 ...

看到沒?ldd不僅告訴你需要哪些庫,還告訴你它們的實際位置和加載地址。

那程序又是怎么找到這些庫的呢?它會按照以下順序查找:

  • 環境變量LD_LIBRARY_PATH指定的目錄
  • 可執行文件的RPATH屬性指定的目錄
  • /etc/ld.so.cache緩存中記錄的位置
  • 默認目錄如/lib、/usr/lib等

動態鏈接器(ld.so)會在程序啟動時自動處理這些依賴關系,把所有需要的庫都加載進來,就像樂高積木一樣把程序拼裝完整,非常巧妙!

4. 鏈接階段:生成可執行文件

鏈接器會把多個目標文件和庫文件鏈接在一起,解決那些"坑"(重定位),最終生成可執行文件。

那么鏈接器具體是怎么解決這些"坑"的呢?簡單來說就是做個"牽線搭橋"的活:

  • 收集所有目標文件中的符號表,建立一個全局符號表
  • 找到所有標記為"未定義"(UND)的符號
  • 在全局符號表或者庫文件中尋找這些符號的定義
  • 把找到的地址填回原來的"坑"中

比如當鏈接器找到printf函數在 libc.so 中的實際地址后,就會修改原來調用 printf 的指令,讓它指向正確的地址。

鏈接完成后,再看同一個程序的符號表,會發現那些 UND 的符號要么有了實際地址(靜態鏈接),要么指向了動態鏈接的跳轉表(動態鏈接)。

在動態鏈接的情況下,還會在 ELF 文件中記錄運行時需要哪些共享庫,前面已經說過了。

5. 加載階段:從文件到進程

當你執行./program時,操作系統(確切地說是加載器 ld.so )會做這些事:

  • 檢查 ELF 頭的合法性
  • 根據程序頭表,將需要的段加載到內存
  • 如果是動態鏈接的,還會找到并加載所需的共享庫
  • 跳轉到 Entry Point 開始執行

這個過程可以用strace命令觀察:

$ strace ./hello
execve("./hello", ["./hello"], 0x7ffcef8db490 /* 52 vars */) = 0
brk(NULL)                               = 0x55c84f34c000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
...

execve就是創建新進程的系統調用,后面一系列操作就是在加載和準備程序運行環境。

六、ELF實用工具箱:玩轉ELF文件

好了,了解了 ELF 的原理后,來看看有哪些工具可以幫我們操作 ELF 文件:

(1) file:判斷文件類型

$ file /bin/ls

(2) readelf:查看ELF文件的所有信息

$ readelf -a /bin/ls  # 顯示全部信息

(3) objdump:反匯編 ELF 文件

$ objdump -d /bin/ls  # 反匯編代碼段

(4) nm:列出符號表

$ nm /bin/ls  # 顯示符號(函數、變量)

(5) ldd:查看動態依賴

$ ldd /bin/ls  # 顯示依賴的共享庫

(6) strings:提取文件中的字符串

$ strings /bin/ls | grep "GNU"  # 查找包含"GNU"的字符串

(7) strip:移除ELF文件中的符號表和調試信息

$ strip -s program  # 減小文件體積

(8) patchelf:修改 ELF 文件的屬性

$ patchelf --set-interpreter /lib64/ld-custom.so program  # 修改解釋器

七、實際應用:ELF文件的那些神奇玩法

ELF文件的知識不僅僅是理論,來看看一些實際的例子:

1. 程序加固與混淆

想象你開發了一個軟件不想被輕易破解:

# 刪除符號表,讓逆向分析更困難
$ strip --strip-all myprogram

# 對比前后大小
$ ls -lh myprogram*
-rwxr-xr-x 1 user user 236K myprogram
-rwxr-xr-x 1 user user 176K myprogram.stripped

看,文件體積一下減少了幾十k,因為符號信息都被刪掉了!

2. 程序補丁與熱修復

假設你想修改程序使用的解釋器路徑:

# 查看當前解釋器
$ readelf -l myprogram | grep interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

# 修改為自定義解釋器
$ patchelf --set-interpreter /opt/mylibs/ld-linux.so myprogram

# 確認修改成功
$ readelf -l myprogram | grep interpreter
      [Requesting program interpreter: /opt/mylibs/ld-linux.so]

這樣程序就會使用你自定義的動態鏈接器,而不需要重新編譯!

更酷的是,Linux 還提供了一種不用重啟程序就能熱修復的黑科技——LD_PRELOAD環境變量!它可以讓你悄悄地"替換"程序中的函數實現。

來看一個簡單實用的例子 —— 監控程序的內存分配:

創建一個簡單的內存跟蹤庫: memtrace.c

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

// 原始malloc函數指針
staticvoid* (*real_malloc)(size_t) = NULL;

// 攔截 malloc 函數
void* malloc(size_t size) {
    // 延遲初始化原始函數
    if (real_malloc == NULL) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    
    // 調用原始malloc
    void* ptr = real_malloc(size);
    
    // 打印跟蹤信息
    fprintf(stderr, "malloc(%zu) = %p\n", size, ptr);
    
    return ptr;
}

編譯成共享庫:

$ gcc -shared -fPIC memtrace.c -o libmemtrace.so -ldl

接著使用我們的庫監控任何程序的內存分配:

LD_PRELOAD=./libmemtrace.so ./my_program

輸出:

malloc(100) = 0x55e930e2f6b0
malloc(200) = 0x55e930e2f720
malloc(300) = 0x55e930e2f7f0

看到了嗎?我們只用了十幾行代碼,就實現了一個能夠監控任何程序內存分配的工具!這個例子的工作原理很簡單:

  • 定義一個與系統函數同名的malloc
  • 用dlsym(RTLD_NEXT, "malloc")找到真正的 malloc 函數
  • 在調用真正的 malloc 前后添加我們的代碼(這里是打印日志)
  • 通過LD_PRELOAD讓系統優先加載我們的庫

這種技術經常用于:

  • 調試內存問題
  • 給程序添加日志
  • 修改程序行為而不用改源碼
  • 臨時修復運行中的服務

當然,這項技術也常被黑客利用來劫持程序函數,所以理解它不僅能提升編程能力,也對安全防護很重要!

八、總結:ELF 文件的精髓

好了,咱們來總結一下 ELF 文件的核心要點:

(1) ELF是容器:裝載了代碼、數據和各種元數據

(2) 分層結構:ELF 頭、程序頭表、節區、節區頭表

(3) 兩種視角:

  • 執行視角:段(Segments)- 加載器關心
  • 鏈接視角:節(Sections)- 鏈接器關心

(4) 生命周期:從源代碼到目標文件,再到可執行文件,最后變成進程

當你理解了 ELF 文件的本質,Linux 下的很多問題就迎刃而解了:為什么有些程序不能在不同版本的 Linux 上運行?為什么動態庫版本不匹配會導致程序崩潰?為什么有些惡意軟件難以檢測?——這些問題的答案都藏在 ELF 文件的結構中!

記住,ELF 文件不僅僅是一個格式,它是 Linux 世界中程序的"靈魂容器",承載著程序從編譯到執行的整個生命周期。

責任編輯:趙寧寧 來源: 跟著小康學編程
相關推薦

2009-12-04 10:45:20

程序員職場

2021-06-18 07:34:12

Kafka中間件微服務

2024-12-11 07:00:00

面向對象代碼

2025-03-13 06:22:59

2025-01-21 07:39:04

Linux堆內存Golang

2019-08-09 10:33:36

開發技能代碼

2025-01-20 08:50:00

2009-05-21 16:23:23

程序員法則職場

2023-12-06 08:48:36

Kubernetes組件

2023-09-18 15:49:40

Ingress云原生Kubernetes

2022-05-23 09:10:00

分布式工具算法

2024-04-29 07:57:46

分布式流控算法

2024-12-04 16:12:31

2025-05-22 08:10:00

C++條件變量編程

2020-03-03 19:59:38

主板無線網卡

2017-03-30 19:28:26

HBase分布式數據

2019-06-14 09:34:59

Linux 系統 數據

2018-09-27 13:56:14

內網外網通信

2023-10-06 20:21:28

Python鏈表

2012-07-05 09:37:04

Java程序員
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品亚洲精品日韩已方 | 国产综合久久 | 99热播精品| 亚洲午夜视频在线观看 | 91秦先生艺校小琴 | 国产精品不卡 | 日韩免费网站 | 亚洲小说图片 | 亚洲精品国产电影 | 免费视频一区二区 | 欧美二级| 日韩精品1区2区3区 成人黄页在线观看 | 久久网一区二区三区 | 成人在线一区二区三区 | 粉嫩av| 成人影| 国产高清久久久 | 国产精品视频网 | 亚洲三区在线观看 | 99国产精品99久久久久久粉嫩 | 北条麻妃99精品青青久久主播 | 日本三级网站在线观看 | 999久久久| 亚洲va欧美va天堂v国产综合 | 欧美一区二区三区四区五区无卡码 | 美女爽到呻吟久久久久 | 成年人视频在线免费观看 | 毛片在线看看 | 日韩亚洲一区二区 | 日韩一区二区在线免费观看 | 久久精品日产第一区二区三区 | www.成人免费视频 | 日本成人在线免费视频 | 欧美一级毛片免费观看 | 免费黄色片在线观看 | 香蕉大人久久国产成人av | 国产激情视频在线观看 | 成人在线视频观看 | 欧美黄色一区 | 免费视频一区二区 | 精品久久久久久红码专区 |