跋山涉水 —— 深入 Redis 字典遍历

逆向实战:360加固DEX解密全流程解析(附Frida脚本与SoFixer修复技巧) 本文深入解析360加固DEX解密的全流程,涵盖动态Hook技术、Frida脚本应用及SoFixer修复技巧。通过实战案例详细展示如何识别加固特征、定位关键函数、实现ELF文件修复,并解析RC4和zlib解密算法,为逆向工程师提供全面的技术指导。 阅读详情

Redis 字典的遍历过程逻辑比较复杂,互联网上对这一块的分析讲解非常少。我也花了不少时间对源码的细节进行了整理,将我个人对字典遍历逻辑的理解呈现给各位读者。也许读者们对字典的遍历过程有比我更好的理解,还请不吝指教。

640?wx_fmt=png

一边遍历一边修改

我们知道 Redis 对象树的主干是一个字典,如果对象很多,这个主干字典也会很大。当我们使用 keys 命令搜寻指定模式的 key 时,它会遍历整个主干字典。值得注意的是,在遍历的过程中,如果满足模式匹配条件的 key 被找到了,还需要判断 key 指向的对象是否已经过期。如果过期了就需要从主干字典中将该 key 删除。

void keysCommand(client *c) {
    dictIterator *di; // 迭代器
    dictEntry *de; // 迭代器当前的entry
    sds pattern = c->argv[1]->ptr; // keys的匹配模式参数
    int plen = sdslen(pattern);
    int allkeys; // 是否要获取所有key,用于keys *这样的指令
    unsigned long numkeys = 0;
    void *replylen = addDeferredMultiBulkLength(c);

    // why safe? 
    di = dictGetSafeIterator(c->db->dict);
    allkeys = (pattern[0] == '*' && pattern[1] == '\0');
    while((de = dictNext(di)) != NULL) {
        sds key = dictGetKey(de);
        robj *keyobj;

        if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
            keyobj = createStringObject(key,sdslen(key));
            // 判断是否过期,过期了要删除元素
            if (expireIfNeeded(c->db,keyobj) == 0) {
                addReplyBulk(c,keyobj);
                numkeys++;
            }
            decrRefCount(keyobj);
        }
    }
    dictReleaseIterator(di);
    setDeferredMultiBulkLength(c,replylen,numkeys);
}

那么,你是否想到了其中的困难之处,在遍历字典的时候还需要修改字典,会不会出现指针安全问题?

重复遍历

字典在扩容的时候要进行渐进式迁移,会存在新旧两个 hashtable。遍历需要对这两个 hashtable 依次进行,先遍历完旧的 hashtable,再继续遍历新的 hashtable。如果在遍历的过程中进行了 rehashStep,将已经遍历过的旧的 hashtable 的元素迁移到了新的 hashtable中,那么遍历会不会出现元素的重复?这也是遍历需要考虑的疑难之处,下面我们来看看 Redis 是如何解决这个问题的。

迭代器的结构

Redis 为字典的遍历提供了 2 种迭代器,一种是安全迭代器,另一种是不安全迭代器。

typedef struct dictIterator {
    dict *d; // 目标字典对象
    long index; // 当前遍历的槽位置,初始化为-1
    int table; // ht[0] or ht[1]
    int safe; // 这个属性非常关键,它表示迭代器是否安全
    dictEntry *entry; // 迭代器当前指向的对象
    dictEntry *nextEntry; // 迭代器下一个指向的对象
    long long fingerprint; // 迭代器指纹,放置迭代过程中字典被修改
} dictIterator;

// 获取非安全迭代器,只读迭代器,允许rehashStep
dictIterator *dictGetIterator(dict *d)
{
    dictIterator *iter = zmalloc(sizeof(*iter));

    iter->d = d;
    iter->table = 0;
    iter->index = -1;
    iter->safe = 0;
    iter->entry = NULL;
    iter->nextEntry = NULL;
    return iter;
}

// 获取安全迭代器,允许触发过期处理,禁止rehashStep
dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);

    i->safe = 1;
    return i;
}

迭代器的「安全」指的是在遍历过程中可以对字典进行查找和修改,不用感到担心,因为查找和修改会触发过期判断,会删除内部元素。「安全」的另一层意思是迭代过程中不会出现元素重复,为了保证不重复,就会禁止 rehashStep。

而「不安全」的迭代器是指遍历过程中字典是只读的,你不可以修改,你只能调用 dictNext 对字典进行持续遍历,不得调用任何可能触发过期判断的函数。不过好处是不影响 rehash,代价就是遍历的元素可能会出现重复。

安全迭代器在刚开始遍历时,会给字典打上一个标记,有了这个标记,rehashStep 就不会执行,遍历时元素就不会出现重复。

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;
    // 这个就是标记,它表示当前加在字典上的安全迭代器的数量
    unsigned long iterators;
} dict;

// 如果存在安全的迭代器,就禁止rehash
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

迭代过程

安全的迭代器在遍历过程中允许删除元素,意味着字典第一维数组下面挂接的链表中的元素可能会被摘走,元素的 next 指针就会发生变动,这是否会影响迭代过程呢?下面我们仔细研究一下迭代函数的代码逻辑。

dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        if (iter->entry == NULL) {
            // 遍历一个新槽位下面的链表,数组的index往前移动了
            dictht *ht = &iter->d->ht[iter->table];
            if (iter->index == -1 && iter->table == 0) {
                // 第一次遍历,刚刚进入遍历过程
                // 也就是ht[0]数组的第一个元素下面的链表
                if (iter->safe) {
                  // 给字典打安全标记,禁止字典进行rehash
                  iter->d->iterators++;
                } else {
                  // 记录迭代器指纹,就好比字典的md5值
                  // 如果遍历过程中字典有任何变动,指纹就会改变
                  iter->fingerprint = dictFingerprint(iter->d);
                }      
            }
            iter->index++; // index=0,正式进入第一个槽位
            if (iter->index >= (long) ht->size) {
                // 最后一个槽位都遍历完了
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    // 如果处于rehash中,那就继续遍历第二个 hashtable
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                } else {
                    // 结束遍历
                    break;
                }
            }
            // 将当前遍历的元素记录到迭代器中
            iter->entry = ht->table[iter->index];
        } else {
            // 直接将下一个元素记录为本次迭代的元素
            iter->entry = iter->nextEntry;
        }
        if (iter->entry) {
            // 将下一个元素也记录到迭代器中,这点非常关键
            // 防止安全迭代过程中当前元素被过期删除后,找不到下一个需要遍历的元素

            // 试想如果后面发生了rehash,当前遍历的链表被打散了,会发生什么
            // 这里要使劲发挥自己的想象力来理解
            // 旧的链表将一分为二,打散后重新挂接到新数组的两个槽位下
            // 结果就是会导致当前链表上的元素会重复遍历

            // 如果rehash的链表是index前面的链表,那么这部分链表也会被重复遍历
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
    }
    return NULL;
}

// 遍历完成后要释放迭代器,安全迭代器需要去掉字典的禁止rehash的标记
// 非安全迭代器还需要检查指纹,如果有变动,服务器就会奔溃(failfast)
void dictReleaseIterator(dictIterator *iter)
{
    if (!(iter->index == -1 && iter->table == 0)) {
        if (iter->safe)
            iter->d->iterators--; // 去掉禁止rehash的标记
        else
            assert(iter->fingerprint == dictFingerprint(iter->d));
    }
    zfree(iter);
}

// 计算字典的指纹,就是将字典的关键字段进行按位糅合到一起
// 这样只要有任意的结构变动,指纹都会发生变化
// 如果只是某个元素的value被修改了,指纹不会发生变动
long long dictFingerprint(dict *d) {
    long long integers[6], hash = 0;
    int j;

    integers[0] = (long) d->ht[0].table;
    integers[1] = d->ht[0].size;
    integers[2] = d->ht[0].used;
    integers[3] = (long) d->ht[1].table;
    integers[4] = d->ht[1].size;
    integers[5] = d->ht[1].used;

    for (j = 0; j < 6; j++) {
        hash += integers[j];
        hash = (~hash) + (hash << 21);
        hash = hash ^ (hash >> 24);
        hash = (hash + (hash << 3)) + (hash << 8);
        hash = hash ^ (hash >> 14);
        hash = (hash + (hash << 2)) + (hash << 4);
        hash = hash ^ (hash >> 28);
        hash = hash + (hash << 31);
    }
    return hash;
}

值得注意的是在字典扩容时进行rehash,将旧数组中的链表迁移到新的数组中。某个具体槽位下的链表只可能会迁移到新数组的两个槽位中。

