首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> 關於C

C 程序運行背後(二):可執行文件加載、進程創建與用戶棧構建全解析

     編輯:關於C

前言

上篇介紹了進程地址空間佈局,本篇聚焦 從輸入 ./program 到 main 執行前 內核完成的所有工作。

一、Shell 創建子進程

  • fork():複製父進程地址空間、文件描述符表、信號處理等
  • 子進程繼承父進程環境,若不調用 exec 將繼續運行父進程代碼

二、execve 系統調用進入內核

int execve(const char *pathname, char *const argv[], char *const envp[]);
/* 內核入口 */
int do_execve(const char *pathname, char *const argv[], char *const envp[],
              struct pt_regs *regs);
  • pt_regs:保存用戶態寄存器(EIP, ESP, EAX...),因 Linux 系統調用用寄存器傳參,進入內核前須保存
  • regs 後續用於構建用戶棧、設置返回地址

三、linux_binprm:可執行文件元數據載體

struct linux_binprm {
    char buf[BINPRM_BUF_SIZE];       // 文件頭 128 字節(魔數、架構判斷)
    struct page *page[MAX_ARG_PAGES]; // 參數/環境變量字符串頁
    struct mm_struct *mm;             // 新進程內存描述符
    unsigned long p;                  // 當前用戶棧頂
    struct file *file;                // 可執行文件 struct file
    int argc, envc;                   // 參數/環境變量個數
    char *filename, *interp;          // 文件名/解釋器名
    /* ... capability, security ... */
};

do_execve 填充此結構,後續格式處理器使用。

四、格式識別與加載:search_binary_handler → load_elf_binary

  1. 讀取 buf 中 ELF 魔數 0x7F 'E' 'L' 'F'
  2. 調用 load_elf_binary()
  3. 關鍵動作:
    • 釋放舊地址空間(flush_old_exec)
    • 按 Program Header 映射段(mmap):.text .rodata .data .bss
    • 設置用戶棧:setup_arg_pages 映射參數/環境變量頁 → create_elf_tables 壓入 argc, argv, envp, auxv

五、用戶棧最終佈局

高地址
+------------------------+
| 環境變量字符串          |
+------------------------+
| argv 字符串             |
+------------------------+
| auxv (AT_* 向量)        |
+------------------------+
| envp[] 指針數組         |  <-- envp
+------------------------+
| NULL                    |
+------------------------+
| argv[] 指針數組         |  <-- argv
+------------------------+
| argc                    |  <-- 栈底對齊處
+------------------------+
| ... 對齊填充 ...        |
低地址 (初始 ESP)

六、start_thread:進入用戶態的最後一躍

#define start_thread(regs, new_eip, new_esp) do { \
    set_fs(USER_DS); \
    regs->xds = regs->xes = regs->xss = USER_DS; \
    regs->xcs = USER_CS; \
    regs->eip = new_eip;   /* ELF 入口點 _start */ \
    regs->esp = new_esp;   /* 用戶棧頂 */ \
} while (0)
  • new_eip = elf_entry(通常是 _start,C 運行時啟動代碼)
  • new_esp = bprm->p(棧頂)
  • 執行 iret 返回用戶態,CPU 從 _start 開始執行

七、_start → __libc_start_main → main

_start:
    xor ebp, ebp          # 標記幀鏈結束
    pop esi               # argc
    mov ecx, esp          # argv
    and esp, -16          # 16 字節對齊
    push eax              # glibc 內部用
    push esp              # auxv
    push edx              # envp
    call __libc_start_main

__libc_start_main 執行:初始化 libc、調用構造函數、註冊析構函數、最後 exit(main(argc,argv,envp))。

八、總結流程圖

./program
   │
   ▼
fork() → 子進程
   │
   ▼
execve() → do_execve()
   │
   ▼
load_binprm() 填充 linux_binprm
   │
   ▼
search_binary_handler() → load_elf_binary()
   │                    ├─ 釋放舊地址空間
   │                    ├─ mmap 映射 ELF 段
   │                    └─ 構建用戶棧 (argc/argv/envp/auxv)
   │
   ▼
start_thread(regs, elf_entry, stack_top)
   │
   ▼
iret → 用戶態 _start
   │
   ▼
__libc_start_main → main()

九、調試驗證

# 靜態分析
readelf -h program     # ELF 頭、入口點
readelf -l program     # Program Headers、加載地址

# 動態追蹤
strace -f ./program    # 觀察 execve、mmap、brk
gdb ./program
(gdb) break _start
(gdb) run
(gdb) info registers
(gdb) x/20x $esp       # 觀察初始棧 argc/argv

總結

可執行文件加載是內核與動態鏈接器協作的精密過程:格式識別 → 內存映射 → 棧構建 → 上下文切換。理解此流程,對逆向分析、啟動優化、容器技術、嵌入式引導開發均有核心價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved