编译                  汇编

1. C语言代码 ——> 汇编代码 ——> 机器码 之后可以得到可执行文件(在内存中的样子)

字长

一个字长 = CPU 单次能处理的二进制位数

  • 字长不固定,由 CPU 架构决定

    • 8 位机:一个字长 = 8 bit
    • 16 位机:一个字长 = 16 bit
    • 32 位机:一个字长 = 32 bit
    • 64 位机:一个字长 = 64 bit
  • 和字节不一样

    • 1 字节(Byte)永远 = 8 bit
    • 1 字长(Word)= 多少 bit 看 CPU

2 可执行文件:

1. 广义:文件中的数据是可执行代码的文件 `.out、.exe、.sh、.py`
2. 狭义:文件中的数据是机器码的文件 如`.out、.exe、.dll、.so`

3. 可执行文件的分类:

  1. Windows:PE(Portable Executable)
    1. 可执行程序 .exe
    2. 动态链接库 .dll
    3. 静态链接库 .lib
  2. Linux: ELF(Executable and Linkable Format)
    1. 可执行程序 .out
    2. 动态链接库 .so
    3. 静态链接库 .a

4. 数据需要载入内存才能与CPU的总线通信

5. ELF文件核心结构:

![[Pasted image 20260305084254.png]]

6. ELF 文件在磁盘和内存中的两种核心视图,分别对应链接和执行两个阶段

链接阶段,主要服务于链接器调试器
执行阶段,主要服务于操作系统加载器

![[Screenshot 2026-03-05 085105.png]]

7. ELF 可执行文件从磁盘到内存的映射过程

当执行 $ ./ELF 时,操作系统加载器会将 ELF 文件映射到进程地址空间,形成段视图,典型 32 位布局如下(从低地址到高地址)

区域 地址范围 作用
Unused 0x00000000 ~ 0x08048000 预留空间,避免空指针访问
Code(代码段) 0x08048000 ~ 0x08049000 映射磁盘上的 RX 节,存放可执行代码和只读数据
Data(数据段) 0x08049000 ~ … 映射磁盘上的 RW 节,存放已初始化和未初始化数据
Heap(堆) Data 段之上 动态内存分配(如 malloc),向上增长
shared libraries 0x40000000 附近 共享库(.so)的内存映射,实现代码和数据共享
Stack(栈) 0xC0000000 以下 函数调用栈、局部变量,向下增长
For Kernel 0xC0000000 ~ 0xFFFFFFFF 内核空间,用户态不可直接访问
![[Screenshot 2026-03-05 090903.png]]
![[Screenshot 2026-03-05 092107.png]]

8.进程虚拟地址空间

地址以字节编码:1Byte = 8bits
虚拟内存用户空间每个进程一份
虚拟内存内核空间所有进程共享一份
虚拟内存mmap段中的动态链接库仅在物理内存中装载一份

9. bss段、data段、text段、堆(heap)、栈(stack)

bss段:

  **bss段(bss segment)**通常是指用来存放程序中未初始化的全局变量的一块内存区域。

  bss是英文Block Started by Symbol的简称。

  bss段属于静态内存分配。 

  

data段:

  **数据段(data segment)**通常是指用来存放程序中已初始化的全局变量的一块内存区域。

  数据段属于静态内存分配。 

  

text段:

  **代码段(code segment/text segment)**通常是指用来存放程序执行代码的一块内存区域。

  这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。

  在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 

  

堆(heap):

  堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。

  当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);

  当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

  

栈(stack)

   栈又称堆栈,是用户存放程序临时创建的局部变量,

  也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。

  除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。

  由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。

  从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 


一个程序本质上都是由 bss段、data段、text段三个组成的。

 这样的概念,不知道最初来源于哪里的规定,但在当前的计算机程序设计中是很重要的一个基本概念。

 而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题。

   在采用段式内存管理的架构中(比如intel的80x86系统),bss段通常是指用来存放程序中未初始化的全局变量的一块内存区域,

 一般在初始化时bss 段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。

  比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。

 text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;

 而bss段不在可执行文件中,由系统初始化。