hash mod 2^n = k
hash mod 2^(n+1) = k or k+2^n

迭代器的选择

除了keys指令使用了安全迭代器,因为结果不允许重复。那还有其它的地方使用了安全迭代器么,什么情况下遍历适合使用非安全迭代器呢?

简单一点说,那就是如果遍历过程中不允许出现重复,那就使用SafeIterator,比如下面的两种情况

  1. bgaofrewrite需要遍历所有对象转换称操作指令进行持久化,绝对不允许出现重复

  2. bgsave也需要遍历所有对象来持久化,同样不允许出现重复

如果遍历过程中需要处理元素过期,需要对字典进行修改,那也必须使用SafeIterator,因为非安全的迭代器是只读的。

其它情况下,也就是允许遍历过程中出现个别元素重复,不需要对字典进行结构性修改的情况下一律使用非安全迭代器。

思考

请继续思考rehash对非安全遍历过程的影响,会重复哪些元素,重复的元素会非常多么还是只是少量重复?

640?wx_fmt=png

微信扫一扫关注公众号「码洞」,一步两步带你「码出个未来」。

本文节选之《Redis深度历险》,点击「阅读原文」,现在就开始学习《Redis深度历险》

【原理图PCB专题】Cadence 17.4版本原理图及PCB 模块复用 在我们设计复杂板卡的时候,往往会遇到一部分电路被反复使用的情况。Cadence允许设计者一开始就将复用模块设计好,以后只要直接调用复用模块就可以直接用以设计更复杂的电路板了。这对于大规模集成设计无疑是非常好的选择,它不仅让设计者不必花费时间在相同模块反复设计上,更有利于电路的模块化设计。 阅读详情

相关推荐

基于Matlab与Gurobi的园区微电网风光储协同规划:从独立运营到联合优化的建模实战

本文详细介绍了如何利用Matlab与Gurobi进行园区微电网的风光储协同规划建模实战。通过构建从独立运营到联合优化的数学模型,结合Yalmip工具箱,旨在解决源荷不匹配问题,实现储能系统的最优配置与运行,从而降低总供电成本并提升新能源利用率。

weixin_29053073的博客 142

redis学习笔记--1

因此,这个命令的含义是:尝试获取名为 lock:codehole 的分布式锁,在获取锁的过程中,如果这个锁已经被其他客户端获取,则当前客户端会等待,直到获取锁为止。ex=5 表示锁的过期时间为 5秒,其中5 是一个数字,表示锁的有效期。tag = random. next int() #随机数 if redis.set(key, tag, nx=True, ex=5}: do_ something () redis.delifequals(key, tag) #假想的 delifequals 指令。

didikai的博客 243

power bi 演唱会-佐罗

我是业务分析人员,注重使用性,因此,课程不到 30% 的内容就可以解决目前为止我碰到的所有业务问题,剩下的 70% 内容是帮助我提升,优化,升华,精益求精用的,比如第三方工具,比如代码运行速度优化,比如查询逻辑优化等等。系统性上来说,结构明晰,安排上非常有条理,层层递进;《BI 真经》,中有真经二字,略显夸大,而当我第一次证明了这点的时候,我也非常惊讶,这就是我做以下所有事情的一切动力。《BI 真经》综合考量思想性,系统性,本质性,创新性,通用性,技巧性,启发性,对比其他课程有压倒性的显著优势。

v_1090213742的博客 911

Hi3798MV300魔百盒安全加固:从刷机到信任链重建

嵌入式设备安全加固是物联网时代的基础课题,其核心在于建立可验证的启动链与细粒度运行时权限控制。基于ARM TrustZone和Verified Boot原理,通过启用Secure Boot、强制SELinux Enforcing模式及内核态eBPF进程拦截,可在资源受限的SoC(如Hi3798MV300)上实现轻量级系统防护。该技术路径不依赖高性能硬件,而重在权限收敛与执行路径可信化,广泛适用于老旧机顶盒改造、智能家居网关加固及运营商定制终端二次赋能等场景。本文以CM201-2为实例,详解如何在1GB内存、

weixin_34319111的博客 425

ENVI5.6 批量处理GF-2/GF-6/GF-7:从安装到融合的完整自动化流程

