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 更直观,也更节省空间。


相关推荐
hunterandroid几秒前
[鸿蒙从零到一] HarmonyOS 媒体能力实战:图片、音频与视频处理
前端
打呵欠的猫3 分钟前
我让 AI 封装了一个 ImageUpload 组件,它设计的 5 层校验链路比我想的周全
前端·ai编程
AlexMaybeBot6 分钟前
躺在沙发上开发 Openclaw 的移动端APP
前端·flutter
用户2181697049307 分钟前
Flutter(十三)Text Image TextField
前端
程序员黑豆12 分钟前
深入解析Java数据类型:基本类型与引用类型的本质区别与实战选择
前端·ai编程·全栈
范什么特西13 分钟前
redis题目面渣重点
数据库·redis·缓存
TDengine (老段)1 小时前
TDengine taosAdapter — 多协议网关详解
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
Nturmoils1 小时前
SQL Server 迁移到 KingbaseES:一次复杂 BI 查询的性能实测
数据库
东方小月1 小时前
从零开发一个 Coding Agent(七):实现纯文本 Agent Loop
前端·人工智能
大家的林语冰1 小时前
👉 尤雨溪再次成立新公司,同时官宣 Pinia 4 正式发布!
前端·javascript·vue.js