第02节-二进制地址和内存基本概念
第 2 节:二进制、地址和内存基本概念
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 1 周
课程主题:计算机如何运行程序
本节目标:理解二进制、十六进制、字节、地址、指针和基本内存区域,为后续学习汇编、逆向、漏洞分析打基础。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- 为什么计算机内部用二进制表示数据?
- 十六进制为什么在逆向和调试中很常见?
- 字节、位、地址之间是什么关系?
- 内存为什么可以理解成一个巨大的字节数组?
- 指针变量里面到底存的是什么?
- 栈、堆、全局区、代码区分别大概放什么?
- 为什么同一个程序多次运行,某些地址会变化?
- 为什么网络安全和逆向工程必须理解地址?
本节课的主线是:
1 | |
后面学习栈溢出、堆漏洞、反汇编、调试器、系统调用时,都会依赖这条线。
2. 为什么计算机使用二进制
计算机底层硬件主要处理电信号。
对硬件来说,最容易稳定区分的是两种状态:
1 | |
所以计算机内部使用二进制。
二进制只有两个数字:
1 | |
一个二进制位叫做:
1 | |
8 个 bit 组成 1 个字节:
1 | |
例如:
1 | |
这是 8 个 bit,也就是 1 个 byte。
如果按 ASCII 编码解释,它表示字符:
1 | |
3. 数据本身没有意义,解释方式决定意义
同样一段二进制数据,可以被解释成不同含义。
例如一个字节:
1 | |
可以解释为:
| 解释方式 | 结果 |
|---|---|
| 无符号整数 | 65 |
| ASCII 字符 | ‘A’ |
| 十六进制 | 0x41 |
| 二进制位模式 | 01000001 |
这说明一个重要结论:
内存里存的本质上只是字节。它到底是整数、字符、指针、机器指令,取决于程序如何解释它。
逆向工程中经常会遇到这个问题。
你看到一串字节:
1 | |
它可以被当作普通数据,也可以被反汇编器解释成机器指令。
所以逆向分析不是“看到字节就知道答案”,而是要结合上下文判断:
- 这段数据在哪里?
- 它在
.text段还是.data段? - CPU 会不会执行它?
- 程序有没有把它当字符串使用?
- 是否有指针指向它?
4. 为什么十六进制很重要
二进制太长,不适合人阅读。
例如:
1 | |
很难一眼看懂。
十六进制用 16 个符号表示数字:
1 | |
一个十六进制位正好对应 4 个二进制位。
例如:
1 | |
所以:
1 | |
在 C、汇编、调试器中,十六进制通常加 0x 前缀。
例如:
1 | |
5. 网络安全中为什么常见十六进制
在安全和逆向方向,十六进制非常常见。
原因是:
- 内存地址通常用十六进制表示
- 机器指令字节通常用十六进制表示
- 文件格式字段通常用十六进制查看
- shellcode、payload、哈希、网络包都经常显示为十六进制
- 调试器中的寄存器值通常是十六进制
例如调试器里可能看到:
1 | |
这些都是十六进制。
如果你对十六进制不熟,逆向和漏洞分析会非常吃力。
6. 字节、字、双字、四字
在底层开发和逆向中,经常看到这些单位。
| 名称 | 英文 | 大小 |
|---|---|---|
| 位 | bit | 1 个二进制位 |
| 字节 | byte | 8 bit |
| 字 | word | 通常 2 byte |
| 双字 | dword | 4 byte |
| 四字 | qword | 8 byte |
在 x64dbg、IDA、Ghidra、汇编资料中,常见:
1 | |
大致意思是:
1 | |
例如:
1 | |
可以理解为:
1 | |
7. 内存可以先理解成一个巨大的字节数组
为了入门,可以把内存想象成一个很大的数组:
1 | |
每一个位置有一个地址。
每一个地址通常对应一个字节。
所以:
1 | |
例如:
1 | |
如果按 ASCII 字符解释:
1 | |
那么这段内存可以解释成:
1 | |
8. 地址是什么
地址可以理解为:
内存中某个位置的编号。
就像现实中的门牌号。
例如:
1 | |
表示进程虚拟地址空间中的某个位置。
这里要特别注意:
你在程序中看到的地址,通常是虚拟地址,不是物理内存条上的真实物理地址。
现代操作系统会给每个进程提供独立的虚拟地址空间。
这意味着:
- 进程 A 可以有地址
0x400000 - 进程 B 也可以有地址
0x400000 - 但它们映射到的实际物理内存不一定相同
这就是虚拟内存的基础。
现在你只需要先记住:
1 | |
虚拟内存会在后面专门讲。
9. 变量和地址
看下面这个 C 程序:
1 | |
其中:
1 | |
表示定义一个整数变量 x。
变量 x 的值是:
1 | |
而:
1 | |
表示变量 x 所在的地址。
也就是说:
1 | |
可能输出:
1 | |
这里的 0x7ffc4d2c6a8c 就是变量 x 在这次运行中的地址。
10. 指针是什么
指针是 C 语言里非常重要的概念。
可以先简单理解成:
指针变量里存的是地址。
例如:
1 | |
这里:
1 | |
如果画出来:
1 | |
代码:
1 | |
其中:
p表示指针变量本身的值,也就是一个地址*p表示访问这个地址指向的内容
所以:
1 | |
11. 指针为什么危险
指针强大,但也危险。
因为指针可以让程序直接访问某个地址。
如果指针是正确的,程序正常运行。
如果指针是错误的,可能出现:
- 程序崩溃
- 读到错误数据
- 写坏其他变量
- 破坏栈或堆
- 形成安全漏洞
例如:
1 | |
这里 p 是空指针。
对空指针解引用通常会导致程序崩溃。
再比如:
1 | |
这就是越界写。
它可能写到数组之外的内存,破坏其他数据。
很多 C/C++ 漏洞都和错误的指针或边界检查有关。
12. 内存里的数据和大小端
多字节数据在内存中有存储顺序问题。
例如一个 4 字节整数:
1 | |
它在内存中可能按下面方式存:
1 | |
这叫小端序。
x86 和 x86-64 通常使用小端序。
所以在调试器里,如果你看到内存:
1 | |
它作为 32 位整数解释时,可能是:
1 | |
这对逆向和漏洞分析很重要。
例如你想在内存里找地址:
1 | |
它在内存中可能显示为:
1 | |
因为是小端序。
13. 程序的基本内存区域
一个运行中的程序,也就是一个进程,通常会有不同的内存区域。
先从入门角度理解四个区域:
1 | |
不同系统和编译选项下细节会不同,但大方向一致。
13.1 代码区
代码区存放程序的机器指令。
例如你写的:
1 | |
编译后会变成机器指令,这些指令通常位于代码区。
在 ELF 中常对应:
1 | |
代码区通常是:
1 | |
这是安全设计。
如果代码区可以随便写,程序就很容易被篡改。
13.2 全局区
全局变量和静态变量通常放在全局区。
例如:
1 | |
已初始化的全局变量常在:
1 | |
未初始化或初始化为 0 的全局变量常在:
1 | |
字符串常量可能在:
1 | |
例如:
1 | |
字符串 hello 本身通常在只读数据区域。
13.3 堆
堆用于动态内存分配。
例如:
1 | |
malloc 申请的内存通常来自堆。
堆的特点:
- 程序运行时动态申请
- 需要手动释放
- 生命周期由程序员控制
- 容易出现内存泄漏、UAF、double free 等漏洞
堆漏洞是二进制安全中非常重要的一类漏洞。
13.4 栈
栈主要用于函数调用。
栈里常见内容包括:
- 函数参数
- 局部变量
- 返回地址
- 保存的寄存器
例如:
1 | |
局部变量 x 通常在栈上。
函数调用结束后,这个栈帧会被回收。
栈是理解缓冲区溢出的关键。
如果一个局部数组被写越界,就可能覆盖栈上的其他内容,甚至影响返回地址。
14. 用程序观察不同区域的地址
写一个程序:
1 | |
编译运行:
1 | |
你会看到不同类型对象的地址。
重点观察:
main和foo的地址- 全局变量地址
- 局部变量地址
- malloc 出来的堆地址
- 字符串常量地址
- 多次运行后哪些地址变化
15. 为什么多次运行地址会变化
现代操作系统通常启用 ASLR。
ASLR 全称是:
1 | |
中文常译为:
1 | |
它的作用是让程序每次运行时,某些内存区域的地址发生变化。
目的:
1 | |
如果攻击者无法预测:
- 栈在哪里
- 堆在哪里
- libc 在哪里
- 程序代码在哪里
利用漏洞就会更困难。
Linux 下可以查看 ASLR 状态:
1 | |
常见值:
| 值 | 含义 |
|---|---|
| 0 | 关闭 ASLR |
| 1 | 部分随机化 |
| 2 | 完整随机化 |
不建议随便修改系统配置。学习阶段先观察即可。
16. 虚拟地址和物理地址的区别
你在 C 程序里打印出来的地址,例如:
1 | |
这通常是虚拟地址。
虚拟地址由操作系统和 CPU 的 MMU 共同转换到物理地址。
简化理解:
1 | |
为什么要这样设计?
- 每个进程拥有独立地址空间
- 防止一个进程随便访问另一个进程
- 支持内存保护
- 支持按需加载和换页
- 支持共享库映射
后面虚拟内存章节会深入讲页表、缺页异常和地址映射。
17. Linux 下查看进程内存布局
Linux 提供 /proc 文件系统,可以观察进程信息。
查看当前 shell 的内存映射:
1 | |
如果你想查看某个程序运行时的内存,可以让它先暂停。
例如:
1 | |
运行后它会等待输入。
另开一个终端:
1 | |
你会看到类似:
1 | |
重点观察:
- 哪些区域可读
r - 哪些区域可写
w - 哪些区域可执行
x - 哪个是堆
[heap] - 哪个是栈
[stack] - 哪些是共享库
18. Windows 下如何观察地址和内存
Windows 下可以用:
- x64dbg
- Process Explorer
- Process Hacker
- WinDbg
在 x64dbg 中,你可以看到:
- CPU 指令窗口
- 寄存器窗口
- 栈窗口
- 内存 Dump 窗口
- 模块列表
在 Process Explorer 中,可以观察:
- 进程 PID
- 线程
- DLL 模块
- 内存占用
- 句柄
Windows 程序中常见地址也通常是虚拟地址。
PE 文件被加载到进程地址空间后,会有一个基址,例如:
1 | |
如果开启 ASLR,实际加载地址可能每次变化。
19. 地址和逆向工程的关系
逆向工程中几乎所有分析都离不开地址。
例如:
1 | |
当你在 Ghidra 或 IDA 里看到:
1 | |
左边的 00401136 就是指令地址。
当你在 x64dbg 里下断点,本质上就是告诉调试器:
1 | |
例如:
1 | |
所以地址是逆向分析的坐标系。
没有地址,调试器、反汇编器、内存窗口都无法组织信息。
20. 地址和漏洞分析的关系
漏洞分析中也离不开地址。
例如栈溢出中,你关心:
- 缓冲区起始地址
- 返回地址在栈上的位置
- 输入覆盖到了哪里
- 崩溃时 RIP 变成了什么
- payload 位于哪个地址
堆漏洞中,你关心:
- 堆块地址
- 堆元数据
- free 后的指针
- 被覆盖的函数指针
权限或内核漏洞中,你可能关心:
- 用户态地址
- 内核态地址
- 指针是否经过检查
- 地址是否可读写
现代防护机制也和地址紧密相关:
| 防护 | 和地址的关系 |
|---|---|
| ASLR | 随机化地址 |
| DEP / NX | 控制某些地址区域是否可执行 |
| Canary | 检测栈上关键位置是否被覆盖 |
| PIE | 让程序代码基址可随机化 |
| CFG | 限制间接调用能跳到哪些地址 |
21. 地址和恶意代码分析的关系
恶意代码分析中,地址同样重要。
你可能需要观察:
- 解密后的字符串在内存哪里
- 配置数据被写到哪里
- 是否申请了可执行内存
- 是否把代码写入其他进程内存
- 是否跳转到动态生成的代码
- 是否修改 IAT 或函数入口
例如,一个程序调用 Windows API:
1 | |
从地址角度看,它可能做了:
- 分配一块内存
- 向某个地址写入数据
- 让线程从某个地址开始执行
这就是理解进程注入等技术的基础。
本课程不会引导你做未授权攻击,但会帮助你理解这些行为为什么可疑、如何分析。
22. 本节 Linux 实验:打印不同对象地址
创建 addr_demo.c:
1 | |
编译运行:
1 | |
记录每次输出。
思考:
- 哪些地址比较接近?
- 哪些地址每次运行都会变化?
- 局部变量和堆变量地址是否在相近区域?
heap_var value和heap_var addr有什么区别?str value和str addr有什么区别?
23. 本节 Linux 实验:查看内存映射
创建 maps_demo.c:
1 | |
编译运行:
1 | |
程序会打印 PID 并等待输入。
另开终端:
1 | |
把 <pid> 换成程序输出的 PID。
观察:
- 程序自身映射区域
[heap][stack]- libc 映射
- 权限位
rwx
24. Windows 对照实验
如果你有 Windows 环境,可以写同样的 C 程序,编译成 exe。
使用 x64dbg:
- 打开程序
- 运行到
main附近 - 查看寄存器
- 查看栈窗口
- 查看内存 Dump
- 查看模块基址
使用 Process Explorer:
- 找到进程
- 查看 DLL 模块
- 查看内存信息
- 观察程序多次运行时模块地址是否变化
思考:
- Windows 下地址是不是也会变化?
- exe 和 DLL 是否都有加载基址?
- 栈和堆是否也存在?
25. 本节重点总结
你需要记住这些核心结论:
- 计算机底层用二进制表示数据。
- 8 个 bit 组成 1 个 byte。
- 十六进制适合表示地址、机器码和二进制数据。
- 内存可以先理解成一个巨大的字节数组。
- 地址是内存位置的编号。
- C 语言里的指针变量存的是地址。
p是地址,*p是访问地址里的值。- 程序运行时通常有代码区、全局区、堆、栈等区域。
- 程序中看到的地址通常是虚拟地址。
- ASLR 会让某些地址在每次运行时变化。
- 地址是逆向工程和漏洞分析的坐标系。
26. 本节课后作业
作业 1:打印地址并分析
写 addr_demo.c,打印:
- 函数地址
- 全局变量地址
- 局部变量地址
- malloc 返回的地址
- 指针变量自己的地址
- 字符串常量地址
运行 3 次,记录输出。
提交内容:
1 | |
作业 2:区分指针的值和指针自己的地址
写程序:
1 | |
回答:
p和&p一样吗?p和&x一样吗?*p是什么意思?- 指针变量本身是否也需要内存保存?
作业 3:查看 /proc/<pid>/maps
运行 maps_demo.c,查看:
1 | |
提交内容:
1 | |
27. 自测题
题 1
1 byte 等于多少 bit?
题 2
为什么十六进制比二进制更适合人阅读?
题 3
内存地址可以如何理解?
题 4
int *p = &x; 中,p、&p、*p 分别是什么意思?
题 5
栈和堆有什么初步区别?
题 6
为什么程序多次运行时地址可能变化?
题 7
为什么逆向工程离不开地址?
28. 自测题参考答案
答 1
1 byte 等于 8 bit。
答 2
因为二进制太长,十六进制一位可以表示 4 个二进制位,更紧凑,适合表示地址、机器码和内存数据。
答 3
内存地址可以理解为内存中某个字节位置的编号。程序中看到的地址通常是虚拟地址。
答 4
p 是指针变量的值,也就是 x 的地址;&p 是指针变量 p 自己所在的地址;*p 是访问 p 指向的地址里的内容,也就是 x 的值。
答 5
栈主要用于函数调用、局部变量和返回地址,通常由编译器和运行时自动管理;堆用于动态内存分配,例如 malloc,需要程序员手动释放。
答 6
因为现代操作系统通常启用 ASLR,让栈、堆、共享库、程序基址等地址随机化,以提高漏洞利用难度。
答 7
因为逆向分析中的入口点、函数、字符串、跳转目标、API、栈、堆、返回地址都需要通过地址定位。地址是二进制分析的坐标系。
29. 下一节预告
下一节课会讲:
1 | |
你会进一步理解:
- 函数调用时栈发生了什么
- 返回地址是什么
- 局部变量如何放在栈上
- 为什么缓冲区溢出可能影响程序控制流