当前位置: 首页 > news >正文

Linux ARM 程序启动全链路解析:从 shell 到 main(含动态/静态链接)

Linux ARM 程序启动全链路解析:从 shell 到 main(含动态/静态链接)

系统框图总览

[Shell] --execve--> [内核 fs/exec.c:do_execveat_common]├─(脚本) fs/binfmt_script.c:load_script│       --bprm_change_interp--> [/bin/bash | /usr/bin/env bash]└─(ELF)   fs/binfmt_elf.c:load_elf_binary├─ load_elf_phdrs(顺序读少量页)├─ PT_LOAD → 建 vma(按需映射)├─ PT_INTERP → 映射 ld.so└─ create_elf_tables → 栈+auxv
[ARM do_page_fault] arch/arm(/arm64)/mm/fault.c├─ 文件页:filemap_fault(页缓存命中快;冷启动块I/O慢)├─ 匿名页:do_anonymous_page(BSS/TLS/栈)└─ COW   :do_wp_page(写时复制)
[解释器 ld.so]├─ 解析 DT_NEEDED → 打开 .so├─ REL/RELA 重定位 + GOT/PLT(懒绑定可延后)├─ TLS 初始化└─ __libc_start_main├─ 运行 PREINIT/INIT_ARRAY└─ 跳转 main()
  • 热点路径:filemap_fault(文件页)、do_anonymous_page(BSS/TLS/栈)、do_wp_page(COW)、ld.so 重定位与懒绑定、__libc_start_main 进入 main 前构造函数。

全局总览(一屏看懂)

  • 输入:shell 触发 execve(脚本或 ELF)。
  • 内核:识别格式 → 若脚本经 binfmt_script 切到解释器;若 ELF 经 binfmt_elf 映射段,存在 PT_INTERP 则先映射 ld.so
  • 内存:建立 VMAPT_LOAD 按需映射),不立即把所有字节读入;首次访问由缺页(do_page_fault)驱动完成。
  • 动态链接:ld.so 打开依赖 .so,解析/重定位(REL/RELAGOT/PLTTLS),再调用 __libc_start_main
  • 进入 main 前:构建用户栈与 auxv、执行构造函数(.init_array),随后才进入 main
  • 耗时关键:冷启动的文件页缺页(块 I/O)、动态链接的重定位与符号查找、首次 PLT 懒绑定。

适配 Linux 4.4.94 内核,以 ARM32/ARM64 平台为例,聚焦:

  • 时间消耗关键路径(I/O、动态链接、缺页、重定位、符号解析)
  • 各段如何被加载(ELF 头、PT_LOAD 段、符号/重定位、BSS/TLS、解释器)
  • 可执行文件大小与启动耗时关系(顺序读、按需缺页、缓存命中)
  • page fault 行为(文本页/数据页、匿名零页、写时复制、文件映射)

参考关键源码:

  • fs/exec.c: do_execveat_common, bprm_change_interp, create_arg_pages, setup_arg_pages
  • fs/binfmt_elf.c: load_elf_binary, load_elf_phdrs, elf_map, create_elf_tables, setup_arg_pages
  • fs/binfmt_script.c: load_script
  • include/linux/binfmts.h: linux_binprm
  • ARM 缺页入口:arch/arm/mm/fault.c / arch/arm64/mm/fault.c
  • 通用缺页处理:mm/memory.c: handle_mm_fault, __handle_mm_fault, handle_pte_fault, do_fault*, filemap_faultmm/filemap.c

