精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

從Execve到進程運行:ELF加載的內核實現詳解

系統 Linux
在 Linux 的世界里,ELF 文件就像是一個神秘的 “魔法盒子”,承載著程序從源代碼到可執行實體的關鍵信息,而 Linux 進程則是這些信息在系統中鮮活運行的體現。從 ELF 文件的結構剖析,到它如何通過編譯鏈接成為可執行文件,再到 Linux 進程的創建以及 ELF 文件在其中的加載過程,每一個環節都充滿了奇妙的技術細節。

在 Linux 的世界里,ELF 文件就像是一個神秘的 “魔法盒子”,承載著程序從源代碼到可執行實體的關鍵信息,而 Linux 進程則是這些信息在系統中鮮活運行的體現。從 ELF 文件的結構剖析,到它如何通過編譯鏈接成為可執行文件,再到 Linux 進程的創建以及 ELF 文件在其中的加載過程,每一個環節都充滿了奇妙的技術細節。

理解從 ELF 文件到 Linux 進程的轉化過程,對于 Linux 開發者、系統管理員以及對計算機底層原理感興趣的技術愛好者來說,都具有重要的意義。它不僅有助于我們優化程序的編譯、鏈接和運行效率,還能在程序出現問題時,通過深入分析 ELF 文件和進程狀態,快速定位和解決問題。例如,在調試程序時,了解 ELF 文件中的符號表和重定位信息,可以幫助我們準確地找到程序中的錯誤代碼位置;在系統性能優化方面,掌握進程的內存布局和動態鏈接機制,可以讓我們更好地管理系統資源,提高程序的運行效率。今天,就讓我們一起揭開從 ELF 文件到 Linux 進程的神秘面紗,深入了解它們之間的奇妙轉化。

一、ELF 文件:Linux 世界的 “靈魂容器”

1.1什么是 ELF 文件

ELF,全稱是 Executable and Linkable Format,即可執行與可鏈接格式 ,是 Linux 下二進制文件的標準格式。就像 Windows 系統中大家熟悉的.exe 文件是可執行程序的載體一樣,ELF 文件在 Linux 系統中承擔著同樣的角色,并且功能更為豐富。

ELF 文件不僅僅局限于可執行文件,它還涵蓋了多種類型:

  1. 可執行文件:這是我們日常使用的程序,比如常見的命令行工具ls、grep等,或者是我們自己編譯生成的可執行程序。當我們在終端輸入命令運行它們時,系統就會依據 ELF 文件的內容將其轉化為運行中的進程。
  2. 可重定位文件:通常是編譯過程中生成的目標文件,以.o 為擴展名。這類文件包含了機器代碼和數據,但它們的地址是相對的,還需要經過鏈接過程才能最終成為可執行文件 。比如我們編寫一個簡單的 C 程序,使用gcc -c命令編譯后生成的.o 文件就是可重定位文件。
  3. 共享對象文件:也就是我們常說的動態鏈接庫,以.so 為擴展名,類似于 Windows 下的.dll 文件。共享對象文件可以在多個程序之間共享代碼和數據,大大節省了系統資源。許多大型軟件項目都會依賴各種共享庫,比如圖形界面程序可能依賴于 GTK 庫,科學計算程序可能依賴于 BLAS、LAPACK 等數學庫。
  4. 核心轉儲文件:當程序出現異常崩潰時,操作系統會生成一個核心轉儲文件,它記錄了程序崩潰時的內存狀態、寄存器值等信息,對于開發者調試程序非常有幫助。通過分析核心轉儲文件,我們可以找到程序崩潰的原因,比如空指針引用、數組越界等問題。

1.2 ELF 的 “身份證”

在 Linux 系統中,判斷一個文件是否為 ELF 格式其實非常簡單,使用file命令就可以輕松做到。例如,我們想要查看/bin/ls這個文件,在終端輸入file /bin/ls,得到的輸出結果開頭如果是 “ELF”,那就說明它是 ELF 格式的文件 ,如/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, ... 。

從更底層的原理來講,ELF 文件有一個獨特的標識 —— 魔數(Magic Number)。通過hexdump -C -n 16 /bin/ls命令查看/bin/ls文件的前 16 個字節,會看到類似00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|的輸出 ,其中最開始的7f 45 4c 46就是 ELF 文件的魔數,45 4c 46對應的正是 ASCII 碼中的 “ELF” 三個字母,而前面的7f是一個特殊字符 。操作系統在識別文件時,首先就會檢查這四個字節,如果匹配,就可以確定它是一個 ELF 文件,所以說魔數就像是 ELF 文件的 “身份證” 。

1.3 ELF 文件的內部結構剖析

ELF 文件的結構就像是一座精心規劃的大廈,每個部分都有其獨特的功能和作用,共同協作使得程序能夠順利地從磁盤走向內存,最終在 CPU 上運行。

圖片圖片

(1)文件頭(ELF Header)

文件頭就像是這本書的封面和目錄,位于 ELF 文件的開頭,固定占據一定的字節數(32 位系統通常是 52 字節,64 位系統通常是 64 字節 )。它包含了許多關鍵信息,這些信息是理解整個 ELF 文件的基礎。

  • 魔數(Magic Number):文件頭的前 4 個字節是魔數,固定為 0x7f 45 4c 46,其中 45 4c 46 分別對應 ASCII 碼中的 'E'、'L'、'F',前面的 0x7f 是一個特殊字符。魔數就像是 ELF 文件的 “身份證”,操作系統在加載文件時,首先會檢查這個魔數,以確定該文件是否為合法的 ELF 文件。
  • 文件類型:它表明了該 ELF 文件屬于可執行文件、可重定位文件、共享對象文件還是核心轉儲文件等。通過這個信息,系統可以知道如何處理這個文件。
  • 目標機器架構:告知系統該 ELF 文件是為哪種 CPU 架構編譯的,比如常見的 x86_64、ARM 等。不同的 CPU 架構有不同的指令集和寄存器結構,了解目標機器架構能確保系統正確地解釋和執行 ELF 文件中的代碼。
  • 入口點地址:程序執行的起始地址,當 ELF 文件被加載到內存并準備執行時,CPU 會從這個地址開始讀取和執行指令。
  • 程序頭表和節區頭表的偏移:這兩個偏移量分別指示了程序頭表和節區頭表在 ELF 文件中的位置。通過這些偏移量,系統可以方便地找到并解析這兩個重要的表。

(2)程序頭表(Program Header Table)

程序頭表就像是給操作系統的指南,它由一系列的程序頭(Program Header)組成,每個程序頭描述了一個段(Segment)的信息。這些段告訴操作系統如何將程序加載到內存中執行。只有可執行文件和共享庫中存在程序頭表,目標文件中是沒有的。