10.大端序与小端序

![[Pasted image 20260310105408.png]]

一般都是小端序存储,如对0x123456按小端序存储如下:

![[Screenshot 2026-03-10 111800.png]]

11. 存储器

距离CPU越近的存储器容量越小,速度越快,价格越昂贵

我们一般会接触到 寄存器、高速缓冲存储器(Cache)、内存(主存)及其他

距离CPU最近的就是寄存器

![[Screenshot 2026-03-10 112032.png]]

12. 程序的执行

(1)静态链接

一、核心流程拆解(按执行时序)

流程以用户态指令触发,经系统调用陷入内核态完成程序装载,最终回到用户态执行代码,核心分为用户态触发内核态处理用户态执行三个阶段

阶段 关键步骤 核心作用 运行模式
触发阶段 $ ./binaryfork()execve() 1. 输入指令启动程序;

2. fork() 创建子进程(复制父进程地址空间);

3. execve() 发起程序替换请求
用户态
内核处理 sys_execve()do_execve()search_binary_handler()load_elf_binary() 1. sys_execve():捕获系统调用,进入内核态;

2. do_execve():执行系统调用的核心逻辑;

3. search_binary_handler():识别文件格式(如 ELF),匹配对应的装载器;

4. load_elf_binary():ELF 程序的核心装载逻辑(映射代码段、数据段、设置入口地址)
内核态
执行阶段 _startmain() 1. _start:程序真正的入口(由链接器定义,早于 main);

2. 完成初始化(如栈设置、环境变量传递)后,调用 main()
用户态

二、关键知识点深度解读

  1. 核心分界:用户态 vs 内核态
  • 图片中虚线是两种模式的边界:execve() 是用户态的系统调用接口,执行后会通过中断 / 陷阱陷入内核态,触发 sys_execve()
  • 内核态拥有最高权限,可直接操作内存、文件系统,完成程序的物理装载;装载完成后,会通过特权级切换回到用户态,执行程序指令。
  1. 静态链接程序的装载特点
  • 静态链接程序在编译时已将所有依赖库(如 libc)打包进二进制文件,无动态链接依赖;
  • load_elf_binary() 处理静态 ELF 时,只需映射程序自身的代码段(.text)数据段(.data)BSS 段,无需额外加载动态链接库(如 ld-linux.so),流程更简洁。
  1. 易混淆点:_startmain
  • main() 是开发者定义的入口,而非程序执行的第一行;
  • _start链接器入口(由 crt0 启动文件提供),负责:初始化栈帧、解析命令行参数 / 环境变量、调用 __libc_start_main 完成 libc 初始化,最终跳转至 main()
  • 在 CTF Pwn 中,_start 是程序的实际起始地址,也是调试(如 GDB)时设置断点的关键位置。

三、CTF Pwn 方向的拓展应用

  1. 程序入口劫持:通过修改 ELF 的 e_entry 字段(程序入口地址),将执行流从 _start 劫持到自定义的 shellcode,是栈溢出、ELF 篡改类题目的核心思路;
  2. 装载机制绕过:部分题目会修改 search_binary_handler() 的逻辑,或伪造 ELF 头部,干扰 load_elf_binary() 的装载,需要熟悉内核装载流程才能定位漏洞;
  3. 静态链接的漏洞特点:静态链接程序不依赖外部 libc,漏洞利用时无法直接调用系统库函数(如 system),通常需要使用ret2shellcode手工实现系统调用的方式完成攻击。

四、补充说明

若为动态链接程序,流程会增加内核态加载 ld-linux.so(动态链接器)的步骤,用户态会先执行 ld-linux.so 完成库的重定位与初始化,再跳转到 _start,这是静态与动态链接程序执行流程的核心区别。

