OpenSSL/ASN.1 对象注册与 LHASH

用线性哈希逐桶扩展动态对象索引

OpenSSL 的 LHASH 以分裂位置 p 记录扩展进度,逐桶重新分配节点;动态 ASN.1 对象注册使用这个通用容器。

b86f4c163索引与查找核验 2026-10-12
相关概念

工程问题#

应用可在运行时注册对象标识符及名称,需要按不同键查找;对象数量增长时,固定桶数会让冲突链变长,而一次搬迁全部节点会集中放大插入成本。

实际实现#

对象注册把类型不同的 ADDED_OBJ 包装放入同一个 LHASH。插入前检查负载,达到阈值则 expand 分裂 p 对应的桶:已有节点按保存的 hash 留在原桶或转入 p+pmax。getrn 根据桶是否已分裂选择取模范围,使扩展中的查询仍能定位正确桶。

处理流程#

  1. 动态对象注册先持写锁检查名称、数据和 NID 冲突
  2. 插入前按负载阈值决定是否分裂下一桶
  3. 只重排分裂桶的冲突链,并推进分裂位置
  4. 查找时依据 p 选择当前映射,再比较 hash 与实际对象键

源码片段#

固定版本 · 原文摘录
crypto/objects/obj_dat.cb86f4c163100a4626597d28126c8b6807a83d1dc · L793–L805
查看原文件
793    if (!ossl_obj_write_lock()) {794        ERR_raise(ERR_LIB_OBJ, ERR_R_UNABLE_TO_GET_WRITE_LOCK);795        goto err2;796    }797798    for (i = ADDED_DATA; i <= ADDED_NID; i++) {799        if (ao[i] == NULL)800            continue;801        ao[i]->type = i;802        ao[i]->obj = o;803        if ((aop[i] = lh_ADDED_OBJ_retrieve(added, ao[i])) != NULL)804            dup = aop[i]->obj;805    }
add_object

动态索引更新在对象写锁下进行,并先检查各类键冲突。

crypto/objects/obj_dat.cb86f4c163100a4626597d28126c8b6807a83d1dc · L823–L839
查看原文件
823    for (i = ADDED_DATA; i <= ADDED_NID; i++) {824        if (ao[i] == NULL)825            continue;826        (void)lh_ADDED_OBJ_insert(added, ao[i]);827        if (lh_ADDED_OBJ_error(added)) {828            while (i-- > ADDED_DATA) {829                if (ao[i] != NULL)830                    lh_ADDED_OBJ_delete(added, ao[i]);831            }832            ERR_raise(ERR_LIB_OBJ, ERR_R_CRYPTO_LIB);833            goto err;834        }835    }836    o->flags &= ~(ASN1_OBJECT_FLAG_DYNAMIC | ASN1_OBJECT_FLAG_DYNAMIC_STRINGS | ASN1_OBJECT_FLAG_DYNAMIC_DATA);837838    ossl_obj_unlock();839    return o->nid;
add_object

同一 ASN.1 对象建立多个类型的索引包装;插入失败会回滚此前登记。