本文详细介绍了ENVI5.6环境下批量处理GF-2/GF-6/GF-7卫星数据的完整自动化流程,包括环境配置、工具安装、正射校正、图像融合等关键步骤。通过实战案例和优化技巧,帮助用户高效完成高分卫星影像处理,提升工作效率。特别针对GF-6和GF-7的特殊处理要点进行了深入解析。

weixin_42546392的博客 179

从安装到出图:一份给振动测试新手的LMS Test.Lab 2021完整避坑手册(含许可证更新和常见弹窗解决)

本文为振动测试新手提供LMS Test.Lab 2021的完整使用指南,涵盖安装配置、许可证更新、数据采集到频域后处理的全流程。特别针对常见弹窗错误提供解决方案,并详细讲解Colormap时频图分析技巧,帮助用户快速掌握振动数据分析核心技能。

weixin_29268331的博客 994

Redis源码解析——字典遍历

之前两篇博文讲解了字典库的基础,本文将讲解其遍历操作。之所以将遍历操作独立成一文来讲,是因为其中的内容和之前的基本操作还是有区别的。特别是高级遍历一节介绍的内容,充满了精妙设计的算法智慧。(转载请指明出于breaksoftware的csdn博客) 迭代器遍历 由于Redis字典库有rehash机制,而且是渐进式的,所以迭代器操作可能会通过其他特殊方式来实现,以保...

方亮的专栏 2931

ESP32 Arduino 学习篇(五)TFT_eSPI库

该库有User_Setup.h和 User_Setup_Select.h两个配置文件,支持 ①自定义参数或 ②使用已有配置 驱动TFT屏幕。优点是: 1. 占用空间非常小 2. 动画清晰,充分利用每个像素点 3. 可以非常灵动. 4. 方便进行微调。首先在Libraries里面搜索安装TFT_eSPI库到你的工程文件里面。初始化屏幕, 如果是ST7735,可以往里面传一个参数, 具体用到时再看。缺点是: 1. 容易闪屏 2. 一般不会太复杂 3.需要大量的计算。特别注意: 字库7是仿7段数码屏的样式。

DOF526570的博客 2万+

redis不稳定字典的遍历

redis 不稳定字典的遍历给新观众老爷的开场redis 不稳定字典的遍历redis字典的状态字典的两种不稳定情况1.字典扩容,由小变大2.字典缩容,由大变小1个容易想到的遍历方法不用重新从头遍历的技巧字典扩容情况的遍历字典缩容情况的遍历小结参考资料彩蛋往期博客回顾dictScan源码与注释 给新观众老爷的开场 大家好,我是弟弟! 最近读了一遍 黄健宏大佬的 <<Redis 设计与实现>>,对Redis 3.0版本有了一些认识 该书作者有一版添加了注释的 redis 3.0源码 ??

a158372582的博客 641

DeepSeek提示词实战:从零到百的AI角色扮演指南(附100+场景模板)

本文提供了一份详尽的DeepSeek提示词实战指南,重点讲解AI角色扮演的核心技巧。通过“角色扮演四要素法”和五大核心场景模板,帮助用户从零开始掌握如何设计高质量提示词,有效提升AI输出的深度与精度,并附赠100+实用场景模板供参考。

weixin_29305337的博客 587

华为OD机试真题 新系统-字符串格式调整(C/C++/Py/Java/Js/Go)

华为OD机试 新系统真题【字符串格式调整】,提供C语言、Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解,以及在线OJ模拟做题。华为OD机试真题【字符串格式调整】多语言题解

qq_45776114的博客 1142

跋山涉水——深入 Redis 字典遍历

Redis 字典的遍历过程逻辑比较复杂,互联网上对这一块的分析讲解非常少。我也花了不少时间对源码的细节进行了整理,将我个人对字典遍历逻辑的理解呈现给各位读者。也许读者们对字典的遍历过程有比我更好的理解,还请不吝指教。 一边遍历一边修改 我们知道 Redis 对象树的主干是一个字典,如果对象很多,这个主干字典也会很大。当...

Java爬坑之路 122

SAP 清账凭证 完整生成逻辑(超详细原理 + 底层规则 + 分录逻辑 + 互清特殊规则)