每個程序頭是一個結構體,在 64 位系統中通常是 Elf64_Phdr 結構 ,包含以下重要字段:

  • 段類型(p_type):指示該段的類型,常見的類型有 LOAD(可加載段,包含代碼或數據)、INTERP(指定程序解釋器,即動態鏈接器的路徑)、DYNAMIC(包含動態鏈接相關信息)等。
  • 段標志(p_flags):描述段的權限和屬性,如可讀(R)、可寫(W)、可執行(E)等。比如代碼段通常具有可讀和可執行權限,數據段可能具有可讀和可寫權限。
  • 文件偏移(p_offset):表示該段在 ELF 文件中的起始偏移位置,通過這個偏移量,系統可以在文件中準確找到該段的內容。
  • 虛擬地址(p_vaddr):段被加載到內存后的虛擬地址,操作系統會根據這個地址將段映射到相應的內存區域。
  • 文件大小(p_filesz):段在 ELF 文件中的大小,即實際存儲在文件中的字節數。
  • 內存大小(p_memsz):段在內存中占用的大小,有些段在內存中可能需要額外的空間,比如.bss 段在文件中通常不占用空間,但在內存中需要為未初始化的變量分配空間。

在進程創建過程中,操作系統會讀取程序頭表,找到所有類型為 LOAD 的段,并按照段的信息將它們加載到內存中,為進程的運行做好準備。

(3)節區頭表(Section Header Table)

節區頭表是給鏈接器、調試器等工具看的指南,它描述了 ELF 文件中各個節區(Section)的信息。節區頭表由多個節區頭(Section Header)組成,每個節區頭是一個結構體,包含了節區的名稱、類型、地址、大小、偏移量等信息。

通過節區頭表,鏈接器可以在鏈接過程中準確地找到各個目標文件中的節區,并將它們合并和重定位,生成最終的可執行文件或共享庫。調試器也可以利用節區頭表中的信息,獲取調試符號、源代碼行號等調試信息,方便開發者調試程序。

節區(Sections)與段(Segments)

節區是 ELF 文件存儲的基本單位,針對鏈接器而言;段是運行時內存的基本單位,針對加載器而言。可以把段看作是一個或多個功能相似的節區的集合。常見的節區有:

  • .text:代碼節區,存放程序的可執行代碼,通常具有可讀和可執行權限。
  • .data:已初始化數據節區,存儲已經初始化的全局變量和靜態變量。
  • .bss:未初始化數據節區,用于存放未初始化的全局變量和靜態變量,在 ELF 文件中,.bss 節區通常不占用實際的磁盤空間,因為它只需要在內存中為這些未初始化變量分配空間。
  • .rodata:只讀數據節區,存放只讀數據,比如字符串常量、常量數組等 。
  • .symtab:符號表節區,記錄了程序中定義和引用的符號信息,包括函數名、變量名、全局變量、局部變量等,以及它們的地址、類型等。符號表在鏈接過程中起著關鍵作用,鏈接器通過符號表來解析外部符號的引用。
  • .strtab:字符串表節區,保存了符號表中符號的名字以及其他一些字符串信息。由于字符串的長度不一,為了方便管理,將所有字符串集中存儲在這個節區,通過偏移量來訪問具體的字符串。

這些節區會根據其功能和屬性被劃分到不同的段中,比如.text 節區通常會被包含在代碼段中,.data 和.bss 節區會被包含在數據段中 。

二、從 ELF 文件到 Linux 進程

2.1ELF 文件的編譯與鏈接

在 Linux 系統中,從我們編寫的源代碼到最終生成可執行的 ELF 文件,需要經過編譯和鏈接兩個重要階段。這兩個階段就像是一場精密的制造過程,將人類可讀的代碼轉化為計算機能夠理解和執行的指令。

(1)編譯階段:當我們使用 C、C++ 等編程語言編寫好源代碼后,首先要進行的就是編譯。以 C 語言為例,我們通常使用 GCC(GNU Compiler Collection)編譯器。假設我們有一個簡單的 C 程序hello.c:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

使用命令gcc -c hello.c進行編譯,這里的-c選項表示只進行編譯,不進行鏈接。編譯過程主要分為以下幾個步驟:

  • 預處理:預處理器首先對源代碼進行處理,它會展開頭文件(比如這里的stdio.h),處理宏定義(如果有宏的話),移除注釋等。經過預處理后的代碼會包含所有展開后的內容,方便后續的編譯操作。
  • 編譯:編譯器將預處理后的代碼轉化為匯編代碼。對于上面的hello.c,會生成對應的hello.s匯編文件。匯編代碼是一種低級的人類可讀代碼,它與機器指令有著緊密的對應關系,不同的 CPU 架構有不同的匯編語言。
  • 匯編:匯編器將匯編代碼進一步轉化為目標文件,通常以.o 為擴展名,這里會生成hello.o。目標文件是一種中間格式,它包含了機器代碼,但這些代碼的地址還不是最終可執行的地址,是相對地址,并且還需要解決外部符號的引用等問題 。目標文件中除了機器代碼外,還包含了符號表、重定位信息等。符號表記錄了程序中定義和引用的符號(如函數名、變量名)及其相關信息;重定位信息則用于在鏈接階段調整代碼和數據的地址,使其能夠正確地運行。

(2)鏈接階段:鏈接的主要作用是將多個目標文件以及所需的庫文件合并成一個可執行文件。鏈接分為靜態鏈接和動態鏈接兩種方式。

①靜態鏈接:在靜態鏈接過程中,鏈接器會將程序所依賴的所有靜態庫文件(通常以.a 為擴展名)中的相關代碼和數據直接拷貝到生成的可執行文件中。這樣生成的可執行文件是一個獨立的文件,不依賴外部的庫文件就可以運行。例如,我們有兩個源文件main.c和func.c,func.c中定義了一個函數add,main.c中調用了這個函數:

// func.c
int add(int a, int b) {
    return a + b;
}
// main.c
#include <stdio.h>
int add(int a, int b);

int main() {
    int result = add(3, 5);
    printf("The result is: %d\n", result);
    return 0;
}

首先分別編譯這兩個文件:gcc -c main.c和gcc -c func.c,生成main.o和func.o。然后進行靜態鏈接:gcc -o main main.o func.o,這里的-o選項指定生成的可執行文件名為main。在這個過程中,鏈接器會將func.o中的add函數代碼直接合并到main可執行文件中。靜態鏈接的優點是可執行文件的運行比較獨立,不依賴外部庫,移植性好;缺點是生成的可執行文件體積較大,因為它包含了所有依賴的庫代碼,如果多個程序都依賴同一個庫,會導致磁盤空間和內存的浪費。

②動態鏈接:動態鏈接則是在程序運行時才加載和鏈接所需的共享庫文件(以.so 為擴展名)。鏈接器在鏈接時并不會將共享庫的代碼直接拷貝到可執行文件中,而是記錄下對共享庫中符號的引用信息。當程序運行時,動態鏈接器(通常是/lib/ld-linux.so.2等)會根據這些引用信息,在系統中找到對應的共享庫文件,并將其加載到內存中,然后完成符號的解析和重定位,使程序能夠正確地調用共享庫中的函數。

繼續以上面的例子,如果add函數是在一個共享庫libfunc.so中定義的,我們可以這樣進行動態鏈接:首先編譯生成共享庫gcc -shared -fPIC -o libfunc.so func.c,其中-shared表示生成共享庫,-fPIC表示生成位置無關代碼(Position - Independent Code),這是共享庫所必需的。然后編譯main.c并鏈接共享庫gcc -o main main.o -L. -lfunc,這里-L.表示在當前目錄查找庫文件,-lfunc表示鏈接名為libfunc.so的共享庫。動態鏈接的優點是節省磁盤空間和內存,多個程序可以共享同一個共享庫;缺點是程序的運行依賴于共享庫,如果共享庫的版本不兼容或者缺失,可能會導致程序運行出錯。

