C语言switch语句的汇编语言实现

时间:2022-06-01 18:56:09

一个好的编译器一定是一群顶尖软件高手们集体长时间创作的作品了,所以研究研究编译器的编译过程就是在向这些高手们学习。说到底,编译器也是一个很好的老师了,它可以为我们揭开高级语言实现的奥秘,为那些永不满足于表面现象的程序员深入掌握一门语言提供一个很好的途径。 来看看微软的cl 8.0编译器是怎样来编译C语言的switch语句的,照例写一个测试的例子:

int test()

{

    int i,j;

    i=j=0;

   switch(i)

    {

       case 1:j+=1;break;

       case 2:j+=2;break;

       default:j+=5;

    }

    return 0;

}

 

看看生成的汇编代码:

; 5    :

; 6    :    switch(i)

    mov ecx, DWORD PTR _i$[ebp]

    mov DWORDPTR tv64[ebp], ecx

;case 1:

cmp DWORD PTR tv64[ebp], 1

    je  SHORT$LN3@test

;case 2:

cmp DWORD PTR tv64[ebp], 2

    je  SHORT$LN2@test

;default:

    jmp SHORT $LN1@test

$LN3@test:

; 7    :    {

; 8    :       case 1:j+=1;break;

    mov edx, DWORD PTR _j$[ebp]

    add edx, 1

    mov DWORDPTR _j$[ebp], edx

    jmp SHORT $LN4@test

$LN2@test:

; 9    :       case 2:j+=2;break;

    mov eax, DWORD PTR _j$[ebp]

    add eax, 2

    mov DWORDPTR _j$[ebp], eax

    jmp SHORT $LN4@test

$LN1@test:

; 10   :       default:j+=5;

    mov ecx, DWORD PTR _j$[ebp]

    add ecx, 5

    mov DWORDPTR _j$[ebp], ecx

$LN4@test:

; 11   :    }

; 12   :

; 13   :    return 0;

    xor eax, eax

; 14   : }

 

 

这段汇编代码是很好理解的,就是比较并跳转的过程。

 

下面看看一个比较复杂的switch

int test()

{

    int i,j;

    i=j=0;

   switch(i)

    {

       case 1:j+=1;break;

       case 2:j+=2;break;

       case 3:j+=3;break;

       case 4:j+=4;break;

       default:j+=10;

    }

 

    return 0;

}

再来看看编译器生成的汇编代码:

 

; 5    :

; 6    :    switch(i)

 

    mov ecx, DWORD PTR _i$[ebp]

    mov DWORDPTR tv64[ebp], ecx

    mov edx, DWORD PTR tv64[ebp]

    sub edx, 1

    mov DWORDPTR tv64[ebp], edx

    cmp DWORD PTR tv64[ebp], 3

    ja  SHORT$LN1@test

    mov eax, DWORD PTR tv64[ebp]

    jmp DWORDPTR $LN10@test[eax*4]

;以上的汇编代码相当于以下的伪指令:

if(i-1 > 3)

{

    goto $LN1@test;//跳转到default

}

else

{

    goto $LN10@test[i-1];//case 1,case 2,case 3,case 4的情况

}

$LN10@test是一个跳转表,通过它就可以跳转到相应的处理代码中去,看来微软的编译器还是很聪明的,会在编译时对case的值作一些判断,这也就是所谓的优化吧。

 

$LN5@test:

; 7    :    {

; 8    :       case 1:j+=1;break;

    mov ecx, DWORD PTR _j$[ebp]

    add ecx, 1

    mov DWORDPTR _j$[ebp], ecx

    jmp SHORT $LN6@test

$LN4@test:

; 9    :       case 2:j+=2;break;

    mov edx, DWORD PTR _j$[ebp]

    add edx, 2

    mov DWORDPTR _j$[ebp], edx

    jmp SHORT $LN6@test

$LN3@test:

; 10   :       case 3:j+=3;break;

    mov eax, DWORD PTR _j$[ebp]

    add eax, 3

    mov DWORDPTR _j$[ebp], eax

    jmp SHORT $LN6@test

$LN2@test:

; 11   :       case 4:j+=4;break;

    mov ecx, DWORD PTR _j$[ebp]

    add ecx, 4

    mov DWORDPTR _j$[ebp], ecx

    jmp SHORT $LN6@test

$LN1@test:

; 12   :       default:j+=10;

    mov edx, DWORD PTR _j$[ebp]

    add edx, 10                  ; 0000000aH

    mov DWORD PTR _j$[ebp], edx

$LN6@test:

 

; 13   :    }

......

$LN10@test:

    DD  $LN5@test          ;case 1

    DD  $LN4@test         ;case 2

    DD  $LN3@test         ;case 3

    DD  $LN2@test         ;case 4

 

 

 

 

下面来看看加大各case的差别会带来怎样的差异:

int test()