【SAP清账凭证核心逻辑摘要】(149字) SAP清账凭证生成遵循两大类型:1)资金往来对冲型(如F-32/F-44互清),强制生成含完整借贷分录的独立凭证,通过AUGDT/AUGBL字段绑定未清项,典型场景如应收冲应付(借:应付账款 贷:应收账款);2)纯核销型(如F.13自动清账),仅标记行项目状态不产生实际分录。关键规则:①仅未清项管理科目可清账;②互清必须配置主数据关联;③差额清账自动补差(折扣/汇差);④原始凭证永不修改,仅更新BSIK/BSAD等表状态。清账凭证均保留可追溯性,支持FBRA重置还

金牌架构师 544

码洞原创深度技术文章大全 —— 高端面试必备

《快学 Go 语言》第 11 课 —— 协程《快学 Go 语言》第 10 课 —— 错误与异常《快学 Go 语言》第 9 课 —— 接口《快学 Go 语言》第 8 课 —...

codehole_的博客 293

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Redis源码解析:04字典的遍历dictScan

dict.c中的dictScan函数,用来遍历字典,迭代其中的每个元素。该函数使用的算法非常精妙!!!所以必须记录一下。 遍历一个稳定的字典,当然不是什么难事,但Redis中的字典因为有rehash的过程,使字典可能扩展,也可能缩小。这就带来了问题,如果在两次遍历中间,字典的结构发生了变化(扩展或缩小),字典中的元素所在的位置相应的会发生变化,那如何保证字典...

weixin_30394251的博客 276

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

求不更学不动之Redis5.0新特性Stream尝鲜

Redis5.0最近被作者突然放出来了,增加了很多新的特色功能。而Redis5.0最大的新特性就是多出了一个数据结构Stream,它是一个新的强大的支持多播的可持久化的消息队列,作者坦言Redis Stream狠狠地借鉴了Kafka的设计。 Redis Stream的结构如上图所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的,Redis重启后...

weixin_34321977的博客 1305

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Redis之字典遍历

遍历和修改 Redis本身就是一个大字典,当我们使用keys命令查找这个大字典时,它会遍历整个redis,如果找到了符合匹配条件的key,还需要判断key指向的对象是否已经过期,如果过期了就需要进行删除操作。 void keysCommand(client *c) { dictIterator *di; // 迭代器 dictEntry *de; // 迭代器当前的entry sds pattern = c->argv[1]->ptr; // keys的匹配模式参数

cainiao1412的博客 1667

Redis-字典遍历内部实现原理源码

1、遍历Redis 1)Redis对象树的主干是一个字典,如果对象很多,那么这个树会很大,当我们执行keys命令时,首先它会遍历整棵树,查找出满足条件的key,其次会判断遍历出的key对应的对象是否过期,如果过期则从主干中删除掉 void keysCommand(client *c) {     dictIterator *di; // 迭代器     dictEn...

哆啦A梦 551

c语言实现Redis遍历所有key

要遍历Redis所有key或者指定模式的key有两种方法:KEYS和SCAN 1.KEYS命令 KEYS pattern 查找所有符合给定模式pattern(正则表达式)的 key 。时间复杂度为O(N),N为数据库里面key的数量。例如,Redis在一个有1百万个key的数据库里面执行一次查询需要的时间是40毫秒 。 警告: KEYS 的速度非常快,但在一个大的数据库中使用它仍然...

king_jie0210的专栏 2812

RedisTemplate的各种操作(set、hash、list、string)

注入以下RedisTemplate @Autowired private RedisTemplate&lt;String,String&gt; redisTemplate; 1、保存和读取Set: 代码示例: SetOperations&lt;String, String&gt; set = redisTemplate.opsForSet(); set.add("set1","22...

lichuangcsdn的博客 10万+

Redis深入浅出——字符串和SDS

一、字符串 字符串的类信息定义 1.1 字符串介绍 字符串是Redis中最为常见的数据存储类型,其底层实现是简单动态字符串sds(simple dynamic string),是可以修改的字符串。 它类似于Java中的ArrayList,它采用预分配冗余空间的方式来减少内存的频繁分配。 如图中所示,内部为当前字符串实际分配的空间 。其中capacity是最大容量,len是实际长度...

我的博客 2万+
上一篇: 《Redis深度历险》《深入理解RPC》答读者问
下一篇: 见缝插针 —— 深入 Redis HyperLogLog 内部数据结构分析
codehole_
博客等级 码龄7年 34粉丝 132原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值