两个汇编的故事(打算用 NASM 替换 TASM)

介绍

最近,在Embarcadero的公共论坛上,有关宣布Win64 Delphi编译器的首个版本很可能没有内置汇编程序(BASM)的讨论非常活跃。建议是改用外部汇编器,据说Embarcadero可能会选择NASM(开源Netwide汇编器)。

更新:现在很清楚,在Delphi XE2及更高版本中有一个Win64内置汇编程序。您将不必使用NASM,至少目前不会。

有人抱怨说,缺少内置的汇编程序将意味着编译器将变得毫无用处,并且将代码转换为像NASM这样的外部汇编程序将需要太多工作。为了检查这一点,我决定重写我的Decimals.pas单元,该单元在NASM中全部或至少部分使用了BASM,以了解这是多么可行,以及它将进行多少工作。

在此过程中,我学到了很多关于BASM和NASM之间异同的知识,并设法编写了一个小的宏程序包,可以使转换变得容易一些。本文介绍了我在此过程中所经历的。

NASA

NASM是一种多功能的但相当简单的汇编器。它确实接受常规的Intel汇编器语法,例如MOV EAX,EDXLEA EAX,[EBP+SomeOffset],但不接受Microsoft的MASM的大部分语法(ml.exe或ml64.exe),也不接受Borland的TASM理想模式的大多数语法。当然,它也不知道Delphi的注释语法以及您可以在Delphi BASM中使用的其他Delphi功能,例如VMTOffset等。

物有所值:在本文中,我使用了2010年10月27日的2.09.03版。

NASM可以产生多种输出格式。它可以生成最常用的16位,32位和64位目标文件,以及简单的.bin或.com文件。我花了一些时间来了解如何创建与Delphi兼容的32位OMF对象文件(请参阅有关在Delphi中使用对象文件的文章)。必须完成两件事:所选格式必须为obj,并且源文件中声明的每个段都必须声明为USE32第一个段声明应该靠近顶部,否则您将获得一个空的16位段,这将使您的目标文件不可用。

NASM是命令行编译器。它带有控制台设置。我对其进行了增强,使其具有160个字符的宽度,75个字符的高度以及Lucide Console 14pt作为字体的窗口。我写了一个小批处理文件(asm.bat)来编译decimals.asm文件:

@回声关闭
nasm -fobj -Ox -l%1.out%1.asm

它用作:

asm小数

我使用的选项是:

  • -fobj—将输出格式设置为OMF。可以包含16和32段!
  • -Ox—全面优化。尽可能选择最小的操作码/文字组合。
  • -l%1.out—%1是批处理文件的第一个参数,例如decimal-l给出清单文件的名称
  • %1.asm—要汇编的文件,例如:decimal.pas

如果没有错误,这会产生文件decimals.obj,在OMF格式和文本文件decimals.out,这是上市文件,可见已经生成什么样的代码。我通常在Delphi编辑器中同时打开两个文件(decimals.asmdecimals.out)。decimals.out文件将被IDE自动更新。

为NASM编写汇编程序

正如我已经说过的,每个段必须显式声明为USE32,否则它将生成为16位。NASM文档说您也可以使用[BITS 32](可能在文件顶部附近),但是我没有成功。

我拥有的decimals.asm文件具有三个声明的段(或节)。我不知道这三个条件是否都是必需的,但是它可以工作:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
剖面 数据    公用 32

; 数据声明,例如记录或外部数据
(在此文件的外部,例如在Delphi单元中)

 const   公共 使用部分32

; 常量声明

部分 代码    公共使用 32

; 

如您所见,两者都声明为use32请注意,NASM不区分大小写,除了其声明的标签和“变量”外,它们可以区分大小写或不区分大小写,具体取决于您声明它们的方式。您还可以看到注释以分号而不是开头//

数据

在.asm文件data部分中声明了所有数据

记录

decimals.pas中,我声明了几种类型。为了能够使用它们,必须在NASM中声明类似的结构。在NASM中,有一个称为的标准宏,struct您可以执行此操作。但是我在那里使用的内置__SECT__宏存在一些问题,因此我编写了自己的record不使用__SECT__可以在本文随附delphi.mac文件中找到它,以及我准备的其他一些宏和定义

