5.14 汇编语言:仿写Switch选择结构

简介: 选择结构,也称为switch语句,是计算机编程中的一种控制结构,用于根据表达式的值选择不同的执行路径。它允许程序根据表达式的值来决定执行哪个代码块,从而实现多分支选择逻辑。switch语句由一个表达式、多个case标签以及对应的代码块组成。程序会将表达式的值与每个case标签进行匹配,一旦找到匹配的case标签,程序将执行对应的代码块,并继续执行该代码块之后的代码,直到遇到break语句或者switch语句结束。

选择结构,也称为switch语句,是计算机编程中的一种控制结构,用于根据表达式的值选择不同的执行路径。它允许程序根据表达式的值来决定执行哪个代码块,从而实现多分支选择逻辑。switch语句由一个表达式、多个case标签以及对应的代码块组成。程序会将表达式的值与每个case标签进行匹配,一旦找到匹配的case标签,程序将执行对应的代码块,并继续执行该代码块之后的代码,直到遇到break语句或者switch语句结束。

11.25 仿写有序线性优化

在switch分支数小于4的情况下,编译器将采用模拟IF-ELSE分支的方式构建SWITCH结构,这样则无法发挥出SWITCH语句的优势,当分支数大于3并且case的判断值存在明显线性关系时,Switch语句的优化特性才可以被凸显出来。

该优化方式将每个case语句块的首地址预先保存在数组(地址表)中,并依据寻址时传入的下标(下标以0开头),在此数组中查询case语句块对应的首地址,取出首地址并跳转到指定分支上,并执行分支流程代码。

int main(int argc, char *argv[])
{
  int index = 1;
  switch (index)
  {
  case 1:
    printf("index 1"); break;
  case 2:
    printf("index 2"); break;
  case 3:
    printf("index 3"); break;
  case 6:
    printf("index 6"); break;
  case 7:
    printf("index 7"); break;
  default:
    printf("default"); break;
  }
  return 0;
}

首先我们手动构建MemArray跳转地址表,通过offset伪指令取出分支内存地址,并将分支地址拷贝到MemArray跳转表中,因为分支语句下标从0开始,所以需要dec ecx减去1,在进入switch语句之前,判断输入的下标是否高于6,如果高于则直接跳出switch,否则执行jmp dword ptr ds:[ecx * 4 +MemArray]寻址并跳转到相应的分支上。

    .386p
    .model flat,stdcall
    option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
    MemArray DWORD 0,0,0,0,0,0,0dh
    InputNumber DWORD ?

.code
    main PROC
      ; 寻找第2个分支语句
      mov dword ptr ds:[InputNumber],2

      ; 填充跳转地址表
      mov dword ptr ds:[MemArray],offset S0
      mov dword ptr ds:[MemArray + 4],offset S1
      mov dword ptr ds:[MemArray + 8],offset S2
      mov dword ptr ds:[MemArray + 12],offset S3
      mov dword ptr ds:[MemArray + 16],offset S4
      mov dword ptr ds:[MemArray + 20],offset S5
      mov dword ptr ds:[MemArray + 24],offset Send

      ; 判断下标是否高于6高于则结束switch
      mov ecx,dword ptr ds:[InputNumber]
      dec ecx                         ; Switch语句获取比例因子,需要减1
      cmp ecx,6                       ; 首先对比输入数据是否大于6
      ja lop_end                      ; 大于则说明Switch分支无对应结构 (则直接跳转到结束)

      jmp dword ptr ds:[ecx * 4 +MemArray] ; 否则直接寻址,找到对应的内存地址

    S0:
      mov eax,0
      jmp lop_end

    S1:
      mov eax,1
      jmp lop_end

    S2:
      mov eax,2
      jmp lop_end

    S3:
      mov eax,3
      jmp lop_end

    S4:
      mov eax,4
      jmp lop_end

    S5:
      mov eax,5
      jmp lop_end

    Send:
      mov eax,6
      jmp lop_end

    lop_end:
      int 3

    main ENDP
END main

11.26 仿写非线性索引优化

如果两个case值间隔较大,仍然使用switch的结尾地址或default地址代替地址表中缺少的case地址,这样则会造成极大的空间浪费,对于这种非线性结构,可采用制作索引表的方式进行优化,但此方式需要通过索引表来查询地址表,会多出一次查表的过程,因此效率上会有所下降。

索引表需要两张表:

  • case 语句块地址表:地址表中每一项保存一个case语句块首地址,有几个case语句块或default语句块,就保存几项,结束地址在地址表中只会保存一份。

  • case 语句块索引表:索引表中保存了地址表中的下标值,索引表最多可容纳256项,每项1字节,所以case值不可超过1字节,索引表也只能存储256项索引编号。