![[Screenshot 2026-03-10 114243.png]]

(2)动态链接

(一)动态链接程序的执行过程

动态链接程序的执行分为用户态触发内核态加载动态链接器处理用户态程序初始化四个核心阶段:

  1. 执行发起与系统调用
  • 输入 $ ./binary 后,Shell 会先调用 fork() 创建子进程。
  • 子进程调用 execve("./binary", *argv[], *envp[]),将自身程序替换为目标二进制,并传递命令行参数与环境变量,触发用户态→内核态切换。
  1. 内核态加载与解析
  • 内核通过 sys_execve() 进入系统调用入口,进而调用 do_execve()

  • do_execve() 调用 search_binary_handler() 找到 ELF 格式处理器,最终执行 load_elf_binary()

    • 解析 ELF 文件,加载代码段、数据段到内存。
    • 识别到这是动态链接 ELF,因此会额外加载动态链接器 ld.so(如 /lib64/ld-linux-x86-64.so.2),并将程序入口点设为 ld.so 的入口。
  1. 动态链接器(ld.so)处理(用户态)
  • 内核加载完成后,CPU 跳转到 ld.so 执行:

    1. 加载程序依赖的所有共享库(如 libc.so.6)到内存。
    2. 完成符号重定位:将程序中未解析的函数 / 变量地址,替换为共享库在内存中的实际虚拟地址。
    3. 初始化共享库的全局变量、构造函数。
  • 动态链接器完成所有工作后,跳转到程序自身的汇编入口 _start

  1. 用户态程序初始化与执行
  • _start 调用 __libc_start_main()(来自 libc):

    1. 先执行程序初始化代码 _init(如全局变量构造、C++ 静态对象初始化)。
    2. 最终调用开发者编写的 main() 函数,执行业务逻辑。
  • main() 执行完毕后,__libc_start_main() 调用 exit() 通知内核回收进程资源。

(二)加载共享库到内存” 的完整逻辑:内核 + ld.so 分工协作

你提到的 “加载程序依赖的所有共享库(如 libc.so.6)到内存”,并不是单一主体完成的动作 ——ld.so(动态链接器)是 “统筹调度者”,内核(Kernel)才是 “实际执行者”。下面用通俗的步骤拆解这个过程:

  1. ld.so:发起 “装载请求”(用户态)

当 ld.so 接管程序执行后,首先会读取可执行文件的 .dynamic 段(这个段里记录了程序所有依赖的共享库列表,比如 libc.so.6libpthread.so.0 等),然后逐个处理:

  • 先检查目标共享库(如 libc.so.6)是否已经被其他进程加载到物理内存(Linux 会通过共享库的磁盘 inode、设备号等唯一标识来判断);
  • 如果未加载,ld.so 会调用 mmap() 系统调用(用户态→内核态切换),通知内核:“需要把 libc.so.6 从磁盘加载到内存”。
  1. 内核:执行 “实际装载 + 虚拟映射”(内核态)

内核收到 ld.so 的请求后,完成核心的 “加载到内存” 操作:

  • 物理装载:把磁盘上的 libc.so.6 文件(比如 /lib64/libc.so.6)读取到物理内存(注意:共享库的 “代码段” 是只读的,只会加载一次,所有进程共享;“数据段” 是可写的,每个进程会有私有副本);
  • 虚拟映射:为当前进程的虚拟地址空间分配一段空闲的虚拟地址,把物理内存中的 libc.so.6 映射到这段地址上(每个进程的虚拟地址可能不同,但指向同一块物理内存);
  • 记录信息:把 libc.so.6 的虚拟基地址、大小等信息,记录到进程的内存管理结构体(mm_struct)中,供后续使用。
  1. ld.so:完成 “装载后收尾”(用户态)