一个示例是十进制类型本身。如果您省略了许多方法和运算符重载,那么在Delphi中将保留以下内容:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
类型
  十进制 = 打包 记录
  私有
    Lo Longword ;                // Hi:Mid:Lo形成96位无符号尾数
    Mid Longword ; 
    长字; 
    情况下 字节 0 保留;        //总是0
          量表Shortint ;       // 0..28
          注册字节;           // $ 80 =阴性,$ 00 =正
      1 标志长字; 
  结束;

以下是对NASM的翻译。请注意,NASM不会“存储”任何已声明的大小(它仅使用该大小来保留空间,但不会自动生成特定操作数大小的操作码),因此我作了一点欺骗,并将Flags声明Word,这使我可以声明缩放签名

1个
2
3
4
5
6
7
8
记录  Decimal 
.Lo              resd     1 
.Mid             resd     1 
.Hi              resd     1。
标志          resw     1 
.Scale           resb     1 
.Sign            resb     1
结束;

这样的记录的“字段”可以按以下方式访问:

1个
2
        MOV      EAX ,[ ESI + Decimal.Hi ],
        MOV      [ .MyVar + Decimal.Hi ],EAX

记录宏实际上宣称的绝对段(如CGA屏幕段0B800H在老的DOS天)为0,而“田”实际上局部标签(这就是为什么他们开始以点)。幸运的是,这与您也可以在BASM中寻址小数的字段类似(BASM还知道其他几种方式,但是如果要与这两种语法保持兼容,则可以使用两种汇编程序都可以理解的语法)。

结束在记录声明的结尾是一个宏太。我发现它比特定的结束记录更好它可以结束记录声明以及函数过程声明,并且将生成适合其放置位置的代码。分号是不必要的(它只是开始进行注释),但是使它看起来更好一点,IMO。

同样,我声明了TAccumulator类型。在Delphi中,这是一个变体记录。我还没有找到在NASM中声明此类记录的方法,所以我声明了一个替代的TAccumulator2,它以相同的方式映射到原始记录,但是使用替代的布局。

对准

您应该知道,这样的记录声明总是打包在一起的。没有对齐。如果需要对齐的记录,则可以使用适当的resb(byte),resw(word),resd(dword)等指令来自己进行对齐它们在NASM文档中进行了说明。

如果您有类似的记录:

1个
2
3
4
5
6
{$ A8}
类型
  TTest  = 记录
    B 布尔值L 朗廷; 
  结束;