2.2Linux 進程的創建

當 ELF 文件準備就緒后,它還需要經歷從磁盤到內存,從靜態文件到動態進程的轉變,才能真正在系統中運行起來,與我們進行交互。在 Linux 系統中,創建進程最常用的方式是使用fork函數。fork函數是一個系統調用,它的作用是創建一個新的進程,這個新進程被稱為子進程,而調用fork的進程則是父進程。子進程幾乎是父進程的一個副本,它會繼承父進程的大部分資源,如文件描述符、內存空間(通過寫時拷貝技術實現,父子進程在寫入數據前共享同一塊物理內存,當有一方進行寫入操作時才會復制一份物理內存)、信號處理方式等,但也有一些不同,比如進程 ID(PID)不同,父進程 ID(PPID)不同等。fork函數的返回值很特別,在父進程中,它返回子進程的 PID;在子進程中,它返回 0;如果創建進程失敗,它返回一個負數。

下面是一個簡單的示例代碼:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子進程
        printf("I am the child process, my pid is %d, my parent's pid is %d\n", getpid(), getppid());
    } else {
        // 父進程
        printf("I am the parent process, my pid is %d, the child's pid is %d\n", getpid(), pid);
    }
    return 0;
}

在這個例子中,當fork函數被調用后,系統會創建一個子進程。父進程和子進程從fork函數返回后,根據返回值的不同,分別執行不同的代碼塊。這種機制使得我們可以方便地創建多進程程序,實現并發處理任務。例如,在一個 Web 服務器程序中,父進程可以負責監聽端口,接受客戶端連接,然后創建子進程來處理每個客戶端的請求,這樣可以同時處理多個客戶端的并發請求,提高服務器的性能。

2.3 ELF 文件的加載

當我們在終端中輸入命令運行一個 ELF 可執行文件時,系統會啟動一個新的進程,并將 ELF 文件加載到這個進程的內存空間中。具體的加載流程如下:

  1. 創建新進程:首先,通過fork函數創建一個新的子進程。這個子進程繼承了父進程的一些環境信息,如當前工作目錄、用戶 ID 等。
  2. 加載 ELF 文件:子進程調用exec函數族(如execve等)來執行 ELF 文件。exec函數會替換當前進程的代碼段、數據段、堆和棧等,將 ELF 文件的內容加載到內存中。在加載過程中,會讀取 ELF 文件的程序頭表,根據程序頭表中描述的段信息,將各個段(如代碼段、數據段、只讀數據段等)加載到內存的相應位置。例如,程序頭表中會指定代碼段的虛擬地址、文件偏移、大小等信息,加載器會根據這些信息將代碼段從 ELF 文件中讀取到內存中對應的虛擬地址處。
  3. 動態鏈接(如果是動態鏈接的 ELF 文件):如果 ELF 文件是動態鏈接的,在加載過程中還會涉及到動態鏈接的步驟。首先,加載器會根據 ELF 文件中INTERP段指定的路徑,找到動態鏈接器(通常是/lib/ld-linux.so.2等),并將其加載到內存中。然后,動態鏈接器會解析 ELF 文件中的動態鏈接信息,找到并加載程序所依賴的共享庫文件。動態鏈接器會在共享庫文件中查找程序所引用的符號(函數、變量等),并將這些符號的地址解析出來,填充到 ELF 文件的相應位置(通過重定位操作),使得程序能夠正確地調用共享庫中的函數。例如,在前面提到的使用共享庫libfunc.so的例子中,動態鏈接器會在運行時找到libfunc.so,解析其中add函數的地址,并將這個地址填充到main程序中調用add函數的地方,這樣main程序就能夠正確地調用libfunc.so中的add函數了。
  4. 啟動進程:當 ELF 文件和所有依賴的共享庫都加載完成,并且動態鏈接也完成后,系統會將程序的入口點地址(在 ELF 文件頭中指定)設置為進程的執行起始地址,然后開始執行程序的第一條指令,至此,ELF 文件成功地轉變為一個正在運行的 Linux 進程,它可以與系統和用戶進行交互,完成各種任務。

代碼示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

// 簡單的ELF程序: 被加載執行的程序
void create_target_elf() {
    FILE *f = fopen("target_elf.c", "w");
    if (!f) {
        perror("無法創建目標文件");
        return;
    }

    fprintf(f, "#include <stdio.h>\n");
    fprintf(f, "int main() {\n");
    fprintf(f, "    printf(\"這是被加載的ELF程序在運行\\n\");\n");
    fprintf(f, "    printf(\"進程ID: %%d\\n\", getpid());\n");
    fprintf(f, "    return 0;\n");
    fprintf(f, "}\n");
    fclose(f);

    // 編譯生成ELF可執行文件
    system("gcc target_elf.c -o target_elf");
}

int main() {
    // 創建一個簡單的ELF可執行文件
    create_target_elf();

    printf("父進程開始運行, PID: %d\n", getpid());

    // 步驟1: 創建新進程(fork)
    pid_t pid = fork();

    if (pid < 0) {
        // fork失敗
        perror("fork失敗");
        return 1;
    } 
    else if (pid == 0) {
        // 步驟2: 子進程 - 加載并執行ELF文件(exec)
        printf("子進程創建成功, PID: %d\n", getpid());
        printf("子進程開始加載ELF文件...\n");

        // 執行ELF文件, 這會替換當前進程的代碼段、數據段等
        char *args[] = {"./target_elf", NULL};
        if (execvp(args[0], args) == -1) {
            perror("execvp執行失敗");
            exit(EXIT_FAILURE);
        }

        // 注意: execvp成功的話, 下面的代碼不會執行
        printf("這行代碼不會被執行\n");
    } 
    else {
        // 父進程
        printf("等待子進程完成...\n");
        int status;
        waitpid(pid, &status, 0); // 等待子進程結束
        printf("子進程已結束, 父進程退出\n");
    }

    return 0;
}

編譯并運行這個程序,可以看到整個流程的輸出,包括父進程 ID、子進程 ID,以及被加載的 ELF 程序的運行情況。這直觀地展示了從命令輸入到 ELF 文件成為運行中進程的整個過程。

三、ELF 文件與 Linux 進程的深度關聯

3.1ELF 文件對進程的重要性

ELF 文件對于 Linux 進程而言,就如同基石對于高樓大廈,是進程得以存在和正常運行的根本。它為進程提供了運行所需的代碼和數據,其結構信息則像精準的導航圖,指導著進程的創建和內存布局。

從代碼層面來看,ELF 文件中的.text 節區存放著可執行代碼,這些代碼是進程執行各種任務的核心指令集。當進程啟動時,CPU 會從 ELF 文件指定的入口點地址開始,讀取并執行.text 節區中的代碼。例如,一個簡單的 C 程序經過編譯鏈接生成 ELF 可執行文件后,程序中的函數調用、變量操作等邏輯都以機器指令的形式存儲在.text 節區。當這個 ELF 文件被加載為進程運行時,CPU 會按照指令順序依次執行,實現程序的功能,如打印信息、計算數據等。

