你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C语言——条件判断语句

[复制链接]
gaosmile 发布时间:2020-6-19 22:48

在C语言中,条件判断语句是程序的重要组成部分,也是系统业务逻辑的控制手段,教科书告诉我们switch...case...语句比if...else if...else执行效率要高,但这到底是为什么呢?本文尝试从汇编的角度予以分析并揭晓其中的奥秘。

switch...case与if...else的根本区别
switch...case会生成一个跳转表来指示实际的case分支的地址,而这个跳转表的索引号与switch变量的值是相等的。从而,switch...case不用像if...else那样遍历条件分支直到命中条件,而只需访问对应索引号的表项从而到达定位分支的目的。
具体地说,switch...case会生成一份大小(表项数)为最大case常量+1的跳表,程序首先判断switch变量是否大于最大case 常量,若大于,则跳到default分支处理;否则取得索引号为switch变量大小的跳表项的地址(即跳表的起始地址+表项大小*索引号),程序接着跳到此地址执行,到此完成了分支的跳转。
第一步,写一个demo程序:foo.c#include <stdio.h>

static int
foo_ifelse(char c)
{
        if (c == '0' || c == '1') {
                c += 1;
        } else if (c == 'a' || c == 'b') {
                c += 2;
        } else if (c == 'A' || c == 'B') {
                c += 3;
        } else {
                c += 4;
        }

        return (c);
}

static int
foo_switch(char c)
{
        switch (c) {
                case '1':
                case '0': c += 1; break;
                case 'b':
                case 'a': c += 2; break;
                case 'B':
                case 'A': c += 3; break;
                default:  c += 4; break;
        }

        return (c);
}

int
main(int argc, char **argv)
{
        int m1 = foo_ifelse('0');
        int m2 = foo_ifelse('1');
        int n1 = foo_switch('a');
        int n2 = foo_switch('b');
        (void) printf("%c %c %c %c\n", m1, m2, n1, n2);
        return (0);
}

第二步,在Ubuntu上使用gcc编译
$ gcc -g -o foo foo.c
第三步,使用gdb对二进制文件foo反汇编 (使用intel语法)o 反汇编foo_ifelse()
(gdb) set disassembly-flavor intel
(gdb) disas /m foo_ifelse
Dump of assembler code for function foo_ifelse:
4       {
   0x0804841d <+0>:     push   ebp
   0x0804841e <+1>:     mov    ebp,esp
   0x08048420 <+3>:     sub    esp,0x4
   0x08048423 <+6>:     mov    eax,DWORD PTR [ebp+0x8]
   0x08048426 <+9>:     mov    BYTE PTR [ebp-0x4],al

5               if (c == '0' || c == '1') {
   0x08048429 <+12>:    cmp    BYTE PTR [ebp-0x4],0x30
   0x0804842d <+16>:    je     0x8048435 <foo_ifelse+24>
   0x0804842f <+18>:    cmp    BYTE PTR [ebp-0x4],0x31
   0x08048433 <+22>:    jne    0x8048441 <foo_ifelse+36>

6                       c += 1;
   0x08048435 <+24>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048439 <+28>:    add    eax,0x1
   0x0804843c <+31>:    mov    BYTE PTR [ebp-0x4],al
   0x0804843f <+34>:    jmp    0x804847b <foo_ifelse+94>

7               } else if (c == 'a' || c == 'b') {
   0x08048441 <+36>:    cmp    BYTE PTR [ebp-0x4],0x61
   0x08048445 <+40>:    je     0x804844d <foo_ifelse+48>
   0x08048447 <+42>:    cmp    BYTE PTR [ebp-0x4],0x62
   0x0804844b <+46>:    jne    0x8048459 <foo_ifelse+60>

8                       c += 2;
   0x0804844d <+48>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048451 <+52>:    add    eax,0x2
   0x08048454 <+55>:    mov    BYTE PTR [ebp-0x4],al
   0x08048457 <+58>:    jmp    0x804847b <foo_ifelse+94>

9               } else if (c == 'A' || c == 'B') {
   0x08048459 <+60>:    cmp    BYTE PTR [ebp-0x4],0x41
   0x0804845d <+64>:    je     0x8048465 <foo_ifelse+72>
   0x0804845f <+66>:    cmp    BYTE PTR [ebp-0x4],0x42
   0x08048463 <+70>:    jne    0x8048471 <foo_ifelse+84>

10                      c += 3;
   0x08048465 <+72>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048469 <+76>:    add    eax,0x3
   0x0804846c <+79>:    mov    BYTE PTR [ebp-0x4],al
   0x0804846f <+82>:    jmp    0x804847b <foo_ifelse+94>

11              } else {
12                      c += 4;
   0x08048471 <+84>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048475 <+88>:    add    eax,0x4
   0x08048478 <+91>:    mov    BYTE PTR [ebp-0x4],al

13              }
14
15              return (c);
   0x0804847b <+94>:    movsx  eax,BYTE PTR [ebp-0x4]

16      }
   0x0804847f <+98>:    leave
   0x08048480 <+99>:    ret