1. 从 shell 到内核 exec 路径(含 shebang 与 ELF 解释器)

  • shell 解析脚本行,命令解析 → 若目标为脚本且首行 #! /path/to/interpreter:用户态调用 execve() 进入内核。
  • fs/exec.c::do_execveat_common:复制参数/环境,填充 linux_binprm,检测魔数/格式。
  • 若为脚本:fs/binfmt_script.c::load_script 解析 shebang,移除旧 argv[0](脚本路径),然后按顺序装入:脚本路径、可选的“单一整串参数”、解释器路径,并通过 bprm_change_interp() 将执行目标切到解释器(如 /bin/bash/usr/bin/env bash)。最终用户态看到的 argv 形如:
    • 无参数:[解释器, 脚本路径, 原脚本其余参数...]
    • 有参数(整串):[解释器, 额外参数, 脚本路径, 原脚本其余参数...]
    • 示例:
      • #!/bin/bash → 解释器 /bin/bashargv[bash, 脚本, ...]
      • #!/usr/bin/env bash → 解释器 /usr/bin/env,额外参数作为“单一整串”"bash"argv[env, "bash", 脚本, ...]
      • #!/usr/bin/env -S bash -O extglob → 内核仍将 "-S bash -O extglob" 作为单一参数推入 argv[env, "-S bash -O extglob", 脚本, ...];后续由 env 在用户态按 -S 规则拆分为 bash -O extglob 并转发。
  • 若为 ELF:fs/binfmt_elf.c::load_elf_binary 解析 ELF 头、Program Headers,若存在 PT_INTERP,则设置解释器为动态链接器(ARM32: /lib/ld-linux.so.3、ARM64: /lib/ld-linux-aarch64.so.1)。随后内核先映射解释器,再映射主程序。

时间关注点:

  • 脚本场景:额外打开脚本与解释器二进制,I/O 次数增加;解释器自身再 execve/dlopen 等加载库。
  • ELF 场景:有 PT_INTERP 时会先加载动态链接器(ld.so),再由其解析依赖库;无 PT_INTERP(静态链接)则直接进入程序入口 _start

2. 段映射与内存镜像构建(按需映射,而非一次性读入)

ELF 映射核心(细化实现与对齐规则):

  • fs/binfmt_elf.c::load_elf_phdrs 顺序读取 Program Header 表(e_phoff/e_phnum/e_phentsize),I/O 连续且很少,通常命中页缓存后几乎不耗时。
  • fs/binfmt_elf.c::elf_map 最终调用 do_mmap_pgoffMAP_FIXED|MAP_PRIVATE 建立 vmap_vaddrp_offset 会按页大小向下对齐,p_align 由链接器保证,权限来自 p_flags → PROT_{R,W,X},对应 vm_flags(如 VM_EXEC/VM_MAYWRITE)。
  • 代码段(RX):文件私有映射,不可写;首次执行/读取触发缺页,经页缓存按需读取文本页。
  • 数据段(RW):p_filesz 部分为文件私有映射(已初始化数据);p_memsz - p_filesz 的部分为匿名零填充(BSS)。
    • 末页零填充:内核使用 padzero 把“已初始化数据的最后一页中未覆盖的尾部”清零,避免残留旧字节。
    • BSS 区域扩展:通过 set_brk(elf_bss, elf_brk)/vm_brk 创建匿名 vma;这些页在首次写入时由 do_anonymous_page 分配物理页。
  • PT_TLS:动态链接器读取 TLS 模板(p_vaddr/p_filesz/p_memsz),为每线程复制 p_filesz 并零填 p_memsz - p_filesz;ARM32 的线程指针寄存器为 tp(实现相关),ARM64 使用 TPIDR_EL0 记录线程本地基址。
  • 解释器(ld.so):若主程序包含 PT_INTERP,内核先以同样方式映射解释器(得到 AT_BASE),再映射主程序;解释器进入用户态继续加载依赖库。

符号/重定位(ARM/ARM64 关键点):

  • 静态链接:链接时地址已决议;运行时无符号查找,无 PLT/GOT 解析路径;首屏主要是缺页与 BSS 分配。
  • 动态链接:解释器解析 DT_NEEDED 并按路径顺序查找依赖(LD_LIBRARY_PATHRPATH/RUNPATHld.so.cache → 默认路径)。
    • 表项解析:读取 DT_SYMTAB/DT_STRTAB(符号/字符串表),DT_REL/DT_RELA(非跳转重定位),DT_PLTREL/DT_JMPREL(PLT 重定位)。
    • 常见重定位:
      • ARM32:R_ARM_RELATIVE(大量,相对地址修正)、R_ARM_GLOB_DAT/R_ARM_JUMP_SLOT(全局数据/PLT)、R_ARM_ABS32R_ARM_COPY(拷贝符号)。
      • ARM64:R_AARCH64_RELATIVER_AARCH64_GLOB_DAT/R_AARCH64_JUMP_SLOTR_AARCH64_ABS64 等。
    • PLT 懒绑定:首次调用时通过解析器 dl_runtime_resolve 完成符号查找并写入 GOT;设置 LD_BIND_NOW=1 或链接器选项 -z now 可改为启动期一次性绑定(更快的首次调用,启动更久)。
    • RELRO:若启用 -z relro/-z now,部分节在重定位后设为只读以提高安全性,但会增加启动写入阶段与页保护切换。

