switch case 二分搜索风格

文章目录

  • [从汇编反向看 C 的 `switch`:二分搜索风格的案例分析](#从汇编反向看 C 的 switch:二分搜索风格的案例分析)
    • [1. 先看看源码](#1. 先看看源码)
    • [2. 函数开头的栈和参数处理](#2. 函数开头的栈和参数处理)
    • [3. 第一层判断:围绕 1000 切分](#3. 第一层判断:围绕 1000 切分)
    • [4. 小于 1000 的分支](#4. 小于 1000 的分支)
    • [5. 大于 1000 的分支](#5. 大于 1000 的分支)
    • [6. case 分支实现细节](#6. case 分支实现细节)
    • [7. 从汇编"还原"成等价 C](#7. 从汇编“还原”成等价 C)
    • [8. 为什么说这是"折半查找 / 二分搜索风格"](#8. 为什么说这是“折半查找 / 二分搜索风格”)
    • [9. 逆向工程实战经验](#9. 逆向工程实战经验)
    • [10. 测试示例](#10. 测试示例)

从汇编反向看 C 的 switch:二分搜索风格的案例分析

最近我在调试一段 C 代码,里面有一个 switch(x),case 值很稀疏,从 10 到 20000,中间跨度很大。为了理解编译器是怎么生成汇编的,我直接打开了反汇编,想把它从汇编"还原"回 C,并且顺便观察它用的到底是跳转表还是别的策略。下面我把整个分析过程记录下来,边看汇编边总结经验。


1. 先看看源码

源码很直观:

c 复制代码
switch (x) {
case 10:    printf("10\n"); break;
case 50:    printf("50\n"); break;
case 100:   printf("100\n"); break;
case 200:   printf("200\n"); break;
case 500:   printf("500\n"); break;
case 1000:  printf("1000\n"); break;
case 2000:  printf("2000\n"); break;
case 5000:  printf("5000\n"); break;
case 10000: printf("10000\n"); break;
case 20000: printf("20000\n"); break;
default:    printf("default\n"); break;
}

可以看到,case 值有序但稀疏 ,如果编译器用传统跳转表,会浪费空间,因为跳转表跨度到 20000。猜测它可能不会用 jump table,而是用比较树 / 二分搜索风格


2. 函数开头的栈和参数处理

看汇编,函数开头是标准的栈帧建立:

asm 复制代码
00085280  push        ebp  
00085281  mov         ebp,esp  
00085283  push        ecx  
00085284  mov         dword ptr [ebp-4],0CCCCCCCCh
...
00085296  mov         eax,dword ptr [ebp+8]  
00085299  mov         dword ptr [ebp-4],eax
  • 参数 x 被保存到 [ebp-4],后面所有 cmp [ebp-4], imm 都是在比较 x

  • 这里还有几条 mov ecx,8C008h / call 00081217,是编译器插的栈保护或初始化,和 switch 本身没关系。


3. 第一层判断:围绕 1000 切分

然后看到第一条关键比较:

asm 复制代码
0008529C  cmp         dword ptr [ebp-4],3E8h  ; 1000
000852A3  jg          000852F2                ; if x > 1000 → 大区间
000852A5  cmp         dword ptr [ebp-4],3E8h
000852AC  je          0008538A                ; if x == 1000 → case 1000

脑子里立刻画出一个思路:

  • x > 1000 → 跳去处理大数的分支

  • x == 1000 → 直接命中 case 1000

  • x < 1000 → 继续下面的小数分支

感觉就像把所有 case 分成三块,这是典型二分搜索的第一层


4. 小于 1000 的分支

继续看 x < 1000 的分支:

asm 复制代码
000852B2  cmp [ebp-4],64h    ; 100
000852B6  jg 000852D3         ; x > 100 → 中间分支
000852B8  cmp [ebp-4],64h
000852BC  je 0008535D         ; x == 100
000852C2  cmp [ebp-4],0Ah     ; 10
000852C6  je 00085339         ; x == 10
000852C8  cmp [ebp-4],32h     ; 50
000852CC  je 0008534B         ; x == 50
000852CE  jmp 000853D5         ; default

结合逻辑:

c 复制代码
if (x < 1000) {
    if (x > 100) {
        // 200, 500 分支
    } else if (x == 100) {
        // case 100
    } else if (x == 10) {
        // case 10
    } else if (x == 50) {
        // case 50
    } else {
        // default
    }
}

再看 x > 100 的那条分支:

asm 复制代码
000852D3  cmp [ebp-4],0C8h   ; 200
000852DA  je 0008536C         ; case 200
000852E0  cmp [ebp-4],1F4h   ; 500
000852E7  je 0008537B         ; case 500
000852ED  jmp 000853D5        ; default

所以小于 1000 的区间逻辑就是一个两层 if-else 树,把 10、50、100、200、500 分开。


5. 大于 1000 的分支

回到第一层 x > 1000

asm 复制代码
000852F2  cmp [ebp-4],2710h  ; 10000
000852F9  jg 00085327         ; x > 10000 → 后续分支
000852FB  cmp [ebp-4],2710h
00085302  je 000853B7         ; case 10000
00085308  cmp [ebp-4],7D0h    ; 2000
0008530F  je 00085399         ; case 2000
00085315  cmp [ebp-4],1388h   ; 5000
0008531C  je 000853A8         ; case 5000
00085322  jmp 000853D5        ; default

逻辑对应 C:

c 复制代码
if (x > 1000) {
    if (x > 10000) {
        if (x == 20000) printf("20000\n");
        else            printf("default\n");
    } else {
        if (x == 10000) printf("10000\n");
        else if (x == 2000) printf("2000\n");
        else if (x == 5000) printf("5000\n");
        else printf("default\n");
    }
}

可以看到编译器在大数区间也做了分区+二分比较


6. case 分支实现细节

每个 case 基本一样,都是 push 字符串地址,call printf,然后跳到 switch 尾部。例如:

asm 复制代码
00085339  push 88620h      ; "10\n"
0008533E  call 00081082    ; printf
00085343  add esp,4
00085346  jmp 000853E2     ; switch 尾部

其它 case 也类似,只是字符串地址不同。default 也是一样:

asm 复制代码
000853D5  push 88668h       ; "default\n"
000853DA  call 00081082
000853DF  add esp,4

尾部统一处理函数返回。


7. 从汇编"还原"成等价 C

把所有比较和跳转串起来,就像在画一棵决策树,最终等价 C 代码如下:

c 复制代码
void binary_search_switch(int x) {
    if (x > 1000) {
        if (x > 10000) {
            if (x == 20000) printf("20000\n");
            else            printf("default\n");
        } else {
            if (x == 10000) printf("10000\n");
            else if (x == 2000) printf("2000\n");
            else if (x == 5000) printf("5000\n");
            else printf("default\n");
        }
    } else if (x == 1000) {
        printf("1000\n");
    } else {
        if (x > 100) {
            if (x == 200) printf("200\n");
            else if (x == 500) printf("500\n");
            else printf("default\n");
        } else {
            if (x == 100) printf("100\n");
            else if (x == 10) printf("10\n");
            else if (x == 50) printf("50\n");
            else printf("default\n");
        }
    }
}

仔细看会发现,这完全对应汇编里的 cmp + 条件跳转组合,编译器就是把稀疏 case 值分层,减少比较次数。


8. 为什么说这是"折半查找 / 二分搜索风格"

总结规律:

  1. 第一层用 1000 把所有 case 分成 <1000==1000>1000 三块。

  2. <1000 区间再用 100{10,50,100}{200,500} 分开。

  3. >1000 区间用 10000{2000,5000,10000}{20000} 分开。

整个结构就像一棵高度 3~4 的决策树。相比线性 if-else:

  • 线性比较最坏要 9 次

  • 现在最坏只要 3~4 次

同时避免了跳转表对稀疏 case 的巨大空间浪费。


9. 逆向工程实战经验

下次再看到类似汇编,识别方法:

  1. 一连串 cmp [ebp-4], imm + jg/jl/je → 明显按大小分段。

  2. 分支里直接 push 常量 → call printf → jmp 统一尾部 → 这就是 case。

  3. 多处 jmp 同一个尾部 → switch 的统一出口。

  4. 没有 jump table 的连续指针数组 → 编译器用了比较树而不是 table。

看到这些,基本可以断定是二分搜索风格 switch


10. 测试示例

c 复制代码
#include <stdio.h>

void binary_search_switch(int x) {
    switch (x) {
    case 10:    printf("10\n"); break;
    case 50:    printf("50\n"); break;
    case 100:   printf("100\n"); break;
    case 200:   printf("200\n"); break;
    case 500:   printf("500\n"); break;
    case 1000:  printf("1000\n"); break;
    case 2000:  printf("2000\n"); break;
    case 5000:  printf("5000\n"); break;
    case 10000: printf("10000\n"); break;
    case 20000: printf("20000\n"); break;
    default:    printf("default\n"); break;
    }
}

int main() {
    binary_search_switch(100);
    binary_search_switch(5000);
    binary_search_switch(300);  // default
    return 0;
}

运行结果与汇编逻辑完全一致。


总结

通过这次分析,我又巩固了几个点:

  • 稀疏、按序但跨度大的 switch case ,编译器常用二分搜索风格的比较树

  • 汇编里连续 cmp + 条件跳转 + push/ call + jmp 是典型特征。

  • 这种结构在逆向工程中非常好认,而且可以快速还原出等价 C 代码,理解程序逻辑。

这比直接看 jump table 更直观,也更节省空间。


相关推荐
九章-13 小时前
一库平替,融合致胜:国产数据库的“统型”范式革命
数据库·融合数据库
C澒13 小时前
面单打印服务的监控检查事项
前端·后端·安全·运维开发·交通物流
pas13613 小时前
39-mini-vue 实现解析 text 功能
前端·javascript·vue.js
qq_5324535313 小时前
使用 GaussianSplats3D 在 Vue 3 中构建交互式 3D 高斯点云查看器
前端·vue.js·3d
2401_8384725113 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
u01092727113 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
wengqidaifeng13 小时前
数据结构---顺序表的奥秘(下)
c语言·数据结构·数据库
what丶k13 小时前
SpringBoot3 配置文件使用全解析:从基础到实战,解锁灵活配置新姿势
java·数据库·spring boot·spring·spring cloud
Code blocks13 小时前
kingbase数据库集成Postgis扩展
数据库·后端
天下·第二13 小时前
达梦数据库适配
android·数据库·adb