End of assembler dump.
(gdb)o 反汇编foo_ifelse()
(gdb) set disassembly-flavor intel
(gdb) disas /m foo_ifelse
Dump of assembler code for function foo_ifelse:
4       {
   0x0804841d <+0>:     push   ebp
   0x0804841e <+1>:     mov    ebp,esp
   0x08048420 <+3>:     sub    esp,0x4
   0x08048423 <+6>:     mov    eax,DWORD PTR [ebp+0x8]
   0x08048426 <+9>:     mov    BYTE PTR [ebp-0x4],al

5               if (c == '0' || c == '1') {
   0x08048429 <+12>:    cmp    BYTE PTR [ebp-0x4],0x30
   0x0804842d <+16>:    je     0x8048435 <foo_ifelse+24>
   0x0804842f <+18>:    cmp    BYTE PTR [ebp-0x4],0x31
   0x08048433 <+22>:    jne    0x8048441 <foo_ifelse+36>

6                       c += 1;
   0x08048435 <+24>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048439 <+28>:    add    eax,0x1
   0x0804843c <+31>:    mov    BYTE PTR [ebp-0x4],al
   0x0804843f <+34>:    jmp    0x804847b <foo_ifelse+94>

7               } else if (c == 'a' || c == 'b') {
   0x08048441 <+36>:    cmp    BYTE PTR [ebp-0x4],0x61
   0x08048445 <+40>:    je     0x804844d <foo_ifelse+48>
   0x08048447 <+42>:    cmp    BYTE PTR [ebp-0x4],0x62
   0x0804844b <+46>:    jne    0x8048459 <foo_ifelse+60>

8                       c += 2;
   0x0804844d <+48>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048451 <+52>:    add    eax,0x2
   0x08048454 <+55>:    mov    BYTE PTR [ebp-0x4],al
   0x08048457 <+58>:    jmp    0x804847b <foo_ifelse+94>

9               } else if (c == 'A' || c == 'B') {
   0x08048459 <+60>:    cmp    BYTE PTR [ebp-0x4],0x41
   0x0804845d <+64>:    je     0x8048465 <foo_ifelse+72>
   0x0804845f <+66>:    cmp    BYTE PTR [ebp-0x4],0x42
   0x08048463 <+70>:    jne    0x8048471 <foo_ifelse+84>

10                      c += 3;
   0x08048465 <+72>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048469 <+76>:    add    eax,0x3
   0x0804846c <+79>:    mov    BYTE PTR [ebp-0x4],al
   0x0804846f <+82>:    jmp    0x804847b <foo_ifelse+94>

11              } else {
12                      c += 4;
   0x08048471 <+84>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x08048475 <+88>:    add    eax,0x4
   0x08048478 <+91>:    mov    BYTE PTR [ebp-0x4],al

13              }
14
15              return (c);
   0x0804847b <+94>:    movsx  eax,BYTE PTR [ebp-0x4]

16      }
   0x0804847f <+98>:    leave
   0x08048480 <+99>:    ret

End of assembler dump.
(gdb)

o 反汇编foo_switch()
(gdb) set disassembly-flavor intel
(gdb) disas /m foo_switch
Dump of assembler code for function foo_switch:
20      {
   0x08048481 <+0>:     push   ebp
   0x08048482 <+1>:     mov    ebp,esp
   0x08048484 <+3>:     sub    esp,0x4
   0x08048487 <+6>:     mov    eax,DWORD PTR [ebp+0x8]
   0x0804848a <+9>:     mov    BYTE PTR [ebp-0x4],al

21              switch (c) {
   0x0804848d <+12>:    movsx  eax,BYTE PTR [ebp-0x4]
   0x08048491 <+16>:    sub    eax,0x30
   0x08048494 <+19>:    cmp    eax,0x32
   0x08048497 <+22>:    ja     0x80484c6 <foo_switch+69>
   0x08048499 <+24>:    mov    eax,DWORD PTR [eax*4+0x80485f0]
   0x080484a0 <+31>:    jmp    eax

22                      case '1':
23                      case '0': c += 1; break;
   0x080484a2 <+33>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x080484a6 <+37>:    add    eax,0x1
   0x080484a9 <+40>:    mov    BYTE PTR [ebp-0x4],al
   0x080484ac <+43>:    jmp    0x80484d1 <foo_switch+80>

24                      case 'b':
25                      case 'a': c += 2; break;
   0x080484ae <+45>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x080484b2 <+49>:    add    eax,0x2
   0x080484b5 <+52>:    mov    BYTE PTR [ebp-0x4],al
   0x080484b8 <+55>:    jmp    0x80484d1 <foo_switch+80>

26                      case 'B':
27                      case 'A': c += 3; break;
   0x080484ba <+57>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x080484be <+61>:    add    eax,0x3
   0x080484c1 <+64>:    mov    BYTE PTR [ebp-0x4],al
   0x080484c4 <+67>:    jmp    0x80484d1 <foo_switch+80>

28                      default:  c += 4; break;
   0x080484c6 <+69>:    movzx  eax,BYTE PTR [ebp-0x4]
   0x080484ca <+73>:    add    eax,0x4
   0x080484cd <+76>:    mov    BYTE PTR [ebp-0x4],al
   0x080484d0 <+79>:    nop

29              }
30
31              return (c);
   0x080484d1 <+80>:    movsx  eax,BYTE PTR [ebp-0x4]

32      }
   0x080484d5 <+84>:    leave
   0x080484d6 <+85>:    ret