在數據方面,.data 節區保存了已初始化的全局變量和靜態變量,.bss 節區為未初始化的全局變量和靜態變量預留了內存空間。這些數據對于進程的運行狀態和功能實現至關重要。比如一個記錄用戶登錄信息的全局變量,就可能存儲在.data 節區,進程在運行過程中可以隨時讀取和修改這個變量,以實現用戶認證、權限管理等功能。

ELF 文件的程序頭表和節區頭表在進程創建和內存布局中發揮著關鍵作用。程序頭表中的 LOAD 類型段,詳細描述了如何將 ELF 文件中的段加載到內存中,包括段的文件偏移、虛擬地址、大小、權限等信息。操作系統根據這些信息,將代碼段、數據段等加載到內存的相應位置,并為其分配合適的權限(如代碼段可讀可執行,數據段可讀可寫等),從而構建起進程的內存映像。節區頭表則為鏈接器和調試器提供了重要信息,在進程的創建和調試過程中,幫助相關工具準確地定位和處理各個節區的內容。

3.2進程運行時對 ELF 文件的依賴

當進程處于運行時,它對 ELF 文件的依賴依然緊密,尤其是在動態鏈接共享庫方面。對于動態鏈接的 ELF 文件,進程在運行過程中需要動態加載和鏈接所需的共享庫文件。這一過程是由動態鏈接器負責完成的。

以一個使用了libc.so庫的程序為例,當該程序對應的 ELF 文件被加載為進程后,動態鏈接器會首先根據 ELF 文件中記錄的共享庫依賴信息,在系統中查找并加載libc.so共享庫。在查找共享庫時,動態鏈接器會按照一定的路徑順序進行搜索,通常會先在/lib和/usr/lib等系統默認目錄中查找,如果沒有找到,還會根據環境變量LD_LIBRARY_PATH指定的路徑進行查找。

在共享庫加載完成后,動態鏈接器需要解析共享庫中程序所引用的符號(如函數、變量等),并將這些符號的地址解析出來,填充到 ELF 文件的相應位置,這個過程稱為重定位。而在這個過程中,GOT(Global Offset Table,全局偏移表)起著關鍵作用。GOT 表是 ELF 文件中的一個重要數據結構,用于存儲外部符號的地址。當程序調用一個共享庫中的函數時,程序代碼中并不會直接包含該函數的實際地址,而是通過 GOT 表來間接獲取。

具體來說,當程序第一次調用共享庫中的某個函數時,程序會跳轉到該函數對應的 GOT 表項。如果這是第一次調用,GOT 表項中存儲的可能是一個指向動態鏈接器中重定位函數的地址。動態鏈接器會根據這個重定位函數,找到共享庫中該函數的實際地址,并將其填充到 GOT 表項中。這樣,下次程序再調用該函數時,就可以直接從 GOT 表中獲取函數的實際地址,從而實現快速調用。這種機制實現了函數的延遲綁定,只有在函數第一次被調用時才進行地址解析和綁定,提高了程序的加載效率和靈活性 。例如,在一個圖形渲染程序中,可能會頻繁調用共享庫中的圖形繪制函數,通過 GOT 表的延遲綁定機制,可以在程序啟動時快速加載,而不必在一開始就解析所有圖形繪制函數的地址,只有在真正需要繪制圖形時才進行地址綁定,大大提高了程序的啟動速度和運行效率。

3.3ELF 加載與進程運行

(1)進程地址空間的構建

ELF 文件的加載過程與進程地址空間的構建密切相關。當內核加載 ELF 文件時,會根據 ELF 文件頭和程序頭表的信息,在進程的虛擬地址空間中為各個段分配相應的內存區域 。

進程的虛擬地址空間通常可以分為多個部分,每個部分都有其特定的用途和權限,并且這些部分與 ELF 文件中的段存在著明確的對應關系 :

  • 代碼段(Text Segment):對應 ELF 文件中的.text節,它包含了程序的可執行機器代碼 。在進程地址空間中,代碼段通常被映射到一個只讀且可執行的區域,這樣可以防止程序運行時對代碼段的意外修改,確保代碼的完整性和穩定性 。例如,當我們運行一個 C 語言編寫的程序時,經過編譯和鏈接生成的 ELF 文件中的.text節會被加載到進程地址空間的代碼段區域,CPU 從這里讀取指令并執行 。
  • 數據段(Data Segment):對應 ELF 文件中的.data節,存放已初始化的全局變量和靜態變量 。數據段在進程地址空間中是可讀可寫的,因為程序在運行過程中可能需要修改這些變量的值 。比如在一個程序中定義了一個全局變量int global_var = 10;,這個變量就會被存儲在數據段中,程序運行時可以對global_var進行讀寫操作 。
  • BSS 段(Block Started by Symbol):對應 ELF 文件中沒有實際內容的.bss節,用于存放未初始化的全局變量和靜態變量 。BSS 段在程序運行時會被自動初始化為 0,并且它不占用磁盤空間,只在內存中分配空間 。這是因為在編譯時,未初始化的變量并不需要實際的數據存儲,只需要在運行時為它們分配內存并初始化為 0 即可 。例如,定義一個未初始化的全局變量int uninit_global_var;,它就會被分配到 BSS 段中 。
  • 堆(Heap):用于動態內存分配,比如通過malloc、new等函數分配的內存都來自堆區 。堆區在進程地址空間中是向上增長的,它的大小在程序運行過程中可以動態變化 。例如,在一個程序中使用malloc函數分配內存int *ptr = (int *)malloc(10 * sizeof(int));,這 10 個整數大小的內存空間就是從堆區分配得到的 。
  • 棧(Stack):用于存放函數調用的上下文信息,包括局部變量、函數參數、返回地址等 。棧區在進程地址空間中是向下增長的,每當一個函數被調用時,會在棧頂為其分配棧幀,函數返回時,棧幀被釋放 。比如在一個函數中定義了局部變量int local_var = 5;,這個局部變量就會被存儲在棧區的當前函數棧幀中 。
  • 共享庫映射區域:用于映射動態鏈接庫(共享對象文件) 。當程序依賴于動態鏈接庫時,動態連接器會將這些共享庫加載到該區域 。共享庫映射區域的大小也是動態變化的,并且多個進程可以共享同一個共享庫的內存映射,從而節省內存資源 。例如,很多程序都會依賴 C 標準庫(如libc.so),這個共享庫就會被映射到進程地址空間的共享庫映射區域 。

在內核中,mm_struct和vm_area_struct等數據結構在進程地址空間的構建中起著關鍵作用 。mm_struct結構體代表一個進程的內存管理信息,它包含了指向進程地址空間各個區域的指針,以及與內存管理相關的其他信息 。而vm_area_struct結構體則用于描述進程地址空間中的一個虛擬內存區域(VMA),每個 VMA 都有其起始地址、結束地址、權限標志、所屬的mm_struct等信息 。這些數據結構通過鏈表或紅黑樹等方式組織起來,方便內核進行內存管理和地址空間的操作 ,例如在加載 ELF 文件時,內核會根據 ELF 文件的信息創建相應的vm_area_struct結構體,并將其插入到mm_struct的管理結構中,從而完成進程地址空間的構建 。

(2)程序執行的 “幕后英雄”:頁表與 MMU

當 CPU 執行 ELF 程序時,需要將程序中的虛擬地址轉換為物理地址,這一過程主要依賴于頁表和 MMU(內存管理單元) 。