{

    int i,j;

    i=j=0;

   switch(i)

    {

       case 1:j+=1;break;

       case 2:j+=2;break;

       case 9:j+=3;break;

       case 15:j+=4;break;

       default:j+=10;

    }

    return 0;

}

来看看编译器产生的汇编代码:

_test  PROC

 

; 2    : {

.....

 

; 5    :

; 6    :    switch(i)

 

    mov ecx, DWORD PTR _i$[ebp]

    mov DWORDPTR tv64[ebp], ecx

    mov edx, DWORD PTR tv64[ebp]

    sub edx, 1

    mov DWORDPTR tv64[ebp], edx

    cmp DWORD PTR tv64[ebp], 14         ; 0000000eH

    ja  SHORT $LN1@test

    mov eax, DWORD PTR tv64[ebp]

    movzx  ecx, BYTE PTR $LN10@test[eax]

    jmp DWORDPTR $LN11@test[ecx*4]

以上的语句相当于下面的代码:

if(i-1>=14)

{

    goto $LN1@test ;//跳转到switch语句的defaul分支

}

else

{

    index = $LN10@test[i-1];//$LN10@test为跳转索引表

    goto $LN11@test[index];//$LN11@teest为跳转表

}

可见,对于case的值有较大的差别的情况编译器还会新增一个跳转索引表,先通过case值找到对应的跳转表的索引,然后通过索引再找到跳转表中的跳转地址。

$LN5@test:

 

; 7    :    {

; 8    :       case 1:j+=1;break;

 

    mov edx, DWORD PTR _j$[ebp]

    add edx, 1

    mov DWORDPTR _j$[ebp], edx

    jmp SHORT $LN6@test

$LN4@test:

 

; 9    :       case 2:j+=2;break;

 

    mov eax, DWORD PTR _j$[ebp]

    add eax, 2

    mov DWORDPTR _j$[ebp], eax

    jmp SHORT $LN6@test

$LN3@test:

 

; 10   :       case 9:j+=3;break;

 

    mov ecx, DWORD PTR _j$[ebp]

    add ecx, 3

    mov DWORDPTR _j$[ebp], ecx

    jmp SHORT $LN6@test

$LN2@test:

 

; 11   :       case 15:j+=4;break;

 

    mov edx, DWORD PTR _j$[ebp]

    add edx, 4

    mov DWORDPTR _j$[ebp], edx

    jmp SHORT $LN6@test

$LN1@test:

 

; 12   :       default:j+=10;

 

    mov eax, DWORD PTR _j$[ebp]

    add eax, 10                  ; 0000000aH

    mov DWORD PTR _j$[ebp], eax

$LN6@test:

 

; 13   :    }

; 14   :

; 15   :    return 0;

 

    xor eax, eax

 

; 16   : }

 

    mov esp, ebp

    pop ebp

    ret 0

$LN11@test:

    DD  $LN5@test

    DD  $LN4@test

    DD  $LN3@test

    DD  $LN2@test

    DD  $LN1@test

$LN10@test:

    DB  0

    DB  1

    DB  4

    DB  4

    DB  4

    DB  4

    DB  4

    DB  4

    DB  2

    DB  4

    DB  4

    DB  4

    DB  4

    DB  4

    DB  3

_test  ENDP

 

 

我们再来进一步的加大case值的差别:

int test()

{

    int i,j;

    i=j=0;

 

    switch(i)

    {

       case 1:j+=1;break;

       case 100:j+=2;break;

       case 250:j+=3;break;

       case 550:j+=4;break;

       default:j+=10;

    }

 

    return 0;

}

 

来看看生成的汇编代码片断:

; 5    :

; 6    :    switch(i)

 

    mov ecx, DWORD PTR _i$[ebp]

    mov DWORDPTR tv64[ebp], ecx

;大于250则跳转到$LN10@test,case 550 的情况

    cmp DWORD PTR tv64[ebp], 250    ; 000000faH

    jg  SHORT $LN10@test

case 250

    cmp DWORD PTR tv64[ebp], 250    ; 000000faH

    je  SHORT $LN3@test

;case 1

    cmp DWORD PTR tv64[ebp], 1

    je  SHORT$LN5@test

;case 100

    cmp DWORD PTR tv64[ebp], 100    ; 00000064H

    je  SHORT $LN4@test

;其它情况跳转到default处的代码

    jmp SHORT $LN1@test

$LN10@test:

;case 550

    cmp DWORD PTR tv64[ebp], 550    ; 00000226H

    je  SHORT $LN2@test

;其它情况跳转到default处的代码

    jmp SHORT $LN1@test

$LN5@test:

 

哈哈,这与第一种情况很相似吧!好端端的代码被编译器处理得有点复杂,但是又滴水不漏。当然,编译器也是先判断了具体的值以后才对进行处理的。这时候,编译器一定在衡量了时间与空间的损失后作出这样的决定的,如果用第三种查表的方法继续编译空间损失会很大,需要一个很长的表格,在这种情况下,编译器选择了损失时间而节省空间。