二进制基础
编译 汇编
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. 可执行文件的分类:
Windows:PE(Portable Executable)
1. 可执行程序.exe
2. 动态链接库.dll
3. 静态链接库.libLinux: 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)静态链接
一、核心流程拆解(按执行时序)
流程以用户态指令触发,经系统调用陷入内核态完成程序装载,最终回到用户态执行代码,核心分为用户态触发、内核态处理、用户态执行三个阶段
| 阶段 | 关键步骤 | 核心作用 | 运行模式 |
|---|---|---|---|
| 触发阶段 | $ ./binary → fork() → 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 程序的核心装载逻辑(映射代码段、数据段、设置入口地址) |
内核态 |
| 执行阶段 | _start → main() |
1. _start:程序真正的入口(由链接器定义,早于 main);2. 完成初始化(如栈设置、环境变量传递)后,调用 main() |
用户态 |
二、关键知识点深度解读
- 核心分界:用户态 vs 内核态
- 图片中虚线是两种模式的边界:
execve()是用户态的系统调用接口,执行后会通过中断 / 陷阱陷入内核态,触发sys_execve(); - 内核态拥有最高权限,可直接操作内存、文件系统,完成程序的物理装载;装载完成后,会通过特权级切换回到用户态,执行程序指令。
- 静态链接程序的装载特点
- 静态链接程序在编译时已将所有依赖库(如
libc)打包进二进制文件,无动态链接依赖; load_elf_binary()处理静态 ELF 时,只需映射程序自身的代码段(.text)、数据段(.data)、BSS 段,无需额外加载动态链接库(如ld-linux.so),流程更简洁。
- 易混淆点:
_start与main
main()是开发者定义的入口,而非程序执行的第一行;_start是链接器入口(由crt0启动文件提供),负责:初始化栈帧、解析命令行参数 / 环境变量、调用__libc_start_main完成 libc 初始化,最终跳转至main();- 在 CTF Pwn 中,
_start是程序的实际起始地址,也是调试(如 GDB)时设置断点的关键位置。
三、CTF Pwn 方向的拓展应用
- 程序入口劫持:通过修改 ELF 的
e_entry字段(程序入口地址),将执行流从_start劫持到自定义的 shellcode,是栈溢出、ELF 篡改类题目的核心思路; - 装载机制绕过:部分题目会修改
search_binary_handler()的逻辑,或伪造 ELF 头部,干扰load_elf_binary()的装载,需要熟悉内核装载流程才能定位漏洞; - 静态链接的漏洞特点:静态链接程序不依赖外部 libc,漏洞利用时无法直接调用系统库函数(如
system),通常需要使用ret2shellcode 或 手工实现系统调用的方式完成攻击。
四、补充说明
若为动态链接程序,流程会增加内核态加载 ld-linux.so(动态链接器)的步骤,用户态会先执行 ld-linux.so 完成库的重定位与初始化,再跳转到 _start,这是静态与动态链接程序执行流程的核心区别。
![[Screenshot 2026-03-10 114243.png]]
(2)动态链接
(一)动态链接程序的执行过程
动态链接程序的执行分为用户态触发、内核态加载、动态链接器处理、用户态程序初始化四个核心阶段:
- 执行发起与系统调用
- 输入
$ ./binary后,Shell 会先调用fork()创建子进程。 - 子进程调用
execve("./binary", *argv[], *envp[]),将自身程序替换为目标二进制,并传递命令行参数与环境变量,触发用户态→内核态切换。
- 内核态加载与解析
内核通过
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的入口。
- 动态链接器(ld.so)处理(用户态)
内核加载完成后,CPU 跳转到
ld.so执行:- 加载程序依赖的所有共享库(如
libc.so.6)到内存。 - 完成符号重定位:将程序中未解析的函数 / 变量地址,替换为共享库在内存中的实际虚拟地址。
- 初始化共享库的全局变量、构造函数。
- 加载程序依赖的所有共享库(如
动态链接器完成所有工作后,跳转到程序自身的汇编入口
_start。
- 用户态程序初始化与执行
_start调用__libc_start_main()(来自libc):- 先执行程序初始化代码
_init(如全局变量构造、C++ 静态对象初始化)。 - 最终调用开发者编写的
main()函数,执行业务逻辑。
- 先执行程序初始化代码
main()执行完毕后,__libc_start_main()调用exit()通知内核回收进程资源。
(二)加载共享库到内存” 的完整逻辑:内核 + ld.so 分工协作
你提到的 “加载程序依赖的所有共享库(如 libc.so.6)到内存”,并不是单一主体完成的动作 ——ld.so(动态链接器)是 “统筹调度者”,内核(Kernel)才是 “实际执行者”。下面用通俗的步骤拆解这个过程:
- ld.so:发起 “装载请求”(用户态)
当 ld.so 接管程序执行后,首先会读取可执行文件的 .dynamic 段(这个段里记录了程序所有依赖的共享库列表,比如 libc.so.6、libpthread.so.0 等),然后逐个处理:
- 先检查目标共享库(如
libc.so.6)是否已经被其他进程加载到物理内存(Linux 会通过共享库的磁盘 inode、设备号等唯一标识来判断); - 如果未加载,ld.so 会调用
mmap()系统调用(用户态→内核态切换),通知内核:“需要把libc.so.6从磁盘加载到内存”。
- 内核:执行 “实际装载 + 虚拟映射”(内核态)
内核收到 ld.so 的请求后,完成核心的 “加载到内存” 操作:
- 物理装载:把磁盘上的
libc.so.6文件(比如/lib64/libc.so.6)读取到物理内存(注意:共享库的 “代码段” 是只读的,只会加载一次,所有进程共享;“数据段” 是可写的,每个进程会有私有副本); - 虚拟映射:为当前进程的虚拟地址空间分配一段空闲的虚拟地址,把物理内存中的
libc.so.6映射到这段地址上(每个进程的虚拟地址可能不同,但指向同一块物理内存); - 记录信息:把
libc.so.6的虚拟基地址、大小等信息,记录到进程的内存管理结构体(mm_struct)中,供后续使用。
- 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 = 图书馆 “助理”(负责登记、找管理员);
- 内核 = 图书馆 “管理员”(负责从仓库取书、放到书架)。
流程对应:
- 读者(进程)想读《C 语言基础》(调用 libc 函数),先找助理(ld.so);
- 助理(ld.so)查记录:如果书架上没有这本书(未加载),就告诉管理员(内核)“需要把这本书放到公共书架”;
- 管理员(内核)把书从仓库(磁盘)拿到公共书架(物理内存),并给每个读者(进程)一张 “书签”(虚拟地址),告诉读者 “这本书在你视野里的 xxx 位置”;
- 所有读者(进程)的书签地址可能不同,但都指向书架上同一本书(物理内存),不用每人都拿一本(节省空间)。
三、关键澄清:避免 2 个常见误区
❌ 误区:ld.so 直接把共享库加载到物理内存
✅ 正解:ld.so 只负责 “发起请求 + 统筹”,实际从磁盘读文件到物理内存的是内核;
❌ 误区:每个进程都加载一份 libc.so.6 到物理内存
✅ 正解:物理内存中
libc.so.6的代码段只有一份,所有进程共享;数据段是每个进程的私有副本(避免互相干扰)。
(三)总结
- “加载共享库到内存” 是内核(实际装载 + 虚拟映射) 和 ld.so(统筹调度 + 递归处理依赖) 协作的结果;
- 共享库的代码段在物理内存中只加载一次,多进程共享,大幅节省内存资源;
- ld.so 不直接执行 “磁盘→物理内存” 的装载动作,核心是发起请求、验证依赖、记录地址。
(3)动态链接 vs 静态链接:核心区别
静态链接程序的执行流程在 load_elf_binary() 后与动态链接完全不同,核心差异如下:
表格
| 对比维度 | 动态链接 | 静态链接 |
|---|---|---|
| 库依赖方式 | 运行时加载共享库(.so),二进制仅保留自身代码,体积小 |
编译时将所有依赖库(如 libc)的代码 / 数据打包进二进制,体积大 |
| 加载流程 | 内核加载后先执行 ld.so,完成共享库加载 + 符号重定位 |
内核加载后直接跳转到程序入口 _start,无 ld.so 参与 |
| 内存占用 | 多进程可共享同一份共享库内存,内存利用率高 | 每个进程都持有独立的库代码副本,内存占用更高 |
| 兼容性 | 依赖系统共享库版本,可能出现「版本不兼容」问题 | 不依赖系统库,兼容性极强,可移植性好 |
| 更新维护 | 共享库更新后,所有依赖它的程序无需重新编译即可生效 | 库更新后,程序必须重新编译才能使用新库版本 |
| 执行入口 | 内核→ld.so→_start→__libc_start_main()→main() |
内核→_start→main()(或 __libc_start_main(),但 libc 已静态嵌入) |
(4)直观总结
- 动态链接:「轻量二进制 + 运行时动态加载共享库」,适合追求体积小、内存共享、库可独立更新的场景(如系统默认程序)。
- 静态链接:「全量二进制 + 无外部依赖」,适合追求兼容性、可移植性的场景(如嵌入式、应急工具)。
![[Screenshot 2026-03-11 083206.png]]