頁表是一種數據結構,用于實現虛擬地址到物理地址的映射 。它以頁(Page)為單位進行管理,每個頁通常大小為 4KB(在一些系統中也可能是其他大小) 。頁表中存儲了虛擬頁號(VPN)與物理頁號(PPN)的對應關系 。例如,在一個 32 位的系統中,虛擬地址空間為 4GB,若頁大小為 4KB,則虛擬地址空間被劃分為 1048576 個頁 。當程序訪問一個虛擬地址時,CPU 會根據虛擬地址的頁號部分在頁表中查找對應的物理頁號 。

MMU 是 CPU 中的一個硬件單元,它在地址轉換過程中起著至關重要的作用 。當 CPU 執行指令時,會將指令中的虛擬地址發送給 MMU 。MMU 首先會根據 CPU 中的頁表基址寄存器(如 x86 架構中的 CR3 寄存器)找到對應的頁表 ,然后根據虛擬地址的頁號在頁表中查找對應的物理頁號 。如果在頁表中找到了匹配的項(稱為頁表項,PTE),MMU 會將物理頁號與虛擬地址的頁內偏移部分組合起來,形成最終的物理地址,然后將該物理地址發送給內存控制器,以訪問實際的內存數據 。例如,假設虛擬地址為 0x08048000,頁大小為 4KB,虛擬地址的高 20 位表示頁號,低 12 位表示頁內偏移 。MMU 根據頁號在頁表中查找對應的物理頁號,假設找到的物理頁號為 0x10000,那么最終的物理地址就是 0x10000 << 12 | 0x08048000 & 0xFFF = 0x10000000 + 0x8000 = 0x10008000 。

在程序執行過程中,PC(程序計數器)指針不斷推進,指向下一條要執行的指令的虛擬地址 。每當 CPU 執行完一條指令后,PC 指針會自動增加,指向下一條指令 。例如,在一個簡單的匯編程序中,指令mov eax, 1執行完畢后,PC 指針會指向下一條指令的地址,CPU 會根據 PC 指針的值從內存中讀取下一條指令的虛擬地址,并通過 MMU 將其轉換為物理地址,從而繼續執行程序 。如果程序中包含函數調用、跳轉等指令,PC 指針會根據指令的要求進行相應的修改,跳轉到指定的地址繼續執行 ,以實現程序的邏輯控制和流程跳轉 。

四、內核加載 ELF 文件全流程

4.1內核初次 “邂逅” ELF

當execve系統調用被觸發后,內核就開始了 ELF 文件的加載之旅。首先,內核會讀取 ELF 文件的頭部信息,這一步就像是我們打開一本書先看目錄一樣 。

內核通過open系統調用打開指定的 ELF 文件,并將文件描述符傳遞給后續的處理函數 。在load_elf_binary函數中(位于 Linux 內核源碼的fs/binfmt_elf.c文件中 ),會讀取 ELF 文件的前 128 個字節,這部分內容包含了 ELF 文件頭的關鍵信息 。

struct linux_binprm *bprm;
// 讀取ELF文件頭前128字節到bprm->buf中
if (kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos) != BINPRM_BUF_SIZE) {
    return -EIO;
}

我們可以使用readelf -h命令來查看 ELF 文件頭的關鍵信息,例如對于/bin/ls文件,執行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:               0x403940
  Start of program headers:          64 (bytes into file)
  Start of section headers:          56440 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 27

從這些信息中,內核可以獲取到文件的類型(如可執行文件、共享庫等 )、目標機器架構(如 x86 - 64、ARM 等 )、程序入口地址、程序頭表和節區頭表的偏移量等關鍵內容 。其中,程序入口地址指明了程序執行的起始位置,內核后續會根據這個地址跳轉到程序的起始處開始執行;程序頭表和節區頭表的偏移量則幫助內核找到對應的表,從而進一步獲取程序的段和節區信息 ,這些信息對于內核后續將 ELF 文件正確地加載到內存并運行起著至關重要的指引作用 。

4.2按圖索驥:加載程序段

內核在讀取并解析了 ELF 文件頭后,就會根據程序頭表(Program Header Table)中的信息,將 ELF 文件中的各個段加載到內存中 。程序頭表就像是一份詳細的 “裝載指南”,它描述了每個段在文件中的位置、大小以及被放進內存后所在的位置和大小等信息 。

在程序頭表中,類型為PT_LOAD的段表示需要被加載到內存中的段 。內核會遍歷程序頭表,查找所有類型為PT_LOAD的段,并使用do_mmap函數將這些段的內容映射到進程的虛擬地址空間中 。

struct elf_phdr *elf_ppnt;
elf_ppnt = elf32_getphdr(bprm);
for (i = 0; i < elf_ex.e_phnum; i++, elf_ppnt++) {
    if (elf_ppnt->p_type == PT_LOAD) {
        unsigned long vaddr = elf_ppnt->p_vaddr;
        unsigned long memsz = elf_ppnt->p_memsz;
        unsigned long filesz = elf_ppnt->p_filesz;
        unsigned long offset = elf_ppnt->p_offset;
        int prot = 0;
        if (elf_ppnt->p_flags & PF_R) prot |= PROT_READ;
        if (elf_ppnt->p_flags & PF_W) prot |= PROT_WRITE;
        if (elf_ppnt->p_flags & PF_X) prot |= PROT_EXEC;
        // 使用do_mmap將段映射到內存
        mm->mmap = do_mmap(file, offset, filesz, prot, MAP_PRIVATE, vma->vm_pgoff); 
        if (IS_ERR(mm->mmap)) {
            return PTR_ERR(mm->mmap);
        }
        // 如果內存大小大于文件大小,對多出的內存進行清零初始化
        if (memsz > filesz) {
            vm = vma_merge(mm, vma, vaddr, vaddr + memsz, prot, NULL, NULL, 0); 
            if (!vm) {
                return -ENOMEM;
            }
            vma = vm;
            memset((char *)vma->vm_start + filesz, 0, memsz - filesz);
        }
    }
}

在這個過程中,p_vaddr指定了段在虛擬內存中的起始地址,p_memsz表示段在內存中的大小,p_filesz是段在文件中的大小,p_offset是段在文件中的偏移量 。p_flags則定義了段的權限,如PF_R表示可讀,PF_W表示可寫,PF_X表示可執行 。內核根據這些信息,通過do_mmap函數在進程的虛擬地址空間中為段分配內存,并將段的內容從文件中讀取到分配的內存區域 。如果段在內存中的大小大于在文件中的大小,內核會將多出的內存區域清零初始化 ,以確保內存中的數據是符合程序預期的 。

4.3尋找動態連接器

對于動態鏈接的 ELF 文件,內核在加載完程序段后,還需要找到并加載動態連接器(Dynamic Linker) 。動態連接器在程序運行過程中起著至關重要的作用,它負責解析程序對共享庫的依賴,并將所需的共享庫加載到內存中,同時還處理程序中的符號重定位等工作 。

內核通過分析 ELF 文件中的.interp段來獲取動態連接器的名稱 。.interp段是一個字符串類型的段,它包含了動態連接器的路徑名 。例如,在常見的 x86 - 64 架構的 Linux 系統中,動態連接器的路徑通常是/lib64/ld-linux-x86-64.so.2 。

