大会 Jump/Branch/Lookup 表而不是很多 cmp/je?

Assembly Jump/Branch/Lookup Tables instead of lots of cmp/je?

我刚刚开始学习汇编,并且正在制作一个简单的引导加载程序作为我 OS class 的一部分。我试图让我的代码更高效一点,即我认为到目前为止我所做的并不是实现我想要的东西的特别好的方法。也就是说,我一直在努力在网上找到任何记录 jump/branch/lookup table 的资源,我认为这是最有效的方法。

为了解释我想要实现的目标,我正在调用一个函数,该函数 return 是 dx 寄存器中的一个值,从 0 到 4。目前我正在使用 cmp指令一个接一个地比较值,如果值相同则进行条件je跳转。如果我用更高级的语言编写它,我基本上会一个接一个地执行多个 if 语句,而不是使用更高效的 switch 语句。

所以这就是我现在正在做的事情:

cmp dx, 1          
je .F_1
cmp dx, 2
je .F_2
cmp dx, 3
je .F_3
cmp dx, 4
je .F_4
cmp dx, 0
je .F_5
jmp RangeError_Handler

.F1:
  mov   si, msg1
  jmp   F_Exit
.F2:
  mov   si, msg2
  jmp   F_Exit
...  ; .F3 and .F4 follow the pattern

.F5:             ; special case
  mov   si, msg_error
  call  PrintLn
  hlt

F_Exit:
  call  PrintLn
  ...            ; and do something else


msg1: db 'Message 1', 0
msg2: ...
...

必须有更好的方法来做到这一点。我的导师暗示跳转 table 是理想的,但没有时间给我任何关于它在汇编中如何工作的进一步解释,所以如果有人能提供某种形式,我将非常感激以我的情况为例。

理论上,我有一个函数检查 dx 的值,然后跳转到一个特定的函数,而不是分别检查 5 次,我只是看不出如何在汇编中实现它。对字符串也使用查找 table 会更有效吗?即 return 值为 1 表示 table?

中的字符串 1

您的大多数案例都有相同的指令和不同的数据,因此您甚至不需要跳转 table。只用table个字符串,只跳转不同指令需要运行的条件,而不是相同指令不同数据

    mov  si, dx                   ; SI can be used in addressing modes, DX can't
    shl  si                       ; 16-bit doesn't allow scaled indices, so we can't just do [table + si*2].  And shl sets flags
    cmp  dx, 4
    ja   RangeError_Handler

    mov  si, [F_messages + si]
      ; call PrintLn   could be here, if it preserves DX or SI for us to test after

    test dx,dx             ; detect the one special case.
    jnz  .F_Exit

    ;; fall through only in the dx==0 case
    call  PrintLn
RangeError_Handler:
    hlt                             ; Are interrupts disabled?  if not, execution will continue after hlt

.F_exit
    call  PrintLn
    ...   ; and do whatever else your code needs to do


F_messages:                # char* F_messages[]
    dw  msg1,
        msg2
        ...

使用table代替一连串的条件跳转是非常广泛适用的。如果这是 64 位 x86 代码,甚至是 ARM 或 MIPS 汇编,逻辑将几乎相同。甚至是 C。(一个好的 C 编译器可能会将您的 switch 转换为 table 数据查找而不是跳转 table)。


您可以将 call PrintLn 从分支的两侧分解出来,但前提是它保留 DX 或 SI。如果您必须 PUSH/POP 输入值以便能够再次测试它,那将是不值得的。由于特殊情况是 DX==0,(不像这个答案的前一版本那样是 DX==5),我们不能用来自一个 CMP 的 FLAGS 做两个 JCC。


如果你确实想跳跃table:

jmp  [jump_table + si]


jump_table:
   dw   .F_1,  .F_2, ...

然后使用 DW 在内存中创建 table 代码地址而不是字符串地址。如果每种情况的大小相同(以机器代码字节为单位),则可以避免使用 table 个指针,而只需计算相对于第一个地址的跳转距离。


确保在使用绝对地址 之前知道CS 的设置。普通跳转是相对的,但间接 jump/call 使用绝对地址。正如@MichaelPetch 的评论所指出的,您代码中某个时刻的 FAR JMP 将为您设置 CS。