burpow
第02节-二进制地址和内存基本概念

第02节-二进制地址和内存基本概念

第 2 节:二进制、地址和内存基本概念

所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 1 周
课程主题:计算机如何运行程序
本节目标:理解二进制、十六进制、字节、地址、指针和基本内存区域,为后续学习汇编、逆向、漏洞分析打基础。


1. 本节课你要学会什么

学完这一节,你应该能回答下面几个问题:

  1. 为什么计算机内部用二进制表示数据?
  2. 十六进制为什么在逆向和调试中很常见?
  3. 字节、位、地址之间是什么关系?
  4. 内存为什么可以理解成一个巨大的字节数组?
  5. 指针变量里面到底存的是什么?
  6. 栈、堆、全局区、代码区分别大概放什么?
  7. 为什么同一个程序多次运行,某些地址会变化?
  8. 为什么网络安全和逆向工程必须理解地址?

本节课的主线是:

1
数据 -> 二进制 -> 字节 -> 内存 -> 地址 -> 指针 -> 程序内存布局

后面学习栈溢出、堆漏洞、反汇编、调试器、系统调用时,都会依赖这条线。


2. 为什么计算机使用二进制

计算机底层硬件主要处理电信号。

对硬件来说,最容易稳定区分的是两种状态:

1
2
3
4
高电平 / 低电平
有电 / 没电
真 / 假
1 / 0

所以计算机内部使用二进制。

二进制只有两个数字:

1
0 和 1

一个二进制位叫做:

1
bit

8 个 bit 组成 1 个字节:

1
1 byte = 8 bits

例如:

1
01000001

这是 8 个 bit,也就是 1 个 byte。

如果按 ASCII 编码解释,它表示字符:

1
A

3. 数据本身没有意义,解释方式决定意义

同样一段二进制数据,可以被解释成不同含义。

例如一个字节:

1
01000001

可以解释为:

解释方式 结果
无符号整数 65
ASCII 字符 ‘A’
十六进制 0x41
二进制位模式 01000001

这说明一个重要结论:

内存里存的本质上只是字节。它到底是整数、字符、指针、机器指令,取决于程序如何解释它。

逆向工程中经常会遇到这个问题。

你看到一串字节:

1
48 89 e5

它可以被当作普通数据,也可以被反汇编器解释成机器指令。

所以逆向分析不是“看到字节就知道答案”,而是要结合上下文判断:

  • 这段数据在哪里?
  • 它在 .text 段还是 .data 段?
  • CPU 会不会执行它?
  • 程序有没有把它当字符串使用?
  • 是否有指针指向它?

4. 为什么十六进制很重要

二进制太长,不适合人阅读。

例如:

1
1111111111111111

很难一眼看懂。

十六进制用 16 个符号表示数字:

1
0 1 2 3 4 5 6 7 8 9 A B C D E F

一个十六进制位正好对应 4 个二进制位。

例如:

1
2
3
1111 = F
0000 = 0
1010 = A

所以:

1
11111111 = 0xFF

在 C、汇编、调试器中,十六进制通常加 0x 前缀。

例如:

1
2
3
4
0x10 = 十进制 16
0x20 = 十进制 32
0x100 = 十进制 256
0x401000 = 一个常见的程序地址形式

5. 网络安全中为什么常见十六进制

在安全和逆向方向,十六进制非常常见。

原因是:

  1. 内存地址通常用十六进制表示
  2. 机器指令字节通常用十六进制表示
  3. 文件格式字段通常用十六进制查看
  4. shellcode、payload、哈希、网络包都经常显示为十六进制
  5. 调试器中的寄存器值通常是十六进制

例如调试器里可能看到:

1
2
3
RIP = 0x0000000000401136
RSP = 0x00007fffffffe2a0
RAX = 0x0000000000000000

这些都是十六进制。

如果你对十六进制不熟,逆向和漏洞分析会非常吃力。


6. 字节、字、双字、四字

在底层开发和逆向中,经常看到这些单位。

名称 英文 大小
bit 1 个二进制位
字节 byte 8 bit
word 通常 2 byte
双字 dword 4 byte
四字 qword 8 byte

在 x64dbg、IDA、Ghidra、汇编资料中,常见:

1
2
3
4
byte ptr
word ptr
dword ptr
qword ptr

大致意思是:

1
按多大单位读写内存

例如:

1
mov eax, dword ptr [rbp-4]