补充:TLS 与构造函数

  • TLS 模型:initial-execglobal-dynamic 等模型影响是否在启动期或首次访问时开销更高(目录表查找、DTV 更新)。
  • 构造函数:解释器在调用 __libc_start_main 前运行依赖库与主程序的 .init_array,这些代码与其引用的数据会触发相应文本/数据页缺页。

时间关注点(更细化):

  • Program Header I/O:通常 1–2 页顺序读;冷启动下仍很快,非瓶颈。
  • 文本/数据页缺页:首次执行/读取触发 filemap_fault;页缓存命中为“次/微秒级”,冷启动需块 I/O(毫秒级),形成启动主耗时。
  • 重定位:RELATIVE 数量巨大时造成 CPU 密集访存并触发对符号/字符串表页的缺页;GLOB_DAT/JUMP_SLOT 受符号个数与查找成本影响。
  • 懒绑定:把部分成本延后到第一次调用;如果首屏立刻调用大量外部符号,热点会集中在解析器与哈希查找(GNU_HASH)上。
  • BSS/TLS/栈:匿名页分配走内存分配与 COW 路径,无磁盘 I/O,但在内存压力下可能受分配延迟影响。

[Shell] --execve--> [fs/exec.c:do_execveat_common]├─(脚本) [fs/binfmt_script.c:load_script]│       --bprm_change_interp--> [/bin/bash]│       (I/O: 读取脚本 + 解释器ELF)└─(ELF)   [fs/binfmt_elf.c:load_elf_binary]├─ load_elf_phdrs (顺序读I/O,受缓存影响)├─ PT_LOAD → elf_map/vm_mmap (建 vma,不读入数据)├─ PT_INTERP → 映射 ld.so (I/O)└─ create_elf_tables → 用户栈+auxv (轻量)[ARM do_page_fault] arch/arm(/arm64)/mm/fault.c└─ handle_mm_fault → __handle_mm_fault → handle_pte_fault (mm/memory.c)├─ 文件页:filemap_fault (mm/filemap.c)│            → 页缓存命中(快) / 冷启动块I/O(慢)├─ 匿名零页:do_anonymous_page (分配物理页)└─ COW:do_wp_page (复制页,带宽敏感)[解释器 ld.so]├─ 解析 DT_NEEDED → 打开 .so (I/O)├─ REL/RELA 重定位 (CPU + 可能触发缺页)├─ GOT/PLT 构建;懒绑定首调开销 (可用 LD_BIND_NOW 改为启动期)├─ TLS 初始化 (每线程数据区)└─ __libc_start_main(main, ...)[程序]├─ 运行构造函数 .init_array (可能触发文件页缺页)└─ 进入 main() (首屏路径涉及代码/数据页触发)

7. 关键源码定位

  • fs/exec.c: do_execveat_common, bprm_change_interp, setup_new_exec, create_arg_pages, setup_arg_pages
  • fs/binfmt_elf.c: load_elf_binary, load_elf_phdrs, elf_map, create_elf_tables
  • fs/binfmt_script.c: load_script
  • mm/memory.c: handle_mm_fault, __handle_mm_fault, handle_pte_fault, do_fault*
  • mm/filemap.c: filemap_fault
  • arch/arm/mm/fault.c, arch/arm64/mm/fault.c: do_page_fault, __do_page_fault
  • include/linux/binfmts.h: linux_binprm

8. 性能诊断建议(实操)

  • 统计缺页与 I/O:perf stat -e page-faults,major-faults,minor-faults -e task-clockperf record/perf report 观察 do_page_fault/filemap_fault 热点。
  • 观测动态链接耗时:设置 LD_DEBUG=libs,reloc,statistics,查看库解析与重定位统计。
  • 预热页缓存:在启动前 cat/readahead 可执行文件与关键 .so,或运行一次以填充缓存。
  • 减少文本段首次缺页:将热点初始化代码体积控制在少量页内,避免零散访问。