我们在上方C代码基础上稍加改动,如下分支结构4,5默认是不存在的,也就是当用户选择4或5时,默认会执行6号分支,如果单独为4,5创建一个4字节存储,分支偏差小还能接受,一旦分支偏差过大,则会占用大量内存空间,此时就需要使用索引表来优化空间占用。

int main(int argc, char *argv[])
{
  int index = 5;
  switch (index)
  {
  case 1:
    printf("index 1"); break;
  case 2:
    printf("index 2"); break;
  case 3:
    printf("index 3"); break;
  case 6:
    printf("index 6"); break;
  case 7:
    printf("index 7"); break;
  }
  return 0;
}

这段C代码如果改成非线性优化则会呈现以下类型的汇编指令,与地址表差不多,索引表MemIndexTable中每一个字节对应一个地址表MemAddressTable的下标,如果该索引在4,5范围内,则会默认指向地址表MemAddressTable中同一块内存区域,这样即可解决内存资源浪费的问题。

    .386p
    .model flat,stdcall
    option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
    MemAddressTable DWORD 0,0,0,0,0,0dh
    MemIndexTable BYTE 0,0,0,0,0,0,0,0dh
    InputNumber DWORD ?

.code
    main PROC

      ; 寻找第5个分支语句 对应S3
      mov dword ptr ds:[InputNumber],5

      ; 填充地址表
      mov dword ptr ds:[MemAddressTable],offset S0
      mov dword ptr ds:[MemAddressTable + 4],offset S1
      mov dword ptr ds:[MemAddressTable + 8],offset S2
      mov dword ptr ds:[MemAddressTable + 12],offset S3
      mov dword ptr ds:[MemAddressTable + 16],offset S4

      ; 填充索引表
      mov byte ptr ds:[MemIndexTable],0            ; 对应S0
      mov byte ptr ds:[MemIndexTable + 1],1        ; 对应S1
      mov byte ptr ds:[MemIndexTable + 2],2        ; 对应S2
      mov byte ptr ds:[MemIndexTable + 3],3        ; 对应S3
      mov byte ptr ds:[MemIndexTable + 4],3        ; 对应S3
      mov byte ptr ds:[MemIndexTable + 5],3        ; 对应S3
      mov byte ptr ds:[MemIndexTable + 6],4        ; 对应S4

      ; 得到索引表的基地址
      lea edx,MemIndexTable

      ; 定位到第5个分支上
      mov ecx,dword ptr ds:[InputNumber]
      dec ecx
      cmp ecx,7
      ja lop_end

      ; 关键定位算法 索引表找下标,下标找函数地址
      movzx eax,byte ptr ds:[ecx + edx]                 ; 从索引表找地址表下标
      jmp dword ptr ds:[eax * 4 + MemAddressTable]      ; 比例因子寻找函数地址

    S0:
      mov eax,0
      jmp lop_end

    S1:
      mov eax,1
      jmp lop_end

    S2:
      mov eax,2
      jmp lop_end

    S3:
      mov eax,3
      jmp lop_end

    S4:
      mov eax,4
      jmp lop_end

    lop_end:
      int 3

    main ENDP
END main

11.27 仿写平衡判定树优化

当最大case值与最小case值之差大于255时,则会采用判定树优化,将每个case值作为一个节点,从节点中找出中间值作为根节点,以此形成一颗平衡二叉树,以每个节点为判定值,大于和小于关系分别对应左子树和右子树,从而提高查询效率。

如果打开编译器体积优先,编译器尽量会以二叉判定树的方式来降低程序占用体积,如果无法使用前两种优化方式时,则需要将switch做成一棵树,首先编译C代码。

int main(int argc, char *argv[])
{
  int index = 15;
  switch (index)
  {
  case 2:
    printf("index 2"); break;
  case 3:
    printf("index 3"); break;
  case 8:
    printf("index 8"); break;
  case 10:
    printf("index 10"); break;
  case 35:
    printf("index 35"); break;
  case 37:
    printf("index 37"); break;
  case 666:
    printf("index 666"); break;
  }
  return 0;
}

判定树,通过增加多条分支结构,从中位数10开始判断,大于走左子树或小于走右子树分支,直到遇到符合条件的分支为止,这段汇编代码编写时应格外注意次序,否则容易写乱套,不论如何本人还是按照编译器习惯将其转换为了对等汇编语句。

    .386p
    .model flat,stdcall
    option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.data
    InputNumber DWORD ?
    ; 排列后 左子树大/右子树小 666 37 35 [10] 2 3 8

.code
    main PROC

      mov dword ptr ds:[InputNumber],40

      ; 先判断输入的数据是否大于最大分支
      mov eax,666
      cmp dword ptr ds:[InputNumber],eax
      jg lop_end

      ; 取出中位数10作为第一个判定条件
      cmp dword ptr ds:[InputNumber],10
      jg left
      je S10

      cmp dword ptr ds:[InputNumber],8     ; 对比8
      jle S8

      cmp dword ptr ds:[InputNumber],7     ; 对比7
      jle S37

      cmp dword ptr ds:[InputNumber],2     ; 对比2
      jle S2
      jmp lop_end

    left:
      cmp dword ptr ds:[InputNumber],35     ; 对比35
      jle S35

      cmp dword ptr ds:[InputNumber],37     ; 对比37
      jle S37

      cmp dword ptr ds:[InputNumber],666    ; 对比666
      jle S35
      jmp lop_end

    S2:
      mov eax,2
      jmp lop_end

    S3:
      mov eax,3
      jmp lop_end

    S8:
      mov eax,8
      jmp lop_end

    S10:
      mov eax,10
      jmp lop_end

    S35:
      mov eax,35
      jmp lop_end

    S37:
      mov eax,37
      jmp lop_end

    S666:
      mov eax,666
      jmp lop_end

    lop_end:
      int 3

    main ENDP
END main

为了降低判定树的高度,在优化过程中,会检查代码是否满足if-else优化,有序线性优化,非线性索引优化,利用三种优化来降低树高度,谁的效率高就优先使用谁,如果三种优化都无法匹配才会使用判定树。

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/c44b7f4.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

目录
相关文章
|
9月前
|
搜索推荐 算法 编译器
5.13 汇编语言:仿写For循环语句
循环语句(for)是计算机编程中的一种基本控制结构,它允许程序按照指定的次数或范围重复执行一段代码块。for循环在处理需要进行迭代操作的情况下非常有用,它使得程序可以更加方便地控制循环的次数。一般来说,for循环由三个部分组成:初始化部分、条件表达式和更新部分,以及一个需要重复执行的代码块。在每次循环迭代开始时,程序首先执行初始化部分,然后检查条件表达式的值,如果为真,则执行代码块,并在每次循环结束后执行更新部分。只要条件表达式为真,for循环就会一直重复执行;一旦条件表达式为假,循环将停止,程序继续执行循环之后的代码。
57 0
|
9月前
|
算法 编译器 C++
5.12 汇编语言:仿写While循环语句
循环语句(While)一种基本控制结构,它允许程序在条件为真的情况下重复执行一段代码块,直到条件为假为止。循环语句在处理需要重复执行的任务时非常有用,它可以让程序更加高效地处理大量数据或者重复性操作。一般来说,While循环由一个条件表达式、一个代码块组成。在每次循环迭代开始时,程序会首先检查条件表达式的值,如果为真,则执行代码块,然后再次检查条件表达式的值。只要条件表达式为真,循环就会一直继续执行;一旦条件表达式为假,循环将停止,程序继续执行循环之后的代码。
84 0
|
9月前
|
编译器 C语言 C++
5.11 汇编语言:仿写IF条件语句
条件语句,也称为IF-ELSE语句,是计算机编程中的一种基本控制结构。它允许程序根据条件的真假来执行不同的代码块。条件语句在处理决策和分支逻辑时非常有用。一般来说,条件语句由IF关键字、一个条件表达式、一个或多个代码块以及可选的ELSE关键字和对应的代码块组成。条件表达式的结果通常是布尔值(True或False),决定了程序将执行IF代码块还是ELSE代码块。
64 0
|
编译器 C++
Win32汇编:仿写多态与虚函数
多态性是面向对象的重要组成部分,利用多态可以设计和实现易于扩展的程序,所谓多态就是一个类函数有多重形态,具有不同功能的函数可以用同一个函数名,实现使用一个函数名调用不同内容的函数,从而返回不同的结果,这就是多态性,多态离不开虚函数的支撑,以下案例本人将深度分析虚函数实现机制,并通过汇编实现虚函数机制。
173 0
|
5天前
|
存储 C++
C/C++中的整数除法运算与汇编指令DIV和IDIV
C/C++中的整数除法运算与汇编指令DIV和IDIV
15 1
|
5天前
|
存储 安全 程序员
C/C++中的整数乘法运算与汇编指令MUL和IMUL
C/C++中的整数乘法运算与汇编指令MUL和IMUL
10 0
|
5天前
|
编译器 C++
C/C++中的逻辑运算与汇编指令的交互
C/C++中的逻辑运算与汇编指令的交互
7 0
|
5天前
|
编译器 程序员 C++
C/C++逻辑与运算与汇编指令的关系
C/C++逻辑与运算与汇编指令的关系
9 0
|
5天前
|
程序员
汇编语言中的带符号比较跳转指令
汇编语言中的带符号比较跳转指令
11 0
|
5天前
|
程序员
深入理解汇编:平栈、CALL和RET指令详解
深入理解汇编:平栈、CALL和RET指令详解
10 1