可以理解为:

1
从 rbp-4 这个地址开始,读取 4 个字节到 eax

7. 内存可以先理解成一个巨大的字节数组

为了入门,可以把内存想象成一个很大的数组:

1
2
3
4
5
6
7
8
地址        内容
0x1000 41
0x1001 42
0x1002 43
0x1003 00
0x1004 7F
0x1005 10
...

每一个位置有一个地址。

每一个地址通常对应一个字节。

所以:

1
内存地址 = 某个字节在内存中的编号

例如:

1
2
3
0x1000 这个地址里存了 0x41
0x1001 这个地址里存了 0x42
0x1002 这个地址里存了 0x43

如果按 ASCII 字符解释:

1
2
3
0x41 = 'A'
0x42 = 'B'
0x43 = 'C'

那么这段内存可以解释成:

1
ABC

8. 地址是什么

地址可以理解为:

内存中某个位置的编号。

就像现实中的门牌号。

例如:

1
0x7fffffffe2a0

表示进程虚拟地址空间中的某个位置。

这里要特别注意:

你在程序中看到的地址,通常是虚拟地址,不是物理内存条上的真实物理地址。

现代操作系统会给每个进程提供独立的虚拟地址空间。

这意味着:

  • 进程 A 可以有地址 0x400000
  • 进程 B 也可以有地址 0x400000
  • 但它们映射到的实际物理内存不一定相同

这就是虚拟内存的基础。

现在你只需要先记住:

1
程序里看到的地址,大多数情况下是虚拟地址。

虚拟内存会在后面专门讲。


9. 变量和地址

看下面这个 C 程序:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
int x = 123;
printf("x = %d\n", x);
printf("&x = %p\n", &x);
return 0;
}

其中:

1
int x = 123;

表示定义一个整数变量 x

变量 x 的值是:

1
123

而:

1
&x

表示变量 x 所在的地址。

也就是说:

1
2
x  = 变量里的值
&x = 变量所在的位置

可能输出:

1
2
x = 123
&x = 0x7ffc4d2c6a8c

这里的 0x7ffc4d2c6a8c 就是变量 x 在这次运行中的地址。


10. 指针是什么

指针是 C 语言里非常重要的概念。

可以先简单理解成:

指针变量里存的是地址。

例如:

1
2
int x = 123;
int *p = &x;

这里:

1
2
x 是 int 变量,里面存 123
p 是 int* 指针变量,里面存 x 的地址

如果画出来:

1
2
3
变量名      内容
x 123
p &x,也就是 x 的地址

代码:

1
2
3
4
printf("x = %d\n", x);
printf("&x = %p\n", &x);
printf("p = %p\n", p);
printf("*p = %d\n", *p);

其中:

  • p 表示指针变量本身的值,也就是一个地址
  • *p 表示访问这个地址指向的内容

所以:

1
2
p  = 地址
*p = 地址里的值

11. 指针为什么危险

指针强大,但也危险。

因为指针可以让程序直接访问某个地址。

如果指针是正确的,程序正常运行。

如果指针是错误的,可能出现:

  • 程序崩溃
  • 读到错误数据
  • 写坏其他变量
  • 破坏栈或堆
  • 形成安全漏洞

例如:

1
2
int *p = NULL;
*p = 123;

这里 p 是空指针。

对空指针解引用通常会导致程序崩溃。

再比如:

1
2
int arr[3] = {1, 2, 3};
arr[10] = 999;

这就是越界写。

它可能写到数组之外的内存,破坏其他数据。

很多 C/C++ 漏洞都和错误的指针或边界检查有关。


12. 内存里的数据和大小端

多字节数据在内存中有存储顺序问题。

例如一个 4 字节整数:

1
0x12345678

它在内存中可能按下面方式存:

1
2
地址低 -> 地址高
78 56 34 12

这叫小端序。

x86 和 x86-64 通常使用小端序。

所以在调试器里,如果你看到内存:

1
78 56 34 12

它作为 32 位整数解释时,可能是:

1
0x12345678

这对逆向和漏洞分析很重要。

例如你想在内存里找地址:

1
0x401236

它在内存中可能显示为:

1
36 12 40 00 00 00 00 00

因为是小端序。


13. 程序的基本内存区域

一个运行中的程序,也就是一个进程,通常会有不同的内存区域。

先从入门角度理解四个区域:

1
2
3
4
代码区
全局区


不同系统和编译选项下细节会不同,但大方向一致。


13.1 代码区

代码区存放程序的机器指令。

例如你写的:

1
2
3
int main() {
return 0;
}

编译后会变成机器指令,这些指令通常位于代码区。

在 ELF 中常对应:

1
.text

代码区通常是:

1
2
可读 + 可执行
通常不可写

这是安全设计。

如果代码区可以随便写,程序就很容易被篡改。


13.2 全局区

全局变量和静态变量通常放在全局区。

例如:

1
2
int global_var = 123;
static int static_var = 456;

已初始化的全局变量常在:

1
.data

未初始化或初始化为 0 的全局变量常在:

1
.bss

字符串常量可能在:

1
.rodata

例如:

1
char *s = "hello";

字符串 hello 本身通常在只读数据区域。


13.3 堆

堆用于动态内存分配。

例如:

1
2
3
#include <stdlib.h>

int *p = malloc(sizeof(int));

malloc 申请的内存通常来自堆。

堆的特点:

  • 程序运行时动态申请
  • 需要手动释放
  • 生命周期由程序员控制
  • 容易出现内存泄漏、UAF、double free 等漏洞

堆漏洞是二进制安全中非常重要的一类漏洞。


13.4 栈

栈主要用于函数调用。

栈里常见内容包括:

  • 函数参数
  • 局部变量
  • 返回地址
  • 保存的寄存器

例如:

1
2
3
void foo() {
int x = 123;
}

局部变量 x 通常在栈上。

函数调用结束后,这个栈帧会被回收。

栈是理解缓冲区溢出的关键。

如果一个局部数组被写越界,就可能覆盖栈上的其他内容,甚至影响返回地址。


14. 用程序观察不同区域的地址

写一个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>

int global_var = 123;
int global_zero;

void foo() {
printf("foo addr: %p\n", foo);
}

int main() {
int local_var = 456;
int *heap_var = malloc(sizeof(int));
char *str = "hello memory";

*heap_var = 789;

printf("main addr: %p\n", main);
printf("foo addr: %p\n", foo);
printf("global_var addr: %p\n", &global_var);
printf("global_zero addr:%p\n", &global_zero);
printf("local_var addr: %p\n", &local_var);
printf("heap_var addr: %p\n", heap_var);
printf("str literal addr:%p\n", str);

free(heap_var);
return 0;
}

编译运行:

1
2
3
4
gcc addr_demo.c -o addr_demo
./addr_demo
./addr_demo
./addr_demo

你会看到不同类型对象的地址。

重点观察:

  • mainfoo 的地址
  • 全局变量地址
  • 局部变量地址
  • malloc 出来的堆地址
  • 字符串常量地址
  • 多次运行后哪些地址变化

15. 为什么多次运行地址会变化

现代操作系统通常启用 ASLR。

ASLR 全称是:

1
Address Space Layout Randomization

中文常译为:

1
地址空间布局随机化

它的作用是让程序每次运行时,某些内存区域的地址发生变化。

目的:

1
增加漏洞利用难度

如果攻击者无法预测:

  • 栈在哪里
  • 堆在哪里
  • libc 在哪里
  • 程序代码在哪里

利用漏洞就会更困难。

Linux 下可以查看 ASLR 状态:

1
cat /proc/sys/kernel/randomize_va_space

常见值:

含义
0 关闭 ASLR
1 部分随机化
2 完整随机化

不建议随便修改系统配置。学习阶段先观察即可。


16. 虚拟地址和物理地址的区别

你在 C 程序里打印出来的地址,例如:

1
0x7ffd1e45a2bc

这通常是虚拟地址。

虚拟地址由操作系统和 CPU 的 MMU 共同转换到物理地址。

简化理解:

1
2
3
4
程序看到虚拟地址
操作系统维护映射关系
硬件 MMU 完成地址转换
最终访问物理内存

为什么要这样设计?

  1. 每个进程拥有独立地址空间
  2. 防止一个进程随便访问另一个进程
  3. 支持内存保护
  4. 支持按需加载和换页
  5. 支持共享库映射

后面虚拟内存章节会深入讲页表、缺页异常和地址映射。


17. Linux 下查看进程内存布局

Linux 提供 /proc 文件系统,可以观察进程信息。

查看当前 shell 的内存映射:

1
cat /proc/$$/maps