struct elf_phdr *interp_elf_ppnt;
interp_elf_ppnt = elf32_getphdr(bprm);
for (i = 0; i < elf_ex.e_phnum; i++, interp_elf_ppnt++) {
    if (interp_elf_ppnt->p_type == PT_INTERP) {
        char *interp_path = kmalloc(interp_elf_ppnt->p_filesz, GFP_KERNEL);
        if (!interp_path) {
            return -ENOMEM;
        }
        // 讀取.interp段內容,獲取動態連接器路徑
        if (kernel_read(bprm->file, interp_path, interp_elf_ppnt->p_filesz, &interp_elf_ppnt->p_offset) != interp_elf_ppnt->p_filesz) {
            kfree(interp_path);
            return -EIO;
        }
        // 加載動態連接器
        if (load_elf_interp(interp_path, bprm) < 0) {
            kfree(interp_path);
            return -EINVAL;
        }
        kfree(interp_path);
        break;
    }
}

內核找到動態連接器的路徑后,會使用load_elf_interp函數來加載動態連接器 。這個過程類似于加載普通的 ELF 文件,內核會為動態連接器分配內存空間,并將其代碼和數據加載到內存中 。加載完成后,動態連接器就開始接管程序的后續初始化和運行工作 。

4.4動態鏈接與重定位

動態連接器加載到內存后,會首先檢查程序對共享庫的依賴關系 。它通過解析 ELF 文件中的動態段(Dynamic Section)來獲取程序所依賴的共享庫列表 。動態段中包含了一系列的標記(Tags),其中DT_NEEDED標記用于指定程序所依賴的共享庫名稱 。

動態連接器會根據這些依賴信息,在系統中查找并加載相應的共享庫 。它會按照一定的搜索路徑來查找共享庫,首先會檢查環境變量LD_LIBRARY_PATH指定的目錄,如果沒有找到,則會查找/etc/ld.so.cache中的緩存路徑,最后會查找默認的庫路徑(如/lib和/usr/lib) 。

在加載共享庫的過程中,動態連接器還會進行重定位(Relocation)操作 。由于共享庫在編譯時并不知道它最終會被加載到內存的哪個位置,所以其中的代碼和數據中涉及到的地址都是相對地址 。當共享庫被加載到內存后,動態連接器需要根據其實際的加載地址,對共享庫中的地址引用進行修正,使其指向正確的內存位置,這個過程就是重定位 。

動態連接器通過解析共享庫中的重定位表(Relocation Table)來進行重定位操作 。重定位表中記錄了需要重定位的符號以及對應的重定位類型和偏移量等信息 。例如,對于一個需要重定位的符號,動態連接器會根據重定位表中的信息,找到符號在共享庫中的引用位置,并根據共享庫的加載地址對引用的地址進行修正 。

此外,動態鏈接還具有延遲定位(Lazy Binding)的特性 。在程序開始運行時,動態連接器并不會立即對所有的符號引用進行重定位,而是在第一次使用某個符號時才進行重定位 。這樣可以減少程序啟動時的開銷,提高程序的啟動速度 。例如,一個程序中可能有很多函數調用,在程序啟動時,動態連接器并不會對所有函數調用的符號進行重定位,只有當實際調用某個函數時,才會對該函數的符號引用進行重定位 ,這種延遲定位的機制對于那些包含大量函數調用但在程序啟動時并不需要全部使用的程序來說,能夠顯著提升程序的運行效率 。

4.5程序初始化與啟動

當動態連接器完成共享庫的加載和重定位后,就會執行程序的初始化操作 。它會執行 ELF 文件中.init節的代碼,這個節中的代碼通常用于完成一些程序運行前的初始化工作,比如初始化全局變量、設置信號處理函數等 。

typedef void (*initfn_t)(void);
initfn_t init = (initfn_t)elf_entry;
init();

在完成初始化后,動態連接器會將控制傳遞給程序 。它會根據ELF文件頭中指定的程序入口地址,跳轉到程序的入口點,開始執行程序的代碼 。此時,程序就正式開始運行了,我們在終端輸入的命令也終于得以按照程序的邏輯執行,并返回相應的結果 ,整個 ELF 文件的加載過程也至此全部完成,從最初的磁盤文件,到在內存中被正確加載、鏈接和初始化,最終成為一個能夠在系統中正常運行的進程 。

五、ELF文件案例分析

為了更直觀地理解從 ELF 文件到 Linux 進程的轉化過程,讓我們以一個簡單的 C 程序為例,逐步展示這個神奇的旅程。

5.1編寫與編譯 C 程序

首先,我們編寫一個簡單的 C 程序sum.c,它的功能是計算兩個整數的和并輸出結果:

#include <stdio.h>

int main() {
    int a = 3;
    int b = 5;
    int sum = a + b;
    printf("The sum of %d and %d is %d\n", a, b, sum);
    return 0;
}

使用 GCC 編譯器將其編譯為 ELF 可執行文件:

gcc -o sum sum.c

這條命令會生成一個名為sum的 ELF 可執行文件,它包含了我們程序的代碼和數據,以及 ELF 文件格式所要求的各種頭部和表結構。

5.2分析 ELF 文件結構

接下來,我們使用readelf命令來分析生成的 ELF 文件sum的結構。

(1)查看 ELF 文件頭:使用readelf -h sum命令查看 ELF 文件頭信息

readelf -h sum

輸出結果包含了文件類型(可執行文件)、目標機器架構(如 x86_64)、入口點地址、程序頭表和節區頭表的偏移等重要信息。例如,通過入口點地址,我們可以知道程序從何處開始執行;通過文件類型,我們能明確這是一個可直接運行的可執行文件 。

(2)查看程序頭表:使用readelf -l sum命令查看程序頭表:

readelf -l sum

程序頭表列出了各個段的信息,如 LOAD 類型的段,包含了代碼段和數據段的加載信息,包括它們在文件中的偏移、加載到內存的虛擬地址、大小以及權限等。我們可以看到代碼段具有可讀和可執行權限,數據段具有可讀和可寫權限,這與我們之前對 ELF 文件結構的理解是一致的。

(3)查看節區頭表:使用readelf -S sum命令查看節區頭表:

readelf -S sum

節區頭表展示了各個節區的詳細信息,如.text 節區存放代碼,.data 節區存放已初始化的數據,.bss 節區為未初始化數據預留空間等。我們還可以看到符號表節區.symtab 和字符串表節區.strtab,它們在程序的鏈接和運行過程中起著關鍵作用,符號表記錄了程序中定義和引用的符號信息,字符串表則保存了符號的名字等字符串信息。

5.3跟蹤進程創建與 ELF 文件加載

為了跟蹤從 ELF 文件到 Linux 進程的創建和加載過程,我們使用strace工具。strace是一個強大的系統調用跟蹤工具,可以監視進程執行時與內核的交互,包括文件操作、進程管理、內存分配等。

使用strace運行我們的sum程序:

strace -f -o sum_trace.txt./sum

這里的-f選項表示跟蹤子進程,-o sum_trace.txt表示將跟蹤結果輸出到sum_trace.txt文件中。

在生成的sum_trace.txt文件中,我們可以看到一系列系統調用,其中關鍵的系統調用有:

(1)fork系統調用:在進程創建階段,fork系統調用用于創建一個新的子進程。在strace的輸出中,我們可以看到類似這樣的記錄:

fork() = 23456

這里的23456是新創建子進程的 PID。

(2)execve系統調用:execve系統調用負責執行 ELF 文件,它是進程執行的核心系統調用。在strace的輸出中,我們可以找到類似這樣的記錄:

execve("./sum", ["./sum"], 0x7ffd12d4) = 0

這表示sum程序被執行,execve的第一個參數是 ELF 文件的路徑,第二個參數是傳遞給程序的參數數組,第三個參數是環境變量。

(3)動態鏈接相關的系統調用:如果我們的程序依賴于共享庫(在這個簡單示例中依賴于libc.so庫),在strace的輸出中,我們可以看到與動態鏈接相關的系統調用,如打開共享庫文件、解析符號等操作。例如:

openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

這表示系統嘗試打開libc.so.6共享庫文件,文件描述符為 3。隨后還會有一系列與符號解析、重定位相關的系統調用,這些操作確保了程序能夠正確地調用共享庫中的函數。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2022-10-20 08:02:29

ELFRTOSSymbol

2023-03-11 11:19:07

loopbackSocket

2023-01-27 18:08:35

eBPF云原生

2023-05-08 07:41:07

Linux內核ELF文件

2025-07-14 00:10:01

2014-09-24 11:01:10

多路鏡像流量聚合鏡像流量

2022-08-27 10:53:15

C語言Linux內核

2021-06-26 07:04:24

Epoll服務器機制

2020-06-04 08:36:55

Linux內核線程

2025-09-09 02:11:00

2025-06-04 02:35:00

2023-11-24 11:24:16

Linux系統

2018-10-10 14:02:30

Linux系統硬件內核

2021-07-07 23:38:05

內核IOLinux

2011-08-25 14:10:47

execve中文man

2021-06-18 06:02:24

內核文件傳遞

2025-10-27 01:55:00

2011-12-02 10:58:06

數據結構Java

2025-10-11 04:11:00

2025-11-03 04:00:00

點贊
收藏

51CTO技術棧公眾號

