radare2 的 SIOL 简单 IO 层:Maps、Sections 变换与 Emulation 内存映射架构解析
2026/9/20 22:13:57 网站建设 项目流程

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定义),提供openreadwriteseekclose等回调。仓库内置了malloc://null://mmap://rap://gdb://等数十种插件。
  • Descs:一个RIODesc(见 libr/include/r_io.h)代表一个已打开的数据源,包含fdpermuriplugin等字段,所有描述符统一存放在RIO.filesRIDStorage)中,通过整数 fd 访问。每个 desc 还挂有自身的物理地址缓存Cache (P)
  • Maps:虚拟地址空间的映射表,一个 Map 把一个 desc 的某个区间映射到虚拟地址。RIO.mapsRIDStorage类型,按 mapid 索引(见 libr/include/r_io.h)。
  • Cache (V):位于 Maps 之上的虚拟缓存层,对应RIO.cacheRIOCache结构,含layersmodeenabled字段),用于在虚拟地址空间暂存写入、支持 undo/redo。
  • Write-Mask:写掩码,见RIO.write_maskwrite_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内维护maprefsRIOMapRef列表,含idts)以及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 目标是纯粹的"查看/编辑"模式,算法如下:

  1. 检查addr != vaddr,若相等则无需变换(直接可用);
  2. min(size, vsize)为大小创建 map,把 fd 对应 desc 从addr起映射到vaddr
  3. filemap设为该 map 的 id,memmap保持为 0。

也就是说,只要文件内容与虚拟地址对齐,只需一张 map 就足以覆盖编辑需求。

Analysis 变换(vsize > size 时的补零方案)

vsize <= size时直接退化为 Hexeditor 变换;否则走"文件段 + 零填充段"两步:

  1. 创建大小为size的 map,把 fd 对应 desc 从addr映射到vaddrfilemap指向该 map;
  2. null 插件打开一个大小为vsize - size的新 desc;
  3. 再创建大小为vsize - size的 map,把新 desc 从0x0映射到vaddr + sizememmap指向该 map。

零填充段的实现载体是null://插件(见 libr/io/p/io_null.c):它的__readmemset(buf, 0x00, ...)返回全零数据,__write直接丢弃写入,__resize可调整虚拟大小。因此 Analysis 模式下,BSS 等需要加载器清零的区域被建模为"读全零、写丢弃"的虚拟 desc。

这套逻辑在 libr/core/cbin.c 的add_sectionio_create_mem_map中有完整落地:当sec->vsize > psize(psize 为文件内实际可读字节数)时,先映射文件部分(大小psize),再构造null://<gap>的 desc 并在addr + psize处映射 gap 大小的零填充区,最后把该 map 命名为mmap.<section>,并复用已存在的同名 null desc 以避免重复分配(findReusableFile)。

Emulation 变换(可写内存的快照复制)

Emulation 目标面向仿真执行,需要可写的虚拟内存,算法如下:

  1. 当 Section 不允许写访问时,退化为 Analysis 变换;
  2. malloc 插件以写权限打开大小为vsize的新 desc;
  3. 从 fd 所指 desc 的addr起,复制min(size, vsize)字节到新 desc 的0x0处;
  4. 创建大小为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
Hexeditoraddr == vaddr原 desc无复制filemap=新 map,memmap=0
Analysisvsize > 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 中通过RIOBindread_at/write_at访问 IO)等不同工作流中对内存视图的差异化需求。

从文档到代码:SIOL 在 radare2 中的落地路径

核心数据结构索引

概念类型定义位置
IO 总控RIOlibr/include/r_io.h
描述符RIODesclibr/include/r_io.h
映射RIOMaplibr/include/r_io.h
映射弱引用RIOMapReflibr/include/r_io.h
BankRIOBanklibr/include/r_io.h
IO 插件接口RIOPluginlibr/include/r_io.h
写掩码实现io->write_masklibr/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_nulllibr/io/p/io_null.c
malloc 插件r_io_plugin_malloclibr/io/p/io_malloc.c
Section 到 Map 变换add_section/io_create_mem_maplibr/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询