那么就没有必要将B(或者实际上是.B保留为字节,因为汇编器无论如何都不会将声明的大小用于其操作码生成。您也可以使用保留它resd,因为这会确保Longint在4字节偏移量上正确对齐:

1个
2
3
4
记录  TTest 
.B       resd     1 
.L 	resd 	1
结束

这意味着您必须了解有关记录对齐的详细信息,并知道如何填充字节以实现某种对齐。当然,在Delphi和BASM中,这要容易得多。

另一种可能性是使用内置alignalignb宏。alignbTFormatSettings记录可以看到一个使用示例

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
记录  TFormatSettings 
.CurrencyString                  RESD     1个
                                对准b   1 
.CurrencyFormat                  RESB     1
                                对准b   1个
.CurrencyDecimals                RESB     1
                                对准b   2 
.DateSeparator                   resw     1个
                                对准b   2 
.TimeSeparator                   resw     1个
                                对准b   2 
.ListSeparator                   resw     1个
                                对准b   4 
.ShortDateFormat                 RESD     1
                                对准b   4 
.LongDateFormat                  resd     1
                                对齐b   4 
.TimeAMString                    resd     1
                                对齐b   4 
.TimePMString                    resd     1
                                对齐b   4 
.ShortTimeFormat                 resd     1
                                对齐b   4 
.LongTimeFormat                  resd     1
                                对齐b   4 
.ShortMonthNames                 resd     12
                                对齐b   4 
.LongMonthNames                  resd     12 
                                align b   4 
.ShortDayNames                   resd     7 
                                align b   4 
.LongDayNames                    resd     7 
                                align b  2 
.ThousandSeparator               resw     1 
                                align b   2 
.DecimalSeparator                resw     1 
                                align b   2 
.TwoDigitYearCenturyWindow       resw     1 
                                align b   1 
.NegCurrFormat                   resd     1 
end ;

NASM的文档说align应该用于代码数据段,而alignb应该用于bss段(bss段包含未初始化的数据(使用resb等),而数据段包含初始化数据(使用db等))。记录声明仅包含resbresw等声明,因此使用进行正确的对齐alignb,因为resb默认情况下使用来保留空间,而align使用NOP或其他汇编代码的序列来填充空白,而这在数据中是不允许的(或bss)段。

这仍然意味着您必须知道对齐方式的工作方式(Delphi使用所谓的自然对齐方式-自动填充变量,以便它们从其大小的倍数开始的地址),但是它允许您正确对齐记录

外部资料

在这种情况下,“外部”表示未在.asm文件中定义数据。它们必须在Delphi程序中定义。decimal.pas单元中,我定义了一些由汇编程序例程访问的类型化常量。这种数据必须extern在.asm文件中声明

1个
2
3
extern           PowersOfTen 
extern           MaxMultiplicands 
extern           MaxMultiplicandsMid

在.pas文件中,它们是这样声明的:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const 
  // ... 
  HLW  = 长字; 
  HUI64  = UInt64 ;

  PowersOfTen 阵列[ 0 .. 9 ] 长字 = 
  1 10 //值被截断

    1000000000 
  ;

  MaxMultiplicands 阵列[ 0 .. 9 ] 长字 = 
  HLW HLW  div的 10 //值被截断

    HLW  div  1000000000 
  ;

  MaxMultiplicandsMid 阵列[ 0 .. 9 ] 长字 = 
  长字HUI64 长字HUI64 的div  10 //值被截断

    长字HUI64  div  1000000000 ;

将它们声明为extern可以很容易地从NASM文件中访问它们。

真常量和枚举

decimals.pas中,我在一const节中声明了一些常量和一个枚举真正的常量不占用内存,它们只是符号,因此必须在汇编文件中重新声明它们。一些例子:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const 
  SingleMShift    =  8 ; 
  SingleMMask     =  $ 007FFFFF ; 
  SingleEShift    =  23 ; 
  SingleEMask     =  $ FF ; 
  SingleBias      =  $ 7F ;

  DoubleMShift    =  11 ; 
  DoubleMMask     =  $ 000FFFFF ; 
  DoubleEShift    =  52 ; 
  DoubleEMask     =  $ 7FF ; 
  DoubleBias      =  $ 3FF ;

  ExtendedEShift  =  64 ; 
  ExtendedEMask   =  $ 7FFF ; 
  ExtendedBias    =  $ 3FFF ;

翻译是:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 const   公共 使用部分32

SingleMShift    设备     8 
SingleMMask     设备     $ 007 FFFFF 
SingleEShift    设备     23 
SingleEMask     设备     $ 0 FF 
SingleBias      设备     $ 7 F

DoubleMShift     EQU      11 
DoubleMMask      EQU      $ 000个FFFFF 
DoubleEShift     EQU      52 
DoubleEMask      EQU      $ 07 FF 
DoubleBias       EQU      $ 03 FF

ExtendedEShift   equ      64 
ExtendedEask    equ      $ 7 FFF 
ExtendedBias     equ      $ 3 FFF

在NASM中,您也可以使用该$符号声明一个十六进制文字,但是由于$它也可以是宏本地标签的第一个字符,因此也必须至少跟随一个数字字符。如果你忘记了0$ 0FFF,你可能会得到一个错误约一个未知的标签$FFF

枚举值也是真正的常数。我写了一个简单的宏,它允许您定义简单的枚举,即以序号0开头且不向其成员提供任何数值的枚举(我对NASM宏不够熟悉,无法使其也接受更多复杂的)。这是Delphi类型:

1个
2
3
类型
  TDecimalErrorType  =  detOverflow detUnderflow detZeroDivide detInvalidOp detParse detConversion detInvalidArg detNaN ;

这是使用enum的NASM转换

1个
2
枚举 detOverflow detUnderflow detZeroDivide detInvalidOp \ 
  detParse detConversion detInvalidArg detNaN

(该\字符是NASM中的行继续字符)

枚举宏没有声明TDecimalErrorType,因为无论如何在汇编程序中都不需要(可以将其声明为DWORD,如果需要的话),但是它声明了它定义的数字常量。

版本差异

当我最终转换TryParse的第二个重载时出现了一个大问题,该重载是通过TFormatSettings参数传递的错误开始发生。

第一个问题是在NASM中将TryParse实现Decimal.TryParse会导致链接器内部错误。这是由于TryParse有两个重载版本因此,我不得不将例程重命名为Decimal.TryParseWithSettings,并从Decimal.TryParse的第二次重载中调用它内部错误已消失,但仍然产生不良结果。

在测试例程中用来创建小数的字符串是由其他代码生成的,并且由于它是在德语Windows上生成的,因此“小数点”用逗号表示。例如我有这样的代码:

1个
2
3
A  :=  '31415926,535897932384626433833' ; 
B  :=  '10,0000000000000000001' ; 
C  :=  '8,5467' ;

逗号是此版本的Windows的正确小数点的性格,但我采取了TFormatSettings从声明SysUtils.pas 2010年德尔福的代码是,然而,在Delphi XE编译。我花了一段时间才发现TFormatSettings在Delphi XE中已完全重新定义!因此,我不得不采用不同的TFormatSettings声明并进行转换。您可以在Decimals.asm中看到结果

对于这些问题,我还没有找到一个很好的解决方案。如果由Delphi(或JEDI)提供的Delphi中使用的数据结构,枚举等有完整的NASM转换,那将是很好的。这意味着您只需要设置适当的目录并自动加载即可。您的Delphi版本的正确声明。

当然decimals.pas最重要的部分是它的代码。大部分在汇编器中。我用内置的汇编,但测试NASM,我做了一个副本,把它称为Decimals_nasm.pas和移动BASM例程decimals.asm,采用Delphi编辑器。

我首先尝试了一个简单的例程,该例程没有局部变量,也没有任何堆栈变量。这些例程在Delphi的register调用约定中。第一个参数在EAX中传递,第二个参数EDX中传递,第三个参数在ECX中传递这意味着该例程不需要任何特殊的堆栈处理,即不需要将EBP设置为堆栈帧的序言代码,也不需要更改ESP来分配局部变量的代码它还不需要Epilog代码来清除堆栈:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
23
函数 Decimal Div192by32 PLongword ; 常量 股息十进制;
  除数Longword Longword ; 
ASM 
        PUSH     ESI 
        PUSH     EDI

        MOV      EDI EAX                  // EDI是商
        MOV      ESI EDX                  // ESI是股息
        CMP      ECX 0 
        JNE      @ NoZeroDivide32

        MOV      EAX detZeroDivide 
        JMP     错误

@ NoZeroDivide32 //一些代码被截断

        MOV      EAX EDX

        POP      EDI 
        POP      ESI
结束;

这是原始的翻译:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
23
24
25
26
27
; 类函数Decimal.Div192by32(商:PLongword; const股息:小数
;;除数:Longword):Longword;

全局 Decimal.Div192by32

Decimal.Div192by32:

        ESI
        EDI

        MOV      EDI EAX                  ; // EDI为商
        MOV      ESI EDX                  ; // ESI为Dividend 
        CMP      ECX 0 
        JNE      .NoZeroDivide32

        MOV      EAX detZeroDivide 
        JMP     错误

.NoZeroDivide32:

        ; 截断相同的代码

        MOV      EAX EDX

        POP      EDI 
        POP      ESI 
        RET

我定义了几个宏procedurebeginend,后来还asmfunction,这使得代码看起来是这样的:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
23
24
25
26
27
; 类函数Decimal.Div192by32(商:PLongword; const股息:小数
;;除数:Longword):Longword;

函数 Decimal.Div192by32

汇编

        ESI
        EDI

        MOV      EDI EAX                  ; // EDI为商
        MOV      ESI EDX                  ; // ESI为Dividend 
        CMP      ECX 0 
        JNE      .NoZeroDivide32

        MOV      EAX detZeroDivide 
        JMP     错误

.NoZeroDivide32:

        ; 截断相同的代码

        MOV      EAX EDX

        POP      EDI 
        POP      ESI
结束;

如您所见,Delphi本地标签@NoZeroDivide32被转换为NASM本地标签.NoZeroDivide32开头的注释//将转换为以分号开头。宏与Delphi中的相应关键字具有相似的含义。Error是Delphi代码中的过程,并且extern在NASM源代码的代码部分中声明为过程

end宏仅生成一个RET操作码。在下一节中,您将看到它可以做的还更多。

请注意,函数名称Decimal.Div192by32可以与在Delphi文件中声明的名称完全相同。这是一个幸运的情况。

堆栈参数和局部变量

decimals.pas中的代码还包含具有在堆栈上传递的参数的函数,例如SingleDoubleExtended类型的参数以及局部变量。我写了一组宏,param并且var,这项工作连同procedurefunctionbeginasmend宏。下面是其用法的示例。这是原始的(代码相当长,所以我将其中的大部分内容剪了掉):

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
23
24
25
26
27
28
29
30
31
32
过程 十进制InternalFromExtended out 结果十进制; 
  const 来源Extended ; 
var 
  A TAccumulator ; 
  LRESULT ^小数; 
ASM 
        PUSH     ESI 
        PUSH     EDI 
        PUSH     EBX 
        MOV      LRESULT EAX

        XOR      EAX EAX 
        MOV     一个L0 EAX 
        MOV     一个L1 EAX 
        MOV     L2 EAX 
        MOV     L3 EAX 
        MOV     L4 EAX 
        MOV     L5 EAX 
        MOV     一个标志EAX 
        MOV      EDI DWORD  PTR  [来源] 
        MOV      ESIDWORD  PTR  [+ 4 ] 
        MOVZX    EDX WORD  PTR  [+ 8 ]

        //代码被截断

        MOV      EAX A 标志
        MOV      [ EDI ] 十进制EAX

        POP      EBX 
        POP      EDI 
        POP      ESI
结束;

这是使用宏的方式:

 1个
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18岁
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
; 类过程Decimal.InternalFromExtended(out结果:Decimal 
;; const来源:Extended);

过程 Decimal.InternalFromExtended

参数   扩展

VAR     TAccumulator 
VAR      LRESULT 指针

asm 
        PUSH     ESI 
        PUSH     EDI 
        PUSH     EBX 
        MOV      [ .LResult ],EAX

        XOR      EAX EAX 
        MOV      [ .A + TAccumulator.L0 ],EAX 
        MOV      [ .A + TAccumulator.L1 ],EAX 
        MOV      [ .A + TAccumulator.L2 ],EAX 
        MOV      [ .A + TAccumulator.L3 ],EAX 
        MOV      [ .A + TAccumulator.L4 ],EAX 
        MOV      [ .A + TAccumulator.L5 ],EAX 
        MOV      [ .A + TAccumulator.Flags],EAX 
        MOV      EDI DWORD  PTR  [ .Source ] 
        MOV      ESI DWORD  PTR  [ .Source + 4 ] 
        MOVZX    EDX WORD  PTR  [ .Source + 8 ]

        ; 代码被截断

        MOV      EAX ,[ .A + TAccumulator.Flags ] 
        MOV      [ EDI + Decimal.Flags ],EAX

        POP      EBX 
        POP      EDI 
        POP      ESI
结束;
procedure Decimal.InternalFromExtended
这将建立一个新的称为NASM的上下文,_procedure_并初始化所有与过程/函数相关的局部变量
param Source,Extended
这声明了一个堆栈变量。它生成一个称为的局部定义(局部“变量”),.Source并在堆栈上为其保留12个字节。param的第二个参数必须是具有已知大小的已知类型。可以是在delphi.mac文件中声明的类型,也可以是record定义生成的类型
var A,TAccumulator
var LResult,Pointer
这与相似param,但可确保更改ESP以便为局部变量腾出空间。可以通过EBP的负偏移量来访问它们这两个宏,varparam会造成序言和结尾码的产生,这类似于用Delphi生成的代码。
asm
如果声明了局部变量或堆栈参数,它将生成一个设置EBP的序言如果声明了局部变量,它还会生成更改ESP的代码以为其留出空间: 
1个
2
3
        PUSH     EBP 
        MOV      EBP ESP 
        SUB      ESP $ DELPHI_varsize_
end
如果声明了局部变量或堆栈参数,它将生成代码以将EBP重置为其原始状态。如果声明了堆栈参数,则还会生成必要的RET n操作码,否则只会生成一个simple RET当然,它仅在_procedure_上下文中这样做。_record_ 上下文中,它生成不同的代码。最后,它返回到之前的上下文: 
1个
2
3
        MOV      ESP EBP 
        POP      EBP 
        RET      $ DELPHI_paramsize_ - 8

如您所见,与您必须使用纯NASM语法相比,宏使定义函数或过程变得容易一些。

请注意,必须这样编写宏,才能生成本地标签。因此,称为的变量A将变为,.A而称为的参数Source变为.Source我首先让它们生成全局定义,但是当使用var重新声明标识符时,这会引起巨大的问题(例如无限递归)param,因为预处理器会在将宏参数传递给宏之前将宏参数扩展A为先前的定义(例如EBP−36,如还有里面的宏文。宏会使情况变得更糟或导致错误,或者同时发生。手册%undefine标识符可以防止这种情况发生,但是可以很容易地将其遗忘。我希望宏负责Delphi BASM所做的大部分工作。

如果对NASM的宏语言有更深入了解的人对此有一个好的解决方案,那就是'.'在避免递归问题的同时允许使用全局名称(即不带引号的名称),我很想听听它

NASM与BASM

显然,NASM不是BASM。内置汇编程序肯定比NASM这样的外部汇编程序更方便使用。但是,想要使用外部汇编程序的人(无论出于何种原因,例如在将来的版本中缺少BASM)必须知道一些重要的区别。这是某些使用Delphi的BASM的人必须了解的最重要的区别。其中一些已经在上一章中提到过了。

指令语法

语法上有很多差异。NASM汇编器非常简单(创建者称其为功能,但我对此不太确定)。所谓的有效地址仅知道一种语法:它们必须完全用方括号括起来。所以这:

1个
2
3
4
5
6
7
asm 
        ... 
        MOV      EAX [ ESI ] 十进制你好    ; //  ESI 指向 一个 小数
        MOV      d EAX                 ; //  d 当地的 VAR 类型 十进制
        MOV      d 符号0 
        ...
结束;

只能这样编码:

1个
2
3
4
5
        ... 
        MOV      EAX ,[ ESI + Decimal.Hi ]             ; [ESI + 8] 
        MOV      [ .D + Decimal.Hi ],EAX              [EBP-16 + 8] = [EBP-8] 
        MOV      BYTE  [ .D + Decimal.Sign ],0         [EBP-16 + 15] = [EBP-1] 
        ...

在上面的示例中,您可以看到其他一些区别:

  • 变量和标签之间没有区别,并且某些类型的标签/变量(局部,宏局部,上下文局部等)必须以某些字符组合开头。我希望有另一种方式来区分此类类型(例如,通过关键字或位置),例如通过名称前缀(或后缀)。这也是为什么varparam宏生成局部标签/变量(即以点开头的局部标签/变量)的原因,.D如上例所示。
  • 不允许使用类似[ESI].Decimal.Lo或的语法MyVar[EBX]就像我在上面说的那样,NASM不允许使用定义Delphi知道的有效地址的替代语法。这是一个限制,灵活性较差,但是它的优点是(对于读者而言)实际组装的东西不那么模棱两可。
  • 像一个类型的成员简单点语法,.D.Lo或者[.D.Lo]是不可能的:“类型”(实际上,段名称,因为这是什么record宏观用途)必须被提及:[.D+Decimal.Lo]
  • 汇编器不考虑变量或成员的大小或类型。即使使用声明Decimal.Signresb并将D其声明为Decimal,也必须使用BYTE指定字节访问@BYTE PTR但是,如果编译器可以从其他操作数推断出您要存储的内容,则可以省略大小规格,例如MOV [.D+Decimal.Sign],AL不需要BYTE规格。

NASM中不允许使用语法BYTE PTRWORD PTR等等。您必须使用BYTEWORD等等。这就是为什么我将空定义PTR放在delphi.mac中的原因现在,您可以BYTE PTR像在BASM代码中一样使用,等等。请注意,PTRBASM也不是必需的,因此,如果要使(新)代码兼容,请不要使用它。

评论不同。在NASM中,只有单行注释以分号(;开头,并在该行的结尾处结束。//在NASM中,Delphi语法被视为数字运算符。为了使新代码更容易转换为NASM,您可以使用该组合;//以两种语法开始注释。在Delphi中,这被视为语句分隔符,;后跟一个注释//,而在NASM中,它仅被视为注释;

默认的标签/变量区分大小写。您可以使用预处理器命令%idefine%ixdefine定义不区分大小写的标签/变量,但NASM伪指令EQU似乎区分大小写。EQU这样做的好处是,您可以真正定义一个常量(不能无意中对其进行重新定义),而不仅仅是一些预处理器文本。

需要注意的是指令,就像MOV EAX,EDX区分大小写。因此,如果您习惯于使用小写或混合大小写,则无需将其转换为大写。通常,只有变量/标签区分大小写。

指令默认也是单行。如果希望它们溢出到下一个源代码行,则必须使用\延续字符。这也适用于预处理器。

如果在汇编程序中很难执行操作(这也适用于BASM),例如引发异常或设置动态数组的长度,或访问另一个对象的属性,则可以在Delphi中的一个简单例程中进行操作并调用来自汇编程序的。decimals.pas中,您可以看到我在例程Error()中进行了此操作。

这同样适用于在Delphi的BASM中更轻松的事情,例如访问对象的成员或调用虚拟方法。继承和隐藏成员或私有成员使得几乎不可能在NASM中完全重新定义对象,因此,您最好的办法是在Delphi中编写这样的方法,如将所有必需的数据传递到汇编程序例程并返回返回值的存根。 ,如果有的话。

同样适用于虚拟或动态方法。NASM无法访问VMTOFFSET或进行类似操作,因此,如果外部汇编程序必须在对象中真正调用虚拟函数或动态函数,则编写一个私有例程来调用该虚拟函数,而汇编程序代码将调用该函数,并根据需要传递所有数据。这比较单调乏味,并且增加了一个额外的呼叫级别,但这可能并不是经常需要的。

转换次数

我已经提到了将BASM转换为NASM必须要做的大多数事情。简单的语法文字转换如更改[ESI].Decimal.Sign[ESI+Decimal.Sign],改变@MyLabel.MyLabel从或建议//;//可以很容易地使用Delphi的IDE的GREP功能来完成。其余的必须手动完成,但是使用宏delphi.mac,这应该不会太耗时。

使用我编写delphi.mac文件可以完成功能框的转换,可能带有局部变量和堆栈参数该文件远非完美,因为我还不太熟悉NASM预处理程序,但是宏使将BASM函数转换为NASM变得容易得多。

Delphi.mac还提供了一种声明记录或枚举的方法。我确信这些宏也可以改进,但是它们确实或多或少地满足了我的要求。

德尔菲方面

Delphi方面很容易解释。必须使用{$L}链接目标文件{$LINK}

1个
2
3
{$ L decimals.obj} 
//或者,您可以使用:
{$ LINK'decimals.obj'}

.obj文件最好.pas文件位于同一目录中,但也可以位于其他目录中。要访问它,可以使用相对寻址'..\asm\decimals.obj'或绝对寻址'C:\source code\asm\decimal type\decimals.obj'

在外部汇编器中完成的例程必须声明为external

1个
2
过程 十进制InternalFromExtended out 结果十进制; 
  const 来源Extended ;  外部;

与您在DLL中使用例程不同,您不会声明modulenameindex(例如external 'bla.obj' name 'Decimal.InternalFromExtended')。链接程序负责查找例程。如果不能,它将报告错误。如果.obj文件要求在Delphi程序中定义例程,则它(可能还有NASM)也将报告错误

据我所知,没有办法在汇编器中声明重载,因此重载应在Delphi中完成,并且仅包含对具有不同名称的汇编器例程的调用:

我之前已经提到过:例程在NASM中与在Delphi中具有相同的名称,因此无需使用全局(“未分配”)名称并从Delphi中的存根中调用它们,您可以直接使用完全限定的成员名称。

未来

由于缺少64位的Delphi,我无法测试64位汇编器或ELF64代(根据Embarcadero的Allen Bauer说法,可能会选择该格式)。我不知道在执行此操作时是否需要注意一些特定的事情。我知道异常和堆栈处理是不同的,但是我假设编译器会知道如何处理它们。无论如何,我通常都会将异常处理留给Delphi。

如果在一个或多个将来的Delphi版本中确实省略了BASM,那么至少要对使用NASM或任何其他合适的外部汇编程序提供更好的支持至关重要。以下几点将使这变得容易得多:

  • 一个编译器指令,该指令将自动支持像NASM这样的外部汇编程序,就像对资源文件一样。
  • 德尔菲运行时库中记录,枚举等的NASM转换,包含文件的形式,或多或少类似于为C ++ Builder生成.hpp文件。
  • 将某些信息(如目录,条件定义,Delphi版本等)以正确的NASM命令行格式传递给NASM汇编程序。
  • 使用NASM(或任何其他汇编程序)的项目选项中的新页面,该页面允许用户设置汇编程序的目录,包含文件和汇编程序文件的搜索路径,条件定义,文件的后缀和前缀名称等
  • 叫“原始人”的一种方式,在督察私有运行程序System.pasSystem.UStrFromPWCharLen @你可以从BASM调用而不是从纯Delphi代码。没有BASM意味着根本无法访问这些原语,而不是从Delphi和外部汇编程序。

结论

无需内置汇编程序即可使用Delphi。但这并不那么方便,并且会带来很多问题。

用BASM语法编写的代码必须转换为NASM语法。这是最基本的,但是IMO大部分可以使用一些grep搜索和替换操作来完成

Delphi中使用的每种数据类型都必须转换为NASM语法。这可以由用户手动完成-可能必须由用户针对其自己的类型(例如记录和枚举)手动完成。例如,也可以由Embarcadero或JEDI之类的组织来完成。实际上,这是必须完成的工作,需要进行大量工作,尤其是如果这些类型在版本之间有所不同时,例如上述TFormatSettings示例。

无法重载汇编程序功能。函数可以在Delphi中重载,但不能在外部汇编器中。使用外部汇编程序时,解决此问题的唯一方法是使重载函数在外部汇编程序中调用不同名称的函数。IOW,重载只是真正的汇编器函数的包装,它们必须具有不同的名称。

Delphi /外部汇编程序系统的一个幸运功能是,您可以像在Delphi中那样命名在NASM中实现的方法。Decimal.InternalMultiply这样的函数也可以在NASM中以相同的方式调用(当然,除非被重载)。

现有代码必须转换为NASM语法。使用Delphi编辑器grep功能可以完成大多数操作我希望我在delphi.mac中放在一起的几个宏和类型声明可以使其他一些部分变得更容易。但这仍然是必须完成的工作。

但是对于64位Delphi,无论如何都必须重写大部分代码,这可能需要更多工作。如果此重写立即以正确的NASM语法(BASM也支持的语法,除了一些小东西,例如以'。'代替'@'开头的本地标签)完成,则可能需要额外的时间来使用外部汇编程序与重写代码所需的时间相比微不足道。

访问类是一个真正的问题。这在BASM中非常容易,但是在任何外部汇编程序中都是一个问题。没有模拟继承的适当方法,因此几乎不可能以某种透明的方式访问类实例的私有,受保护或公共字段。避免这种情况的唯一方法是编写额外的静态类方法或包含汇编器部分的全局例程,实例方法将适当的信息作为参数传递给Delphi代码,以将其传递给汇编器部分。

类的另一个问题是,外部汇编器无法访问VMTOFFSETDMTINDEX之类的伪宏,因此也不可能透明地调用虚拟或动态方法。虚拟方法可能必须由额外的调用实际代码的包装程序来调用(IOW,要从汇编程序中调用虚拟方法,您要在Delphi中编写一个非虚拟方法,除了使用适当的参数调用虚拟方法并传递任何结果返回,并且非虚拟方法由汇编代码调用)。

版本问题在BASM中不是问题,但在外部汇编程序中却是另一个大问题。如以上针对未来的建议中所述,Delphi的适当支持是解决此问题的唯一有用方法。

在BASM,您可以访问特定的私有函数System.pas使用一种特殊的语法,如常规_UStrFromPWCharLenSystem.pas可以作为被访问System.@UStrFromPWCharLen的汇编。这在外部汇编程序中是不可能的,并且在没有BASM的情况下,也无法从Delphi代码中调用它们(目前-如果编译器可以从Delphi代码中访问此类“原语”,那就太好了)。System.@UStrFromPWCharLen的情况下,我写了一个高级等效项,它可能隐式调用了实际的原语。这需要对这些例程的内部结构有一些思想和知识,并且在高级代码中可能很难做到。

使用类型信息的例程也需要在Delphi中使用包装器,但是很难从外部汇编器访问该信息。

访问Decimals.pasDecimals_nasm.pas中未使用的高级功能可能还存在其他问题,例如泛型,匿名方法,RTTI等。如果发现一些问题,我将在这里进行讨论。

最后

尽管可以使用外部汇编程序,但要进行转换需要花费大量额外的精力对于重写代码全新代码,BASM与外部汇编程序之间的区别远没有那么明显。好的,它需要很多包装器,但是可以访问基元并使用正确的语法,因此应该可行。不过,IMO必须提供编译器和IDE的支持。访问运行时类型的NASM转换尤其重要,但是我所描述的形式也需要IDE支持。

http://rvelthuis.de/articles/articles-nasm.html

posted @ 2021-01-04 17:35  findumars  Views(579)  Comments(0Edit  收藏  举报