内核完成映射后,返回 libc.so.6 的虚拟基地址给 ld.so,ld.so 继续处理:

  • 验证共享库的合法性(比如是否是标准 ELF 格式、版本是否匹配);
  • 检查该共享库是否有 “子依赖”(比如 libpthread.so.0 依赖 libc.so.6),递归重复上述 1、2 步骤,直到所有依赖的共享库都被处理;
  • 记录所有共享库的虚拟基地址,为后续 “符号重定位”(让程序找到 printf 等函数的实际地址)做准备。

二、通俗类比:帮你理解分工

可以把这个过程比作 “读者借图书馆的书”:

  • 物理内存 = 图书馆的 “公共书架”;
  • 共享库(libc.so.6) = 书架上的 “《C 语言基础》这本书”;
  • 进程 = 来借书的 “读者”;
  • ld.so = 图书馆 “助理”(负责登记、找管理员);
  • 内核 = 图书馆 “管理员”(负责从仓库取书、放到书架)。

流程对应:

  1. 读者(进程)想读《C 语言基础》(调用 libc 函数),先找助理(ld.so);
  2. 助理(ld.so)查记录:如果书架上没有这本书(未加载),就告诉管理员(内核)“需要把这本书放到公共书架”;
  3. 管理员(内核)把书从仓库(磁盘)拿到公共书架(物理内存),并给每个读者(进程)一张 “书签”(虚拟地址),告诉读者 “这本书在你视野里的 xxx 位置”;
  4. 所有读者(进程)的书签地址可能不同,但都指向书架上同一本书(物理内存),不用每人都拿一本(节省空间)。

三、关键澄清:避免 2 个常见误区

  1. ❌ 误区:ld.so 直接把共享库加载到物理内存

    ✅ 正解:ld.so 只负责 “发起请求 + 统筹”,实际从磁盘读文件到物理内存的是内核;

  2. ❌ 误区:每个进程都加载一份 libc.so.6 到物理内存

    ✅ 正解:物理内存中 libc.so.6 的代码段只有一份,所有进程共享;数据段是每个进程的私有副本(避免互相干扰)。

(三)总结

  1. “加载共享库到内存” 是内核(实际装载 + 虚拟映射)ld.so(统筹调度 + 递归处理依赖) 协作的结果;
  2. 共享库的代码段在物理内存中只加载一次,多进程共享,大幅节省内存资源;
  3. ld.so 不直接执行 “磁盘→物理内存” 的装载动作,核心是发起请求、验证依赖、记录地址。

(3)动态链接 vs 静态链接:核心区别

静态链接程序的执行流程在 load_elf_binary() 后与动态链接完全不同,核心差异如下:

表格

对比维度 动态链接 静态链接
库依赖方式 运行时加载共享库(.so),二进制仅保留自身代码,体积小 编译时将所有依赖库(如 libc)的代码 / 数据打包进二进制,体积大
加载流程 内核加载后先执行 ld.so,完成共享库加载 + 符号重定位 内核加载后直接跳转到程序入口 _start,无 ld.so 参与
内存占用 多进程可共享同一份共享库内存,内存利用率高 每个进程都持有独立的库代码副本,内存占用更高
兼容性 依赖系统共享库版本,可能出现「版本不兼容」问题 不依赖系统库,兼容性极强,可移植性好
更新维护 共享库更新后,所有依赖它的程序无需重新编译即可生效 库更新后,程序必须重新编译才能使用新库版本
执行入口 内核→ld.so_start__libc_start_main()main() 内核→_startmain()(或 __libc_start_main(),但 libc 已静态嵌入)

(4)直观总结

  • 动态链接:「轻量二进制 + 运行时动态加载共享库」,适合追求体积小、内存共享、库可独立更新的场景(如系统默认程序)。
  • 静态链接:「全量二进制 + 无外部依赖」,适合追求兼容性、可移植性的场景(如嵌入式、应急工具)。

![[Screenshot 2026-03-11 083206.png]]