End of assembler dump.
(gdb)

分析:
  • 在foo_ifelse()中,采用的方法是按顺序比较,如满足条件,则执行对应的代码,否则跳转到下一个分支再进行比较;
  • 在foo_switch()中,下面的这段汇编代码比较有意思,

..
21 switch (c) {
   0x0804848d <+12>:    movsx  eax,BYTE PTR [ebp-0x4]
   0x08048491 <+16>:    sub    eax,0x30
   0x08048494 <+19>:    cmp    eax,0x32
   0x08048497 <+22>:    ja     0x80484c6 <foo_switch+69>
   0x08048499 <+24>:    mov    eax,DWORD PTR [eax*4+0x80485f0]
   0x080484a0 <+31>:    jmp    eax
..

注意:
第17行 jmp eax
也就是说,当c的取值不同,是什么机制保证第17行能跳转到正确的位置开始执行呢?
第16行: eax = [eax * 4 + 0x80485f0]
搞清楚了从地址0x80485f0开始,对应的内存里面的内容也就回答了刚才的问题。
执行完第16行后,
  • 当c为'1'或'0'时, eax的值应该是0x080484a2;
  • 当c为'b'或'a'时, eax的值应该是0x080484ae;
  • 当c为'B'或'A'时, eax的值应该是0x080484ba;

通过gdb查看对应的内存,确实如此!
>>> ord('1') - 0x30
>>> ord('0') - 0x30
(gdb) x /2wx  0*4+0x80485f0
0x80485f0:    0x080484a2    0x080484a2

>>> ord('b') - 0x30
>>> ord('a') - 0x30
(gdb) x /2wx 49*4+0x80485f0
0x80486b4:    0x080484ae    0x080484ae
               
>>> ord('B') - 0x30
>>> ord('A') - 0x30
(gdb) x /2wx 17*4+0x80485f0
0x8048634:    0x080484ba    0x080484ba

那么,我们可以大胆的猜测,虽然c的取值不同但是跳转的IP确实是精准无误的,一定是编译阶段就被设定好了,果真如此吗?接下来分析一下对应的二进制文件foo,
第四步,使用objdump查看foo,$ objdump -D foo > /tmp/x

$ vim /tmp/x
509 Disassembly of section .rodata:
...
518  80485f0:       a2 84 04 08 a2          mov    %al,0xa2080484
519  80485f5:       84 04 08                test   %al,(%eax,%ecx,1)
...
534  8048630:       c6 84 04 08 ba 84 04    movb   $0x8,0x484ba08(%esp,%eax,1)
535  8048637:       08
536  8048638:       ba 84 04 08 c6          mov    $0xc6080484,%edx
...
566  80486b0:       c6 84 04 08 ae 84 04    movb   $0x8,0x484ae08(%esp,%eax,1)
567  80486b7:       08
568  80486b8:       ae                      scas   %es%edi),%al
569  80486b9:       84 04 08                test   %al,(%eax,%ecx,1)
...

在0x80485f0地址,存的8个字节正好是0x080484a2, 0x080484a2 (注意:按照小端的方式阅读)
在0x80486b4地址,存的8个字节正好是0x080484ae, 0x080484ae
在0x8048634地址,存的8个字节正好是0x080484ba,0x080484ba
果然不出所料,要跳转的IP的值正是在编译的时候存入了.rodata(只读数据区)。一旦foo开始运行,对应的内存地址就填写上了正确的待跳转地址,接下来只不过是根据c的取值计算出对应的IP存放的内存起始地址X,从X中取出待跳转的地址,直接跳转就好。
16    0x08048499 <+24>:    mov    eax,DWORD PTR [eax*4+0x80485f0]
17    0x080484a0 <+31>:    jmp    eax

到此为止,我们已经搞清楚了为什么switch...case...语句相对于if...else if...else...来说执行效率要高的根本原因。简言之,编译的时候创建了一个map存于.rodata区中,运行的时候直接根据输入(c的值)查表,找到对应的IP后直接跳转。(省去了cmp, jmp -> cmp, jmp -> cmp, jmp...这一冗长的计算过程。)
总结:
switch...case...执行效率高,属于典型的以空间换时间。也就是说,(套用算法的行话)以提高空间复杂度为代价降低了时间复杂度。

收藏 评论1 发布时间:2020-6-19 22:48

举报

1个回答
chat2019 回答时间:2020-6-20 12:23:11
赞一个

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版