在 Delphi 汇编程序中,如何使用 `.align` 协调短条件跳转与分支目标对齐?

How can I reconcile short conditional jumps with branch target alignments with `.align` in Delphi assembler?

如何在 Delphi 汇编程序中协调短条件跳转与分支目标对齐?

我正在使用 Delphi 版本 10.2 Tokyo,用于 32 位和 64 位汇编,完全使用汇编编写一些函数。

如果我不使用 .align,编译器会正确编码 short 条件跳转指令(2 字节指令,由 1 字节操作码 074h 和 1-字节相对偏移量 -+ 高达 07Fh)。但是,如果我曾经放过一个 .align,甚至小到 .align 4——所有位于 .align 之前并且目标位于 .align 之后的条件跳转指令——在这个如果所有这些指令都变成 6 字节指令,而不是它们应该的 2 字节指令。只有位于 .align 之后的指令仍正确编码为 2 字节 short.

Delphi 汇编程序不接受“短”前缀。

如何在 Delphi 汇编程序中使用 .align 协调短条件跳转与分支目标对齐?

这是一个示例程序 – 请注意中间有一个 .align

    procedure Test; assembler;
    label
      label1, label2, label3;
    asm
      mov     al, 1
      cmp     al, 2
      je      label1
      je      label2
      je      label3
    label1:
      mov     al, 3
      cmp     al, 4
      je      label1
      je      label2
      je      label3
      mov     al, 5
      .align 4
    label2:
      cmp     al, 6
      je      label1
      je      label2
      je      label3
      mov     al, 7
      cmp     al, 8
      je      label1
      je      label2
      je      label3
    label3:
    end;

这是它的编码方式——条件跳转,位于 align 之前,指向 label2 和 label3(在 align 之后)被编码为 6 字节指令(这是一个 64 位 CPU 目标):

0041C354 B001          mov al,      //   mov     al, 1
0041C356 3C02          cmp al,      //   cmp     al, 2
0041C358 740C          jz [=12=]41c366    //   je      label1
0041C35A 0F841C000000  jz [=12=]41c37c    //   je      label2
0041C360 0F8426000000  jz [=12=]41c38c    //   je      label3
0041C366 B003          mov al, //label1: mov al, 3
0041C368 3C04          cmp al,      //   cmp     al, 4
0041C36A 74FA          jz [=12=]41c366    //   je      label1
0041C36C 0F840A000000  jz [=12=]41c37c    //   je      label2
0041C372 0F8414000000  jz [=12=]41c38c    //   je      label3
0041C378 B005          mov al,      //   mov     al, 5
0041C37A 8BC0          mov eax,eax     //  <-- a 2-byte dummy instruction, inserted by ".align 4" (almost a 2-byte NOP)
0041C37C 3C06          cmp al, //label2: cmp al, 6
0041C37E 74E6          jz [=12=]41c366    //   je      label1
0041C380 74FA          jz [=12=]41c37c    //   je      label2
0041C382 7408          jz [=12=]41c38c    //   je      label3
0041C384 B007          mov al,      //   mov     al, 7
0041C386 3C08          cmp al,      //   cmp     al, 8
0041C388 74DC          jz [=12=]41c366    //   je      label1
0041C38A 74F0          jz [=12=]41c37c    //   je      label2
0041C38C C3            ret        // label3:

但是如果我删除 .align - 所有的指令都有正确的大小 - 就像以前一样只有 2 个字节:

0041C354 B001          mov al,      //   mov     al, 1
0041C356 3C02          cmp al,      //   cmp     al, 2
0041C358 7404          jz [=13=]41c35e    //   je      label1
0041C35A 740E          jz [=13=]41c36a    //   je      label2
0041C35C 741C          jz [=13=]41c37a    //   je      label3
0041C35E B003          mov al, //label1: mov     al, 3
0041C360 3C04          cmp al,      //   cmp     al, 4
0041C362 74FA          jz [=13=]41c35e    //   je      label1
0041C364 7404          jz [=13=]41c36a    //   je      label2
0041C366 7412          jz [=13=]41c37a    //   je      label3
0041C368 B005          mov al,      //   mov     al, 5
0041C36A 3C06          cmp al, //.align 4 label2:cmp al, 6
0041C36C 74F0          jz [=13=]41c35e    //   je      label1
0041C36E 74FA          jz [=13=]41c36a    //   je      label2
0041C370 7408          jz [=13=]41c37a    //   je      label3
0041C372 B007          mov al,      //   mov     al, 7
0041C374 3C08          cmp al,      //   cmp     al, 8
0041C376 74E6          jz [=13=]41c35e    //   je      label1
0041C378 74F0          jz [=13=]41c36a    //   je      label2
0041C37A C3            ret             //   je      label3
                                //  label3: 

返回条件跳转指令:如何在 Delphi 汇编程序中使用 .align 协调短条件跳转与分支目标对齐?

我承认在像 SkyLake 和更高版本这样的处理器上对齐分支目标的好处很小,我知道我可以避免使用 .align - 它也会节省代码大小。但我想知道如何使用 Delphi 汇编器生成带有 align 的短跳转。这个问题在 32 位目标中也存在,不仅在 64 位目标中。

除非你的 assembler 有一个选项来做更好的分支位移优化(这可能需要重复通过),否则你可能不走运。 (当然你可以自己手动完成所有对齐,但每次更改任何内容都必须重新完成。)

或者您可以使用与 assemble 不同的 assembler。但正如我所料,这是非常不受欢迎的 。 (感谢@Rudy 的评论。)

您可以在 Delphi assembler 中编写一些函数,并尽可能多地执行 Delphi 特定的内容。在另一个 assembler 中编写关键循环部分,hexdump 将其机器代码输出转储到您放在 Delphi 程序集中间的 db 伪指令中。

如果每个函数的开头至少与函数内部的任何内容一样对齐,这可能会工作正常,但您可能最终会浪费指令或将常量放入寄存器以供 NASM 部分使用,这可能比只有更长的分支更糟糕。


Only the instructions that are located after the .align remain correctly encoded as 2-byte short

这不太准确。第一个 je label1 看起来不错,它在 .align.

之前

看起来 任何向前跨越尚未评估的 .align 指令的分支都为 rel32 和 assembler 永远不会回来修复它。其他情况似乎都很好:向后分支跨越 .align,向前分支不跨越 .align.


分支位移优化不是一个简单的问题,尤其是当有 .align 个指令时。不过,这似乎是一个真正次优的实现。

相关:Why is the "start small" algorithm for branch displacement not optimal? 了解有关 assemblers 用于分支置换优化的算法的更多信息。即使是好的 assemblers 也可能不会做出最佳选择,尤其是当有 .align 指令时。