如果你想查看某个程序运行时的内存,可以让它先暂停。

例如:

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <unistd.h>

int main() {
printf("pid = %d\n", getpid());
getchar();
return 0;
}

运行后它会等待输入。

另开一个终端:

1
cat /proc/<pid>/maps

你会看到类似:

1
2
3
4
5
6
7
55b1a5f1d000-55b1a5f1e000 r--p ... /home/user/addr_demo
55b1a5f1e000-55b1a5f1f000 r-xp ... /home/user/addr_demo
55b1a5f1f000-55b1a5f20000 r--p ... /home/user/addr_demo
...
[heap]
...
[stack]

重点观察:

  • 哪些区域可读 r
  • 哪些区域可写 w
  • 哪些区域可执行 x
  • 哪个是堆 [heap]
  • 哪个是栈 [stack]
  • 哪些是共享库

18. Windows 下如何观察地址和内存

Windows 下可以用:

  • x64dbg
  • Process Explorer
  • Process Hacker
  • WinDbg

在 x64dbg 中,你可以看到:

  • CPU 指令窗口
  • 寄存器窗口
  • 栈窗口
  • 内存 Dump 窗口
  • 模块列表

在 Process Explorer 中,可以观察:

  • 进程 PID
  • 线程
  • DLL 模块
  • 内存占用
  • 句柄

Windows 程序中常见地址也通常是虚拟地址。

PE 文件被加载到进程地址空间后,会有一个基址,例如:

1
ImageBase = 0x140000000

如果开启 ASLR,实际加载地址可能每次变化。


19. 地址和逆向工程的关系

逆向工程中几乎所有分析都离不开地址。

例如:

1
2
3
4
5
6
7
8
入口点地址
函数地址
字符串地址
跳转目标地址
API 地址
栈地址
堆地址
返回地址

当你在 Ghidra 或 IDA 里看到:

1
2
3
00401136  call puts
0040113b mov eax, 0
00401140 ret

左边的 00401136 就是指令地址。

当你在 x64dbg 里下断点,本质上就是告诉调试器:

1
程序执行到某个地址时暂停

例如:

1
breakpoint at 0x401136

所以地址是逆向分析的坐标系。

没有地址,调试器、反汇编器、内存窗口都无法组织信息。


20. 地址和漏洞分析的关系

漏洞分析中也离不开地址。

例如栈溢出中,你关心:

  • 缓冲区起始地址
  • 返回地址在栈上的位置
  • 输入覆盖到了哪里
  • 崩溃时 RIP 变成了什么
  • payload 位于哪个地址

堆漏洞中,你关心:

  • 堆块地址
  • 堆元数据
  • free 后的指针
  • 被覆盖的函数指针

权限或内核漏洞中,你可能关心:

  • 用户态地址
  • 内核态地址
  • 指针是否经过检查
  • 地址是否可读写

现代防护机制也和地址紧密相关:

防护 和地址的关系
ASLR 随机化地址
DEP / NX 控制某些地址区域是否可执行
Canary 检测栈上关键位置是否被覆盖
PIE 让程序代码基址可随机化
CFG 限制间接调用能跳到哪些地址

21. 地址和恶意代码分析的关系

恶意代码分析中,地址同样重要。

你可能需要观察:

  • 解密后的字符串在内存哪里
  • 配置数据被写到哪里
  • 是否申请了可执行内存
  • 是否把代码写入其他进程内存
  • 是否跳转到动态生成的代码
  • 是否修改 IAT 或函数入口

例如,一个程序调用 Windows API:

1
2
3
VirtualAlloc
WriteProcessMemory
CreateRemoteThread

从地址角度看,它可能做了:

  1. 分配一块内存
  2. 向某个地址写入数据
  3. 让线程从某个地址开始执行

这就是理解进程注入等技术的基础。

本课程不会引导你做未授权攻击,但会帮助你理解这些行为为什么可疑、如何分析。


22. 本节 Linux 实验:打印不同对象地址

创建 addr_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>

int global_var = 123;
int global_zero;

void foo() {
printf("inside foo\n");
}

int main() {
int local_var = 456;
int *heap_var = malloc(sizeof(int));
char *str = "hello memory";

*heap_var = 789;

printf("main addr: %p\n", main);
printf("foo addr: %p\n", foo);
printf("global_var addr: %p\n", &global_var);
printf("global_zero addr: %p\n", &global_zero);
printf("local_var addr: %p\n", &local_var);
printf("heap_var value: %p\n", heap_var);
printf("heap_var addr: %p\n", &heap_var);
printf("str value: %p\n", str);
printf("str addr: %p\n", &str);

free(heap_var);
return 0;
}

