常用语句汇编

  1. switch的汇编
    1. 形式一
    2. 形式二
    3. 形式三
    4. 形式四
  2. do while的汇编
  3. while的汇编
  4. for循环的汇编

switch的汇编

switch语句的汇编总共有4种

形式一

当分支少于4种时,其汇编本质上与if else相同,如下:

c代码:

void fun(int x) {//传入2
    switch(x) {
        case 1:
            printf("1");
            break;
        case 2:
            printf("2");
            break;
        case 3:
            printf("3");
            break;
        default:
            printf("error");
            break;
    }
}

汇编(主要部分):

0040D758   mov         eax,dword ptr [ebp+8]
0040D75B   mov         dword ptr [ebp-4],eax
0040D75E   cmp         dword ptr [ebp-4],1
0040D762   je          fun+32h (0040d772)
0040D764   cmp         dword ptr [ebp-4],2
0040D768   je          fun+41h (0040d781)
0040D76A   cmp         dword ptr [ebp-4],3
0040D76E   je          fun+50h (0040d790)
0040D770   jmp         fun+5Fh (0040d79f)
0040D772   push        offset string "1" (00422fa8)
0040D777   call        printf (0040d6c0)
0040D77C   add         esp,4
0040D77F   jmp         fun+6Ch (0040d7ac)
0040D781   push        offset string "2" (00422fa4)
0040D786   call        printf (0040d6c0)
0040D78B   add         esp,4
0040D78E   jmp         fun+6Ch (0040d7ac)
0040D790   push        offset string "%d " (0042210c)
0040D795   call        printf (0040d6c0)
0040D79A   add         esp,4
0040D79D   jmp         fun+6Ch (0040d7ac)
0040D79F   push        offset string "error" (00422f9c)

显然,对标与if else语句,当分支少于4种时,使用switch语句将没有意义。

形式二

而当分支情况大于等于4种时且case后的常量值连续,情况会完全不同:

c代码:

void fun(int x) {//传入2
    switch(x) {
        case 1:
            printf("1");
            break;
        case 2:
            printf("2");
            break;
        case 3:
            printf("3");
            break;
        case 4:
            printf("4");
            break;	
        default:
            printf("error");
            break;
    }
}

汇编(主要部分):

0040D818   mov         eax,dword ptr [ebp+8]
0040D81B   mov         dword ptr [ebp-4],eax
0040D81E   mov         ecx,dword ptr [ebp-4]
0040D821   sub         ecx,1
0040D824   mov         dword ptr [ebp-4],ecx
0040D827   cmp         dword ptr [ebp-4],3
0040D82B   ja          $L539+0Fh (0040d873)
0040D82D   mov         edx,dword ptr [ebp-4]
0040D830   jmp         dword ptr [edx*4+40D891h]
$L533:
0040D837   push        offset string "1" (00422fac)
0040D83C   call        printf (0040d6c0)
0040D841   add         esp,4
0040D844   jmp         $L539+1Ch (0040d880)
$L535:
0040D846   push        offset string "2" (00422fa8)
0040D84B   call        printf (0040d6c0)
0040D850   add         esp,4
0040D853   jmp         $L539+1Ch (0040d880)
$L537:
0040D855   push        offset string "3" (00422fa4)
0040D85A   call        printf (0040d6c0)
0040D85F   add         esp,4
0040D862   jmp         $L539+1Ch (0040d880)
$L539:
0040D864   push        offset string "%d " (0042210c)
0040D869   call        printf (0040d6c0)
0040D86E   add         esp,4
0040D871   jmp         $L539+1Ch (0040d880)
0040D873   push        offset string "error" (00422f9c)
0040D878   call        printf (0040d6c0)

可以看到,编译器在将参数2传入ecx后减1,再与3比较,若大于3,则直接输出error,否则通过表达式[edx*4+40D891h]进行跳转。

我们通过看具体地址存储的内容,可以知道:

[edx*4+40D891h]分别代表了每个分支的首地址(edx=0,1,2)。

switch语句的汇编过程:

编译器会在编译时生成一个大表,用来存储每个分支的首地址,通过特定的表达式来直接进行跳转(表达式是否固定?)。

当case后的常量值可以无序,不影响大表的生成,如下:

void fun(int x) {//传入2
    switch(x) {
        case 4:
            printf("4");
            break;
        case 2:
            printf("2");
            break;
        case 3:
            printf("3");
            break;
        case 1:
            printf("1");
            break;	
        default:
            printf("error");
            break;
    }
}

所以,在分配每个分支的地址时,编译器会采取某种排序算法。

形式三

当分支情况大于等于4种时且case后的常量值总体不连续但有规律,情况又会不同:

c代码:

void fun(int x){
    switch(x){
        case 1:
            printf("1");
            break;
        case 4:
            printf("4");
            break;
        case 5:
            printf("5");
            break;
        case 6:
            printf("6");
            break;
        case 7:
            printf("7");
            break;
        case 8:
            printf("8");
            break;
        case 9:
            printf("9");
            break;
        case 10:
            printf("10");
            break;
        default:
            printf("error");
            break;
    }
}

把等于2和3的情况删除后,case 2和case 3对应的地址上的地址值将被替换为default的地址值(default没写的时候或许是跳出switch)。

0040D830   jmp         dword ptr [edx*4+40D8BEh]

image-20230813202251990

显然当删除的情况过多时,将删除的情况对应的地址替换成default的地址这种做法过于浪费空间。此时,会有另外的做法:

当我删除6种情况(不同编译器上限不同)

void fun(int x){
    switch(x){
        case 1:
            printf("1");
            break;
        case 8:
            printf("8");
            break;
        case 9:
            printf("9");
            break;
        case 10:
            printf("10");
            break;
        default:
            printf("error");
            break;
    }
}

对应汇编(主要部分):

0040D818   mov         eax,dword ptr [ebp+8]
0040D81B   mov         dword ptr [ebp-4],eax
0040D81E   mov         ecx,dword ptr [ebp-4]
0040D821   sub         ecx,1
0040D824   mov         dword ptr [ebp-4],ecx
0040D827   cmp         dword ptr [ebp-4],9
0040D82B   ja          $L539+0Fh (0040d87b)
0040D82D   mov         eax,dword ptr [ebp-4]
0040D830   xor         edx,edx
0040D832   mov         dl,byte ptr  (0040d8ad)[eax]
0040D838   jmp         dword ptr [edx*4+40D899h]

image-20230813204022664

(0040d8ad)即为小表,且小表紧挨着大表,此时大表中的被替换的部分会消除,目的是为了节省内存。操作过程,假设我们传入的参数为2:

减1后传给eax,edx清0,查小表,将小表中第eax个字节(dl是8位)位置的值传给edx,再去查询大表,因为case 2是不存在的,所以会直接跳到default处。

形式四

当分支情况大于等于4种时且case后的常量值毫无规律时。此时编译器会按if else语句处理。

do while的汇编

c代码(传入参数2):

void fun(int x){
    do {
        printf("hello!");
        x--;
    }while(x>0);
}

汇编:

0040D818   push        offset string "hello!" (00422f9c)
0040D81D   call        printf (0040d6c0)
0040D822   add         esp,4
0040D825   mov         eax,dword ptr [ebp+8]
0040D828   sub         eax,1
0040D82B   mov         dword ptr [ebp+8],eax
0040D82E   cmp         dword ptr [ebp+8],0
0040D832   jg          fun+18h (0040d818)

do while语句有下面的特点:

  • 根据条件跳转指令所跳转到的地址,可以得到循环语句块的起始地址
  • 条件跳转的逻辑与源码相同

while的汇编

c代码(传入参数2)

    while(x>0) {
        printf("hello!");
        x--;
    }

汇编:

0040D818   cmp         dword ptr [ebp+8],0
0040D81C   jle         fun+36h (0040d836)
0040D81E   push        offset string "hello!" (00422f9c)
0040D823   call        printf (0040d6c0)
0040D828   add         esp,4
0040D82B   mov         eax,dword ptr [ebp+8]
0040D82E   sub         eax,1
0040D831   mov         dword ptr [ebp+8],eax
0040D834   jmp         fun+18h (0040d818)

while语句有下面的特点:

  • 根据条件跳转指令所跳转到的地址,可以得到循环语句块的结束地址;
  • 根据jmp 指令所跳转到的地址,可以得到循环语句块的起始地址
  • 在还原while比较时,条件跳转的逻辑与源码相反

for循环的汇编

c代码(传入参数2)

void fun(int x){
    for(int i=0; i<x; i++)
        printf("hello!");
}

汇编:

0040D818   mov         dword ptr [ebp-4],0
0040D81F   jmp         fun+2Ah (0040d82a)
0040D821   mov         eax,dword ptr [ebp-4]
0040D824   add         eax,1
0040D827   mov         dword ptr [ebp-4],eax
0040D82A   mov         ecx,dword ptr [ebp-4]
0040D82D   cmp         ecx,dword ptr [ebp+8]
0040D830   jge         fun+41h (0040d841)
0040D832   push        offset string "hello!" (00422f9c)
0040D837   call        printf (0040d6c0)
0040D83C   add         esp,4
0040D83F   jmp         fun+21h (0040d821)
0040D841   pop         edi
0040D842   pop         esi
0040D843   pop         ebx

for循环语句有下面的特点:

第一个jmp指令之前为赋初值部分
第一个jmp指令所跳转的地址为循环条件判定部分起始
判断条件后面的跳转指令条件成立时跳转的循环体外面
条件判断跳转指令所指向的地址上面有一个jmp,jmp地址为表达式3(第二个分号后的表达式)的起始位置


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1666739907@qq.com
github