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


相关推荐
焰火199916 分钟前
[Vue]可重置的响应式状态reactive
前端·vue.js
陆枫Larry17 分钟前
CSS transform scale:图片放大效果背后的原理
前端
猫的玖月25 分钟前
(一)MY SQL概述
数据库·sql
老王以为27 分钟前
为什么 React 和 Vue 不一样?
前端·vue.js·react.js
web打印社区29 分钟前
2026最新Web静默打印解决方案,无插件无预览,完美替代Lodop
前端·javascript·vue.js·electron·pdf
这个DBA有点耶44 分钟前
分组排名不用窗口函数?那你还在写几十行的子查询
前端·代码规范
ZhiqianXia1 小时前
《The Design of Design》阅读笔记
前端·笔记·microsoft
有马贵将1 小时前
【5】微前端知识点总结
前端·架构
mkae1 小时前
eBPF高性能版fail2ban
前端
脑子进水养啥鱼?1 小时前
PostgreSQL .history 文件
数据库·postgresql