9. FAQ 速查

  • “函数符号如何加载?”:符号表本身不映射为运行时必需数据;动态链接在解析时读取 DT_SYMTAB/DT_STRTAB 所在页,完成后主要通过 GOT/PLT 间接访问函数入口;静态链接则已在链接期决议,无运行时符号解析。
  • “静态变量在哪里?”:位于数据段(已初始化)或 BSS(未初始化,零页供给),在 PT_LOAD 的 RW 映射中。
  • “文件大是否一定慢?”:决定启动耗时的是“首次实际访问到的页数”和“动态链接工作量”,不是总文件大小;热缓存命中时差异更小。
  • “脚本触发与直接 ELF 执行差别?”:脚本会先切到解释器,再由解释器执行命令/再触发新的 execve;相较直接 ELF,多一次解释器加载与其自身初始化开销。

6A. 实际案例:ARM 上从脚本触发一个动态链接程序

  • 准备示例:
    • 脚本 run_app.sh
      #!/usr/bin/env bash
      ./app-dyn
      
    • 程序 app-dyn:动态链接构建(示例,C 源码)
      • 源码 app.c
        #include <stdio.h>
        #include <math.h>
        extern void foo_init(void); // 来自自定义共享库,可选
        int main(void) {foo_init(); // 若未提供 libfoo.so,此调用可注释double x = 0.5;printf("cos(%f)=%f\n", x, cos(x));return 0;
        }
        
      • 编译:arm-linux-gnueabihf-gcc -O2 app.c -o app-dyn -Wl,--as-needed -lm(如需 libfoo.so:在同目录放置并 -lfoo -L.
      • 依赖:libc.so, libm.so,可选 libfoo.so
  • 运行:chmod +x run_app.sh && ./run_app.sh
  • 观察:
    • strace -f -o trace.txt ./run_app.sh
    • perf stat -e page-faults,major-faults,minor-faults -e task-clock ./run_app.sh
    • (可选)LD_DEBUG=libs,reloc,statistics ./run_app.sh 查看动态链接统计

流程图(简化):

run_app.sh --execve--> do_execveat_common└─ load_script → bprm_change_interp → /usr/bin/env bash└─ bash execve ./app-dyn → load_elf_binary├─ load_elf_phdrs(顺序读)├─ PT_INTERP → 映射 ld.so├─ PT_LOAD → 建 vma(按需)└─ create_elf_tables → 栈+auxvARM do_page_fault → handle_mm_fault├─ filemap_fault:.text/.rodata/.data 的文件页按需载入(冷启动块I/O)├─ do_anonymous_page:BSS/TLS/栈分配(零页,无磁盘I/O)└─ do_wp_page:首次写 .data 触发 COW(页复制)ld.so:解析 DT_NEEDED,REL/RELA,GOT/PLT,TLS → __libc_start_main程序:.init_array → main

首屏耗时定位建议:

  • 代码页首访:查看 perf reportdo_page_fault/filemap_fault 的比例;冷启动下占比高。
  • 动态链接重定位:LD_DEBUG=statistics 查看 relocations 数量与 time;符号多、库多时显著。
  • 懒绑定:首次调用某些函数时看到 ld.so 相关符号查找热点。

小节补充说明:文本/数据页缺页 vs 匿名页缺页

  • 文本页缺页(.text):PT_LOAD 的 RX 文件私有映射,CPU取指首次命中未映射页触发;处理路径 filemap_fault,页缓存未命中需块 I/O(major fault)。
  • 数据页缺页(.rodata/.data 的文件部分):PT_LOAD 的 R/RW 文件私有映射,首次读取触发;处理路径同 filemap_fault。对 .data 的首次写入会走 do_wp_page 将该页复制为匿名页(COW)。
  • 匿名页缺页(BSS/TLS/栈):不从文件读,首次访问经 do_anonymous_page 分配零页;无磁盘 I/O,但存在内存分配与可能的COW成本。
  • 末页与 BSS:padzero 清理已初始化数据末页未覆盖尾部;set_brk/vm_brk 扩展 BSS 的匿名 vma,首写分配。

小节补充说明:进入 main 前执行与耗时来源

  • 执行顺序(动态链接):
    • 解释器 ld.so 完成依赖映射与重定位后,先运行“共享库的 .init_array/DT_INIT”与主程序的 .preinit_array;随后调用 __libc_start_main
    • __libc_start_main 在进入 main 之前,运行主程序的 .init_array/DT_INIT(以及注册的构造函数),最后跳转 main
  • 执行顺序(静态链接):
    • 由启动例程直接调用 __libc_start_main,在进入 main 之前运行主程序的 .init_array/构造函数。
  • 耗时构成:
    • 文件页缺页:构造函数代码首次执行引发文本页缺;访问只读常量或已初始化数据引发数据页缺。冷启动下,major fault(块I/O)为主耗时。
    • 动态链接工作:REL/RELA 重定位(尤其大量 RELATIVE)、符号查找(GLOB_DAT/JUMP_SLOT),以及首次 PLT 懒绑定会在进入 main 前或构造阶段发生。
    • 匿名页分配:BSS/TLS/栈 的首写分配与 COW 复制产生内存开销,非磁盘 I/O,但在内存压力下会变慢。
  • 来自可执行/库的具体来源:
    • 可执行文件:其 .init_array/.text/.rodata/.data;若 PT_INTERP 存在,解释器先行映射与执行其自身初始化。
    • 依赖库:各库的 .text/.rodata/.data.init_array;库越多、构造函数越多,启动前的缺页与重定位开销越高。
  • 观测建议:
    • perf stat -e page-faults,major-faults,minor-faults -e task-clock 量化缺页与CPU时间;perf report 查看 filemap_fault/do_page_fault/ld.so 热点。
    • LD_DEBUG=libs,reloc,statistics 统计库加载与重定位耗时;readelf -W -a 查看 INIT/INIT_ARRAY/PREINIT_ARRAY
http://www.dtcms.com/a/498860.html

相关文章:

  • 具身智能黑客松之旅002
  • 免费发布产品网站网站权重能带来什么作用
  • 碰一碰发视频 系统源码 /PHP 语言开发方案
  • 网站大学报名官网入口网站插件代码下载
  • Cors能干什么?为什么需要它?
  • 远程办公自由:rdesktop+cpolar让Windows桌面随身而行
  • 计算机网络(tcp_socket )
  • 【小白笔记】在编程中,如何将概念上的数据结构(比如“树”)转化为代码中具体的数据类型和对象
  • 【STM32项目开源】STM32单片机智能农业大棚控制系统
  • github开源笔记应用程序项目推荐-Joplin
  • 【Swift】LeetCode 438. 找到字符串中所有字母异位词
  • 【SoC】【W800】基于WM IoT SDK的环境搭建
  • BFS 与 DFS——力扣102.二叉树的层序遍历
  • 使用IOT-Tree的OPC UA Client连接器对接OPC UA Server获取数据到系统中
  • 优质网站建设在哪里wordpress分类目录名称
  • 专题一 之 【双指针】
  • 将Windows应用上架至Microsoft Store
  • 对LlamaFactory的一点见解
  • 紫金保险车险官方网站关键词优化营销
  • 大模型-智能体-【篇一:单智能体框架】
  • LLMs之MultiAgent:OpenAgents(创建AI智能体网络)的简介、安装和使用方法、案例应用之详细攻略
  • IDEA 中 Tomcat 部署 Java Web 项目(2)
  • [SCADE编译原理] 状态机到数据流的源到源翻译(2005)
  • 小迪安全v2023学习笔记(一百三十四讲)—— Windows权限提升篇数据库篇MySQLMSSQLOracle自动化项目
  • 2023年10月份04741计算机网络原理真题及答案
  • Room 概要
  • 元宇宙中的数字身份与数据主权:个体权益的守护与边界
  • 函数模板与类模板:C++泛型编程核心解析
  • [GO]Go语言包访问控制与导入机制
  • Flink细粒度滑动窗口性能优化与解决方案深度解析