日韩精品在线观| 亚洲一区二区三区小说| 国产精品久久久久福利| 多男操一女视频| 一区二区三区国产好| 黑人精品xxx一区一二区| 亚洲国产一区二区三区在线| 性中国xxx极品hd| 久久午夜精品| 欧美大荫蒂xxx| 久久久久亚洲av成人无码电影| 中文幕av一区二区三区佐山爱| 亚洲国产成人av网| 一区二区视频在线播放| 无码国产色欲xxxx视频| 精品亚洲欧美一区| 日本一区二区三区四区视频| 暗呦丨小u女国产精品| 美女网站一区| 日韩亚洲国产中文字幕欧美| 男人透女人免费视频| 在线视频国产区| 欧美激情综合五月色丁香小说| 亚洲自拍高清视频网站| 最新中文字幕免费| 国产精品夜夜夜| 日韩欧美美女在线观看| 久久精品在线免费观看| 成人av男人的天堂| 亚洲字幕av一区二区三区四区| 亚洲精品韩国| 美女av一区二区三区| 91在线无精精品白丝| 精品成人自拍视频| 精品久久久久久久久久久久包黑料| 三级在线免费看| 丝袜诱惑一区二区| 亚洲第一主播视频| 久久久无码中文字幕久...| 日本视频在线播放| 中文字幕精品在线不卡| 六月婷婷久久| 青青草超碰在线| 99精品国产91久久久久久| 91免费在线观看网站| 国产老女人乱淫免费| 人人超碰91尤物精品国产| 国产成人一区二区在线| 日本一区二区免费电影| 99精品视频免费| 97在线看福利| 日产精品久久久久久久| 亚洲午夜一区| 久久久亚洲影院| 精品无码人妻一区二区三区| 欧美成人亚洲| 欧美精品国产精品日韩精品| 欧美日韩精品亚洲精品| 国内一区二区三区| 久久99亚洲精品| 午夜免费激情视频| 国一区二区在线观看| 欧美激情综合色| 精品少妇theporn| 影音先锋中文字幕一区| 97精品免费视频| 99精品在线播放| 日韩在线一区二区| 国产精品中文字幕久久久| 91女人18毛片水多国产| 国产九色精品成人porny| 91视频免费在线| 肥臀熟女一区二区三区| 99re视频精品| 日韩免费av一区二区三区| 香蕉视频免费在线播放| 一区二区三区不卡在线观看| 亚洲一区二区三区av无码| 国模精品视频| 在线精品观看国产| 中文字幕 欧美日韩| 欧美国产中文高清| 亚洲国产天堂网精品网站| 欧美熟妇激情一区二区三区| 99久久夜色精品国产亚洲狼| 欧美第一黄色网| 久久国产黄色片| 看国产成人h片视频| 91偷拍精品一区二区三区| 日本精品久久久久久| 久久久国产综合精品女国产盗摄| 亚洲一区二区三区午夜| 男人天堂亚洲| 欧美在线色视频| 一个人看的视频www| 亚洲综合小说图片| 久久精品99久久久久久久久| 国产成人亚洲欧洲在线| 蜜桃精品在线观看| 国产精品国色综合久久| 国产美女视频一区二区三区 | 午夜激情在线| 狠狠色狠色综合曰曰| 在线播放av中文字幕| 久久精品国产亚洲blacked| 中文字幕亚洲字幕| 日本午夜小视频| 国内不卡的二区三区中文字幕 | 国产91免费观看| 国产美女主播在线观看| 久久天天做天天爱综合色| 日本不卡一区二区三区四区| 韩国成人动漫| 亚洲成人激情在线| 91麻豆精品成人一区二区| 一本色道88久久加勒比精品| 91久久久精品| 国产对白叫床清晰在线播放| 亚洲国产精品久久艾草纯爱| 日韩高清在线一区二区| 亚洲人成伊人成综合图片| 九九热精品视频国产| 中文字幕乱码视频| 久久久久久免费网| 欧美亚洲精品一区二区| 91成人短视频| 欧美巨乳美女视频| 在线免费观看一区二区| 国产午夜精品在线观看| 国产91xxx| 国产精品香蕉| 欧美激情网站在线观看| 国产精品一区二区免费视频 | 亚洲色婷婷一区二区三区| 日韩综合在线视频| 欧美激情国产日韩| 涩涩视频在线免费看| 欧美videofree性高清杂交| 我要看黄色一级片| 激情六月婷婷久久| 亚洲欧洲免费无码| 欧美男男gaygay1069| 最近2019中文字幕在线高清| 无码人妻丰满熟妇区五十路 | 性欧美videos| 精品一区二区三区欧美| 亚洲一区二区三区四区中文| 成人精品国产亚洲| 在线视频欧美性高潮| 波多野结衣二区三区| 欧美激情一二三区| 簧片在线免费看| 99久久99久久精品国产片桃花| 国产精品香蕉在线观看| 欧美成人精品一区二区男人看| 欧美日韩免费一区二区三区| 日本伦理一区二区三区| 狠狠色丁香婷综合久久| 99精品一区二区三区的区别| 久久伊人影院| 国产69精品久久久久99| 三级视频网站在线| 91成人在线免费观看| 国产一区二区三区四区在线| 久久se这里有精品| av一区二区三区免费观看| 麻豆一区二区麻豆免费观看| 55夜色66夜色国产精品视频 | 黄色欧美在线| 日韩美女在线看| 91在线直播| 欧美一区二区免费观在线| 国产极品美女高潮无套嗷嗷叫酒店| 成人免费视频国产在线观看| 色综合av综合无码综合网站| 欧美一区二区三| 亚洲a中文字幕| bl在线肉h视频大尺度| 亚洲色在线视频| 国产又大又黄又爽| 亚洲第一福利一区| 免费观看a级片| 国产精品主播直播| 青青青在线播放| 图片区亚洲欧美小说区| 国产日本一区二区三区| 国产成人精品一区二三区在线观看 | 欧美日韩一区二区三区 | 99久久婷婷这里只有精品| 成人欧美一区二区三区视频| 国产精品专区免费| 欧美精品一区三区| 国产主播福利在线| 精品少妇一区二区三区日产乱码| 中文字幕在线播| 成人av影音| 精品国产1区二区| 一级特黄免费视频| 亚洲一区电影777| 在线观看亚洲大片短视频| 国产成人免费网站| 亚洲福利精品视频| 国产日韩1区| 精品少妇人妻av一区二区| 亚洲人成网www| 国产精品一区二区免费看| 成人国产一区| 国产69久久精品成人| av片在线观看| 一区二区三区视频免费| 天堂av手机版| 欧美一区二区视频观看视频| 波多野结衣视频在线观看| 午夜日韩在线电影| 国产精品 欧美激情| 国产欧美综合色| 三级男人添奶爽爽爽视频| 国产精品一区免费视频| 久久撸在线视频| 美女视频一区免费观看| 日本黄色片一级片| 99免费精品| 亚洲一区二区三区精品动漫| 国产精品入口久久| 精品日产一区2区三区黄免费| 久久国产精品美女| 91精品国产综合久久香蕉最新版| 欧美韩国亚洲| 欧美中文字幕第一页| 韩国日本一区| 欧美日韩国产成人在线| av软件在线观看| 欧美美最猛性xxxxxx| 韩国av网站在线| 日韩专区在线观看| 亚洲1卡2卡3卡4卡乱码精品| 中文字幕免费国产精品| www.亚洲资源| 自拍偷拍亚洲一区| 大胆av不用播放器在线播放 | 欧美色电影在线| 波多野结衣毛片| 欧美在线free| 做爰无遮挡三级| 欧美日韩一区二区三区四区| 中文字幕+乱码+中文字幕明步 | 亚洲国产av一区二区| 91精品国产日韩91久久久久久| 一区二区小视频| 欧美日韩www| 国产理论视频在线观看| 91精品国产麻豆国产自产在线| 一级黄色片在线| 欧美肥胖老妇做爰| 国产视频一区二区三| 日韩一区二区三免费高清| 国产精品欧美亚洲| 日韩一区二区在线播放| 亚洲黄色小说网| 亚洲高清一二三区| 欧美成熟毛茸茸| 在线视频欧美日韩精品| 免费观看成人高潮| 欧美成人一区在线| 黄在线观看免费网站ktv| 欧美一级视频免费在线观看| av在线一区不卡| 91精品久久久久久久| www.神马久久| 欧美日韩综合另类| 亚洲国产精品成人| 和岳每晚弄的高潮嗷嗷叫视频| 久久精品麻豆| 粗暴91大变态调教| 国产在线播放一区三区四| 国产污在线观看| 久久精品亚洲麻豆av一区二区| 人人干在线观看| 精品国产乱码久久久久久虫虫漫画| 一级做a爰片久久毛片| 欧美日韩不卡在线| 天堂中文在线看| 日韩在线视频免费观看高清中文| av在线播放国产| 欧美美女15p| 欧美日韩大片| 97超碰人人模人人爽人人看| 中日韩免视频上线全都免费| 小说区视频区图片区| 国产一区二区三区久久久久久久久| 久久婷五月综合| 成人午夜视频网站| 久久国产柳州莫菁门| 一区二区高清免费观看影视大全| 男人天堂2024| 日韩欧美一区电影| av每日在线更新| 97久久精品在线| 精品视频一区二区三区| 日本成人三级电影网站| 精品成人久久| 亚洲激情在线看| 久久久美女毛片| 麻豆成人在线视频| 色94色欧美sute亚洲13| 丰满岳乱妇国产精品一区| 色噜噜亚洲精品中文字幕| 欧美巨大丰满猛性社交| 91久久国产综合久久蜜月精品| 国产一区二区在线| 免费av观看网址| 国产成+人+日韩+欧美+亚洲| 国产一二三av| 色先锋aa成人| 无码精品一区二区三区在线| 久热99视频在线观看| av在线一区不卡| 日韩av不卡在线播放| 国产亚洲精品bv在线观看| 亚洲一级片免费观看| 国产精品高清亚洲| 欧美日韩在线视频播放| 日韩精品视频免费专区在线播放 | 日本亚洲欧美三级| 欧美激情影院| 日本午夜激情视频| 国产成人精品亚洲午夜麻豆| 2025国产精品自拍| 欧美片网站yy| 欧美激情二区| 蜜桃视频在线观看一区| 国产又爽又黄无码无遮挡在线观看| 亚洲综合视频在线| 精品久久人妻av中文字幕| 久久综合国产精品台湾中文娱乐网| 国产成人免费| 亚洲精品国产系列| 美女视频一区二区三区| 亚洲欧美日韩第一页| 欧美日韩视频在线第一区| 国产视频福利在线| 国产精品国产自产拍高清av水多 | 男人天堂2024| 亚洲日韩欧美视频| 日本欧美一区| 亚洲精品一区二区三区四区五区| 奇米精品一区二区三区四区| 大吊一区二区三区| 欧美妇女性影城| 成人午夜在线影视| 91情侣在线视频| 亚洲人www| av在线网站观看| 日本国产一区二区| 香蕉视频网站在线观看| 91福利视频导航| 欧美午夜久久| 欧美高清性xxxx| 在线观看免费亚洲| 免费av网站在线观看| 99re视频| 国产精品一国产精品k频道56| 国产人妻大战黑人20p| 欧美日韩精品一区视频| av网站在线免费看推荐| 国产精品久久久久av福利动漫| 亚洲一区二区三区四区五区午夜 | 99久久亚洲精品| 性鲍视频在线观看| 亚洲成av人片| 高清av在线| 99一区二区| 亚洲永久视频| 任我爽在线视频| 欧美精品一区男女天堂| 你懂得影院夜精品a| 成人在线观看www| 99视频一区二区三区| 这里只有精品999| 欧美国产日韩一区| 精品一区免费| 能看毛片的网站| 色偷偷久久一区二区三区| 麻豆免费在线观看| 精品一区二区三区国产| 美日韩一级片在线观看| 精品少妇久久久| 日韩中文在线中文网在线观看| 中文字幕区一区二区三| 欧美一级片中文字幕| 亚洲黄网站在线观看| 免费黄色片在线观看| 成人h在线播放| 蜜桃精品视频在线观看| 日韩手机在线观看| 久久躁狠狠躁夜夜爽| 少妇精品久久久一区二区| 免费黄色在线播放| 欧美丝袜丝交足nylons图片| 超碰激情在线|