crypto/lhash/lhash.cb86f4c163100a4626597d28126c8b6807a83d1dc · L117–L137
查看原文件
117void *OPENSSL_LH_insert(OPENSSL_LHASH *lh, void *data)118{119    unsigned long hash;120    OPENSSL_LH_NODE *nn, **rn;121    void *ret;122123    lh->error = 0;124    if ((lh->up_load <= (lh->num_items * LH_LOAD_MULT / lh->num_nodes)) && !expand(lh))125        return NULL; /* 'lh->error++' already done in 'expand' */126127    rn = getrn(lh, data, &hash);128129    if (*rn == NULL) {130        if ((nn = OPENSSL_malloc(sizeof(*nn))) == NULL) {131            lh->error++;132            return NULL;133        }134        nn->data = data;135        nn->next = NULL;136        nn->hash = hash;137        *rn = nn;
OPENSSL_LH_insert

插入前检查负载并调用 expand,随后寻找桶内位置。

crypto/lhash/lhash.cb86f4c163100a4626597d28126c8b6807a83d1dc · L248–L265
查看原文件
248    nni = lh->num_alloc_nodes;249    p = lh->p;250    pmax = lh->pmax;251    if (p + 1 >= pmax) {252        j = nni * 2;253        n = OPENSSL_realloc_array(lh->b, j, sizeof(OPENSSL_LH_NODE *));254        if (n == NULL) {255            lh->error++;256            return 0;257        }258        lh->b = n;259        memset(n + nni, 0, sizeof(*n) * (j - nni));260        lh->pmax = nni;261        lh->num_alloc_nodes = j;262        lh->p = 0;263    } else {264        lh->p++;265    }
expand

推进分裂轮次;到轮次边界仍需扩大桶指针数组。

crypto/lhash/lhash.cb86f4c163100a4626597d28126c8b6807a83d1dc · L267–L281
查看原文件
267    lh->num_nodes++;268    n1 = &(lh->b[p]);269    n2 = &(lh->b[p + pmax]);270    *n2 = NULL;271272    for (np = *n1; np != NULL;) {273        hash = np->hash;274        if ((hash % nni) != p) { /* move it */275            *n1 = (*n1)->next;276            np->next = *n2;277            *n2 = np;278        } else279            n1 = &((*n1)->next);280        np = *n1;281    }
expand

只处理当前分裂桶,根据保存的 hash 将节点留在原链或移至新桶。

crypto/lhash/lhash.cb86f4c163100a4626597d28126c8b6807a83d1dc · L329–L349
查看原文件
329    *rhash = hash;330331    nn = hash % lh->pmax;332    if (nn < lh->p)333        nn = hash % lh->num_alloc_nodes;334335    ret = &(lh->b[(int)nn]);336    for (n1 = *ret; n1 != NULL; n1 = n1->next) {337        if (n1->hash != hash) {338            ret = &(n1->next);339            continue;340        }341342        if (lh->compw != NULL) {343            if (lh->compw(n1->data, data, lh->comp) == 0)344                break;345        } else {346            if (lh->comp(n1->data, data) == 0)347                break;348        }349        ret = &(n1->next);
getrn

先使用 pmax 取模;桶已分裂时再使用更大的范围,最后比较实际键。

摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。

收益与取舍#

工程分析

带来的收益

  • 分裂时复用节点中保存的 hash,不必重新计算全部对象的散列值
  • 查询由分裂进度决定桶位置,不需要在两张完整表之间逐个尝试

付出的代价

  • 单桶分裂仍要遍历整条冲突链,不能保证每次插入固定耗时
  • 一轮分裂完成附近会 realloc 桶指针数组并清零新增区域;逐桶搬迁不等于完全消除扩容停顿
  • LHASH 自身不是并发容器,对象注册路径的同步由外层锁提供

可比较的选择

一次性重新散列结构简单,但扩容操作集中遍历并搬迁全部条目。

双表渐进式迁移可分批迁移,但查询和删除必须处理新旧两张表;与此处按 p 推导桶位置不同。

版本与适用范围#

固定到提交 b86f4c163100。只分析通用 LHASH 扩展和动态对象注册,不涉及密码算法或常数时间安全保证。完整源码还含收缩逻辑;此例未把扩展流程推广为删除路径。

参考来源#

源码、官方文档与原始论文
01
OpenSSL · add_object

crypto/objects/obj_dat.c · add_object

源码b86f4c163100a4626597d28126c8b6807a83d1dc
02
OpenSSL · OPENSSL_LH_insert

crypto/lhash/lhash.c · OPENSSL_LH_insert

源码b86f4c163100a4626597d28126c8b6807a83d1dc

相关实现与比较#

4 个相关案例
QEMU·TCG 翻译块v9.2.0

QEMU TCG 用 QHT 保存 TranslationBlock 索引,帮助复用与当前客体执行状态一致的翻译结果。

附源码片段 · 缓存与复用
9 个匹配案例