radare2 的 SIOL 简单 IO 层:Maps、Sections 变换与 Emulation 内存映射架构解析
【免费下载链接】radare2UNIX-like reverse engineering framework and command-line toolset项目地址: https://gitcode.com/gh_mirrors/ra/radare2
导读
SIOL(Simple IO Layer)是 radare2 中负责将二进制文件、内存缓存与虚拟地址空间统一为可寻址视图的 IO 抽象层设计文档。它定义了 Write-Mask、Buffer、Cache、Maps、Descs 与 Plugin 的分层结构,并解释了 Hexeditor(十六进制编辑)、Analysis(静态分析)与 Emulation(仿真执行)三种场景下 Section 到 Map 的变换规则。读完本文,你将掌握 RIO 层 Map/Desc 的职责划分、mapid 的引用约定、三种变换算法的逐步推演,以及在 libr/io 源码中对应的实现载体,能够直接用于理解 radare2 的内存映射加载与调试器初始化流程。
SIOL 的顶层分层视图
文档用一张自下而上的 ASCII 图描述了 IO 层的完整栈结构。整理后从底层到顶层依次为:
+==================+ | Write-Mask | 写掩码:按位与运算过滤写操作字节 +==================+ | Buffer | 缓冲区(文档作者认为可能已废弃,暂无使用场景) +==================+ | Cache (V) | 虚拟地址缓存层 +==================+ +========================+ | Maps | <=== | Sections (transformed) | Section 变换为 Map +==================+ +========================+ | Descs | | +===========+ | | Cache (P) | 物理地址缓存层(per-desc) +======+===========+ | Plugin | 底层 IO 插件,如 malloc、null 等 +==================+各层的职责与仓库中的对应实现如下:
- Plugin:最底层的数据来源,通过
RIOPlugin结构描述(见 libr/include/r_io.h 中RIOPlugin定义),提供open、read、write、seek、close等回调。仓库内置了malloc://、null://、mmap://、rap://、gdb://等数十种插件。 - Descs:一个
RIODesc(见 libr/include/r_io.h)代表一个已打开的数据源,包含fd、perm、uri、plugin等字段,所有描述符统一存放在RIO.files(RIDStorage)中,通过整数 fd 访问。每个 desc 还挂有自身的物理地址缓存Cache (P)。 - Maps:虚拟地址空间的映射表,一个 Map 把一个 desc 的某个区间映射到虚拟地址。
RIO.maps是RIDStorage类型,按 mapid 索引(见 libr/include/r_io.h)。 - Cache (V):位于 Maps 之上的虚拟缓存层,对应
RIO.cache(RIOCache结构,含layers、mode、enabled字段),用于在虚拟地址空间暂存写入、支持 undo/redo。 - Write-Mask:写掩码,见
RIO.write_mask与write_mask_len字段。在 libr/io/io.c 的写入路径中,若io->write_mask非空,写入数据会先与掩码做按位与运算再落盘,常用于模拟只读内存区。
Buffer 层在文档中被明确标注"maybe this could be deprecated, I see no usecase for the buffer",属于设计者的备注而非现行功能,因此不做展开。
Maps:以 mapid 为核心的地址区间抽象
为什么用 mapid 而不是指针
文档强调:每个 Map 拥有唯一的 mapid,RIO 外部的代码应当使用这个整数 id 而非指针来引用 Map。这样做的代价是查找多了一层间接(r_id_storage_get按 id 检索),但收益是避免了悬垂指针带来的稳定性风险——外部模块不再持有可能失效的RIOMap*。
在源码中,RIOMap结构(见 libr/include/r_io.h)包含了文档列出的全部映射信息字段:
| 字段 | 含义 |
|---|---|
id | 唯一映射 id(mapid),由r_id_storage_add分配 |
fd | 关联的 desc 文件描述符 |
delta | 物理偏移:paddr = vaddr - itv.addr + delta |
itv | 地址区间RInterval{addr, size},即虚拟地址区间 |
perm/sperm | 实际权限 / 期望权限(软权限) |
ts | 时间戳,配合RIOMapRef做弱引用校验 |
overlay | 覆盖写红黑树,用于不落盘的补丁写 |
name | 映射名称,如mmap.<section> |
映射的区间运算由头文件宏提供:r_io_map_begin/r_io_map_from取起点,r_io_map_to取终点(r_itv_end(itv) - 1),r_io_map_size取大小,r_io_map_contain判断地址是否落在区间内。
Map 的增删改查
- 创建:
r_io_map_add(io, fd, perm, delta, addr, size)在 libr/io/io_map.c 实现。其内部限制:map 的权限不能高于其所属 desc 的权限,即map->perm = desc->perm & perm;地址区间会与 fd 的实际大小取交集(R_MIN(size, fd_size - delta))。r_io_map_add_bottom与之对称,但把新 map 挂到 bank 底部(低优先级)。 - 查询:
r_io_map_get按 id 取 map;r_io_map_get_at返回虚拟地址处优先级最高的 map;r_io_map_get_paddr按物理地址反查;r_io_map_get_by_fd列出某 fd 关联的所有 map。 - 删除:
r_io_map_del会从所有 bank 中移除该 map 引用再释放;r_io_map_del_for_fd批量删除某 fd 的 map。 - 重定位:
r_io_map_remap改变 map 的起始虚拟地址,并同步更新所有 bank 中的边界。 - 优先级:
r_io_map_priorize/r_io_map_depriorize将 map 在 bank 中置顶或置底,影响地址冲突时的命中顺序。
RIOBank 与 Map 引用
文档中的 Maps 层在现代实现中经由RIOBank组织(见 libr/io/io_bank.c)。RIOBank内维护maprefs(RIOMapRef列表,含id与ts)以及submaps红黑树。RIOMapRef是一种带时间戳的弱引用——r_io_map_get_by_ref会比较map->ts == ref->ts,时间戳不匹配说明 map 已被删除重建,从而避免使用陈旧引用。这也是文档强调"指针会伤害你"的工程落地:外部代码持有的实际上是(id, ts)这样的令牌。
Section 变换:三种目标与两条 map 关系
Section 的映射信息
从二进制加载器(RBinSection)到 IO 层的变换,是 SIOL 的核心场景。Section 携带以下字段:
| 字段 | 含义 |
|---|---|
addr | 文件中的物理起始地址(paddr) |
size | 文件中的大小 |
vaddr | 加载后的虚拟地址 |
vsize | 虚拟内存中的大小(含 BSS 等零填充区) |
fd | 所属文件描述符 |
文档指出:一个 Section 可以关联到两个 Map,即memmap(内存映射)与filemap(文件映射)。当vsize > size时,文件无法覆盖完整的虚拟区间,就需要用内存映射补足差值;filemap记录文件数据的落点,memmap记录补零内存的落点。
Hexeditor 变换(大小无需扩展的场景)
Hexeditor 目标是纯粹的"查看/编辑"模式,算法如下:
- 检查
addr != vaddr,若相等则无需变换(直接可用); - 以
min(size, vsize)为大小创建 map,把 fd 对应 desc 从addr起映射到vaddr; filemap设为该 map 的 id,memmap保持为 0。
也就是说,只要文件内容与虚拟地址对齐,只需一张 map 就足以覆盖编辑需求。
Analysis 变换(vsize > size 时的补零方案)
当vsize <= size时直接退化为 Hexeditor 变换;否则走"文件段 + 零填充段"两步:
- 创建大小为
size的 map,把 fd 对应 desc 从addr映射到vaddr;filemap指向该 map; - 用null 插件打开一个大小为
vsize - size的新 desc; - 再创建大小为
vsize - size的 map,把新 desc 从0x0映射到vaddr + size;memmap指向该 map。
零填充段的实现载体是null://插件(见 libr/io/p/io_null.c):它的__read用memset(buf, 0x00, ...)返回全零数据,__write直接丢弃写入,__resize可调整虚拟大小。因此 Analysis 模式下,BSS 等需要加载器清零的区域被建模为"读全零、写丢弃"的虚拟 desc。
这套逻辑在 libr/core/cbin.c 的add_section与io_create_mem_map中有完整落地:当sec->vsize > psize(psize 为文件内实际可读字节数)时,先映射文件部分(大小psize),再构造null://<gap>的 desc 并在addr + psize处映射 gap 大小的零填充区,最后把该 map 命名为mmap.<section>,并复用已存在的同名 null desc 以避免重复分配(findReusableFile)。
Emulation 变换(可写内存的快照复制)
Emulation 目标面向仿真执行,需要可写的虚拟内存,算法如下:
- 当 Section 不允许写访问时,退化为 Analysis 变换;
- 用malloc 插件以写权限打开大小为
vsize的新 desc; - 从 fd 所指 desc 的
addr起,复制min(size, vsize)字节到新 desc 的0x0处; - 创建大小为
vsize的 map,把新 desc 从0x0映射到vaddr。
malloc://插件(见 libr/io/p/io_malloc.c)在此扮演"堆内存"角色:它用calloc分配缓冲区,支持malloc://<size>、hex://<hex>、slurp://<file>、stdin://、hexfile://等 URI 形式,__write直接修改缓冲区。仿真场景下,文件内容先被快照进 malloc 缓冲区,之后的写入全部发生在内存中而不会污染原始文件,这正是r_io_open_at打开 desc 后执行r_io_map_add的标准流程(见 libr/io/io.c)。
三种变换的对比
| 目标 | 触发条件 | 使用插件 | 复制行为 | filemap / memmap |
|---|---|---|---|---|
| Hexeditor | addr == vaddr | 原 desc | 无复制 | filemap=新 map,memmap=0 |
| Analysis | vsize > size | 原 desc + null | 无复制,补零 | filemap=文件段,memmap=零段 |
| Emulation | 允许写 | malloc | 快照min(size,vsize)字节 | 单一 malloc map |
从源码结构看,三种变换的目标对象(Hexeditor/Analysis/Emulation)分别对应 radare2 的RCore在-w/普通打开、aaa分析、aaa+ESIL 仿真(如 libr/esil/esil.c 中通过RIOBind的read_at/write_at访问 IO)等不同工作流中对内存视图的差异化需求。
从文档到代码:SIOL 在 radare2 中的落地路径
核心数据结构索引
| 概念 | 类型 | 定义位置 |
|---|---|---|
| IO 总控 | RIO | libr/include/r_io.h |
| 描述符 | RIODesc | libr/include/r_io.h |
| 映射 | RIOMap | libr/include/r_io.h |
| 映射弱引用 | RIOMapRef | libr/include/r_io.h |
| Bank | RIOBank | libr/include/r_io.h |
| IO 插件接口 | RIOPlugin | libr/include/r_io.h |
| 写掩码实现 | io->write_mask | libr/io/io.c |
| Map 操作实现 | r_io_map_* | libr/io/io_map.c |
| Bank 操作实现 | r_io_bank_* | libr/io/io_bank.c |
| null 插件 | r_io_plugin_null | libr/io/p/io_null.c |
| malloc 插件 | r_io_plugin_malloc | libr/io/p/io_malloc.c |
| Section 到 Map 变换 | add_section/io_create_mem_map | libr/core/cbin.c |
初始化与打开流程
RIO的生命周期从 libr/io/io.c 的r_io_new开始:依次初始化 desc(r_io_desc_init)、bank(r_io_bank_init)、map(r_io_map_init)与 cache(r_io_cache_init),并创建一个名为default的 bank。打开文件时:
r_io_open_nomap只打开 desc 不建立映射;r_io_open在打开 desc 后立即r_io_map_add(io, fd, perm, 0, 0, desc_size),把整个文件映射到虚拟地址 0x0;r_io_open_at则把 desc 映射到指定地址at。
读写在 libr/io/io.c 中经由r_io_read_at/r_io_write_at分发:先通过 bank 的 submap 树定位命中的 map,再换算到 desc 的物理偏移执行插件回调。写路径还会依次经过写掩码(write_mask按位与)与虚拟缓存(RIOCache),这正是 SIOL 顶层图中 Write-Mask → Cache → Maps 分层顺序的运行逻辑。
实战验证:在 r2 中观察 Map 变换
用户可以通过 radare2 命令行直接观察 SIOL 的分层结果。以一个含 BSS 段的 ELF 为例:
$ r2 /bin/ls [0x00000000]> o # 列出打开的 desc(fd、插件、uri、权限) [0x00000000]> om # 列出当前 bank 的所有 map(mapid、区间、权限、fd) [0x00000000]> om~heap # 过滤查看与堆相关的元数据映射 [0x00000000]> on null://4096 # 手动打开一个 4KB 的零填充 desc [0x00000000]> om # 再次查看,可看到新增的 null 映射其中om输出的每一行即一个RIOMap,其 id 列就是文档强调的 mapid;o输出的 fd 与插件名则对应 Descs 层。加载二进制时,iS输出的vsize大于size的 section(如.bss)就是 Analysis 变换的典型触发对象。
总结
SIOL 设计文档为 radare2 的 IO 层画出了清晰的职责边界:Plugin 负责字节来源,Desc 负责数据源生命周期,Map 负责虚拟地址到物理数据的区间换算,Cache 与 Write-Mask 负责写入语义的修饰,而 Section 变换则把二进制格式中的段描述翻译为运行时所需的 map 组合。三种变换目标的递进关系(Hexeditor → Analysis → Emulation)本质上是对"文件视图 → 补零内存视图 → 可写快照视图"的渐进抽象,在 libr/io 与 libr/core/cbin.c 的源码中均有对应实现可供对照阅读。理解 SIOL,也就理解了 radare2 如何让十六进制编辑、静态分析与动态仿真共享同一套统一的内存寻址模型。
【免费下载链接】radare2UNIX-like reverse engineering framework and command-line toolset项目地址: https://gitcode.com/gh_mirrors/ra/radare2
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考