编译运行:

1
2
3
4
gcc addr_demo.c -o addr_demo
./addr_demo
./addr_demo
./addr_demo

记录每次输出。

思考:

  1. 哪些地址比较接近?
  2. 哪些地址每次运行都会变化?
  3. 局部变量和堆变量地址是否在相近区域?
  4. heap_var valueheap_var addr 有什么区别?
  5. str valuestr addr 有什么区别?

23. 本节 Linux 实验:查看内存映射

创建 maps_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int global_var = 123;

int main() {
int local_var = 456;
void *heap = malloc(100);

printf("pid = %d\n", getpid());
printf("main = %p\n", main);
printf("global_var = %p\n", &global_var);
printf("local_var = %p\n", &local_var);
printf("heap = %p\n", heap);
printf("press Enter to exit...\n");
getchar();

free(heap);
return 0;
}

编译运行:

1
2
gcc maps_demo.c -o maps_demo
./maps_demo

程序会打印 PID 并等待输入。

另开终端:

1
cat /proc/<pid>/maps

<pid> 换成程序输出的 PID。

观察:

  • 程序自身映射区域
  • [heap]
  • [stack]
  • libc 映射
  • 权限位 rwx

24. Windows 对照实验

如果你有 Windows 环境,可以写同样的 C 程序,编译成 exe。

使用 x64dbg:

  1. 打开程序
  2. 运行到 main 附近
  3. 查看寄存器
  4. 查看栈窗口
  5. 查看内存 Dump
  6. 查看模块基址

使用 Process Explorer:

  1. 找到进程
  2. 查看 DLL 模块
  3. 查看内存信息
  4. 观察程序多次运行时模块地址是否变化

思考:

  • Windows 下地址是不是也会变化?
  • exe 和 DLL 是否都有加载基址?
  • 栈和堆是否也存在?

25. 本节重点总结

你需要记住这些核心结论:

  1. 计算机底层用二进制表示数据。
  2. 8 个 bit 组成 1 个 byte。
  3. 十六进制适合表示地址、机器码和二进制数据。
  4. 内存可以先理解成一个巨大的字节数组。
  5. 地址是内存位置的编号。
  6. C 语言里的指针变量存的是地址。
  7. p 是地址,*p 是访问地址里的值。
  8. 程序运行时通常有代码区、全局区、堆、栈等区域。
  9. 程序中看到的地址通常是虚拟地址。
  10. ASLR 会让某些地址在每次运行时变化。
  11. 地址是逆向工程和漏洞分析的坐标系。

26. 本节课后作业

作业 1:打印地址并分析

addr_demo.c,打印:

  • 函数地址
  • 全局变量地址
  • 局部变量地址
  • malloc 返回的地址
  • 指针变量自己的地址
  • 字符串常量地址

运行 3 次,记录输出。

提交内容:

1
2
3
4
5
1. 源代码
2. 三次运行结果
3. 哪些地址发生变化
4. 哪些地址比较接近
5. 你对栈、堆、代码区、全局区的初步理解

作业 2:区分指针的值和指针自己的地址

写程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main() {
int x = 123;
int *p = &x;

printf("x = %d\n", x);
printf("&x = %p\n", &x);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("*p = %d\n", *p);

return 0;
}

回答:

  1. p&p 一样吗?
  2. p&x 一样吗?
  3. *p 是什么意思?
  4. 指针变量本身是否也需要内存保存?

作业 3:查看 /proc/<pid>/maps

运行 maps_demo.c,查看:

1
cat /proc/<pid>/maps

提交内容:

1
2
3
4
5
6
1. 程序 PID
2. main 地址
3. heap 地址
4. stack 地址
5. maps 中对应区域
6. 哪些区域可执行,哪些区域可写

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
函数调用和栈帧

你会进一步理解:

  • 函数调用时栈发生了什么
  • 返回地址是什么
  • 局部变量如何放在栈上
  • 为什么缓冲区溢出可能影响程序控制流
隐藏
换装
本文作者:burpow
本文链接:https://youthfulnesszxx.github.io/2026/05/28/第02节-二进制地址和内存基本概念/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可