Skip to content

Latest commit

 

History

History
536 lines (344 loc) · 20.4 KB

5.md

File metadata and controls

536 lines (344 loc) · 20.4 KB

五、数据段

数据段是程序在内存中存储其全局和静态数据的地方。该数据在编译时定义。数据段不保存运行时分配的变量(堆用于此目的)或子进程本地的变量(栈用于保存这些变量)。这里提供的大部分信息将用于任何细分市场。例如,变量可以在未初始化的数据段()中声明。数据)或常量数据段(。常数)。

| | 注意:在数据段中声明的所有变量在实际中都会变成字节。exe 文件。它们不是在程序运行时生成的;它们是从。exe 文件。创建一个包含 150 兆字节变量的数据段将生成一个 150 兆字节的。并且需要很长时间来编译。 |

标量数据

数据段中定义的标量数据被赋予名称、大小和可选的初始值。在数据段中声明一个变量就是命名一个从数据段开始的偏移量。所有的变量名实际上都是指针;它们是一个数字,指的是内存中数据段的偏移量,因此程序员不必将偏移量记为数字。

| | 注意:汇编中的变量有时被称为标签,但是为了避免与代码段中的标签混淆,我将数据段标签称为变量。 |

要在数据段中定义变量,一般布局如下:

[VarName] [Type] [Initial Value]

其中[VarName]是任何合法的变量名,并且将是您希望用作引用的数据点的名称。

| | 注意:变量名的规则与 C++ 相同。它们不能以数字开头,可以包含字母、数字和下划线。还可以使用一些 C++ 中不合法的附加符号,比如@和?。 |

[Type]是数据类型,可以是 基础数据类型 表的 ASM 列中的任何一种数据类型或短版本。

初始值可以是文字值,也可以是“?”。问号表示数据没有初始值。实际上,即使数据未初始化,也会给它一个值。“?”的意义声明程序员不关心数据最初被设置为什么值,并且推测程序将在使用数据之前设置一些其他的初始值。

以下是在数据段中定义简单标量数据类型的一些示例。

    .data
    myByte db 0 ; Defines a byte set to 0 called myByte
    patientID dw ? ; Defines a word, uninitialized called patientID
    averageSpeed dt 0.0 ; Defines 10-byte real, reals must have a decimal
    ; point if initialized
    totalCost sdword 5000 ; Defines signed dword set to 5000, called totalCost

| | 注意:第一个变量放在 DS:0 处(它放在数据段的第一个字节处),第二个放在其后(变量之间没有填充间距)。如果第一个变量是 1 字节,那么第二个变量将是 DS:1。如果第一个变量是一个单词,那么第二个变量就是 DS:2。连续变量存储在内存中的方式称为对齐,这对于性能很重要,因为一些最快的数据处理指令要求数据对齐到 16 个字节。 |

阵列

在标量数据类型之后,下一个最基本的数据类型可能是数组。数组是连续内存中相同数据类型的元素列表。在汇编中,数组只是一个内存块,数组的第一个元素被赋予一个名称。

用逗号声明的数组

您可以声明由逗号分隔的数组元素。

    MyWord dw 1, 2, 3, 4 ; Makes a 4 word array with 1, 2, 3, and 4 as elements

如果需要使用多行,请用逗号结束一行,然后继续下一行。

    MyWord dw 1, 2, 3, 4, ; Four words in an array
    5, 6, 7, 8 ; Another four words in the same array!

这是合法的,因为你实际上根本不需要这个标签。变量的 MyWord 名称是完全可选的。

较大数组的重复语法

您可以使用重复语法在数据段中创建更大的数组,但是请记住,数据段中的每个字节都是最终文件中的一个字节。

要创建更大的数组,可以使用以下模式(重复语法)声明一个值数组:

[Name] [type] [n] [dup (?)]

其中[Name]为数组名,任意合法变量名。[Type]基础数据类型 表中的数据类型之一,[n]是数组中的项数。DUP是重复的缩写,它复制的数据在括号中。要在名为 MyArray 的数组中将 50 个单词全部设置为 25,使用重复语法的数组声明如下:

    MyArray word 50 dup (25)

您可以将简单的逗号分隔数组定义语法与重复语法结合起来,生成重复模式的数组。

    MyArray byte 50 dup (1, 6, 8)

这将定义一个 150 字节长(50×3)的数组,其重复模式为 1、6、8、1、6、8、1、6、8....

您可以嵌套重复的指令来创建多维数组。例如,要创建一个 10×25 维字节数组并将所有元素设置为 A,可以使用以下方法:

    MyMArray byte 10 dup (25 dup ('A'))

| | 注意:内存是线性的。样本代码实际定义的是 10×25 还是 25×10 数组,必须由程序员决定。对 CPU 来说,它只是一块线性 RAM,并不存在多维数组这种东西。 |

对于三维数组,您可以使用类似这样的东西:

    My3dArray byte 10 dup (25 dup (100 dup (0)))

这将创建一个 10×25×100 的三维字节数组,所有字节都设置为 0。从中央处理器的角度来看,这个三维阵列与以下完全相同:

    My3dArray byte 25000 dup (0)

获取关于数组的信息

定义后,MASM 有一些指令来检索关于阵列的信息:

lengthof:以元素为单位返回数组的长度。

sizeof:以字节为单位返回数组的长度。

type:以字节为单位返回数组的元素大小。

例如,如果您有一个名为 myArray 的数组,并且想要将关于它的信息移动到 AX 中,您可以执行以下操作:

    mov ax, lengthof myArray ; Move length in elements of the array
    mov ax, sizeof myArray ; Move the size in bytes of the array
    mov ax, type myArray ; Move the element size into AX

| | 注意:在汇编文件之前,这些指令被转换为即时值;因此,“mov lengthof myArray,200”实际上被翻译为“mov 16,200”。将值移动到文字常量中没有任何意义(即使在汇编中,我们也不能更改 16 的含义),因此该行是非法的。 |

定义字符串

在 MASM,单引号和双引号完全相同。它们用于字符串和单个字符。

| | 注意:C 和 C++ 中的字符串是一个字节数组,通常在末尾有一个空值 0。这些类型的字符串称为零分隔字符串。许多 C++ 函数被设计为使用零分隔字符串。 |

要定义文本字符串,可以使用以下字符串语法:

    errMess db 'You do not have permission to do this thing, lol', 0

这相当于定义一个字节数组,其值设置为字符串中字符的 ASCII 数字。开始处的 Y 将是数组的第一个(最低有效)字节。

| | 注意:末尾的逗号和零是最后的空值。这使得该字符串成为 cout 和其他 C++ 函数所理解的空终止字符串。当字符串到达 0 字符时,Cout 停止写入。在 C++ 中,当我们使用双引号时,会自动添加 0;在程序集中,它必须是显式的。 |

要定义不适合单行的字符串,可以使用以下方法:

    myLongString db "This is a ",
    "string that does not ",
    "fit on a single lion!", 0

在前面的每个示例中,单引号也可以用于相同的效果:

    myLongString db 'This is a ',
    'string that does not',
    'fit on a single lion!', 0

如果需要在单引号数组中使用单引号或在双引号数组中使用双引号,请将其中两个放在一起:

    myArr1 db "This ""IS"" Tom's array!", 0 ; This "IS" Tom's array!
    myArr2 db 'That''s good, who''s Tom?', 0 ; That's good, who's Tom?

类型定义

您可以使用类型定义(typedef)指令为数据类型声明自己的名称。

integer typedef sword ; Defines “integer” to mean sword

MyInteger integer ? ; Defines a new sword called MyInteger

您不能为您的 typedefs 使用保留字,因此尝试创建名为“int”的带符号 dword 类型将不起作用,因为“int”是调用中断的 x86 指令。

| | 注意:可以使用 typedef 为用户定义的类型、基本类型、结构、联合和记录定义新名称。 |

结构和联盟

要定义一个结构(类似于 C++ 结构),可以使用 struc(或 struct)指令。

    ExampleStructure struct ; Structure name followed by "struct" or "struc"
    X word 0
    Y word 0
    Z word 0
    ID byte 0
    ExampleStructure ends ; The name followed by “ends” closes the definition

这将创建一个具有四个变量的结构;三个是设置为 0 的单词,称为 X、Y 和 Z,最后一个变量是一个名为 ID 的字节,默认情况下也设置为 0。

前面的例子是原型。要创建先前结构原型的实例,可以使用以下方法:

    person1 ExampleStructure { } ; Declares person1 with default values
    person2 ExampleStructure { 10, 25, 8, ? } ; Declares person2 with
    ; specific values
    ; and ID of ?, or 0 probably

结构实例的每个字段都可以用花括号中按顺序提供的值进行初始化。使用“?”初始化为 MASM 的默认值(0)。您可以初始化的值少于结构和其他部分自动给出的值。根据结构的原型,这些是它们的默认值。

    person2 ExampleStructure { 10 } ; Declares person2 with 10 for x
    ; but the rest are as per the
    ; structure's prototype

通过原型声明,您可以通过不包含任何值来创建该结构的实例,其中一些值已初始化,而另一些值具有默认值。只需在空格处加一个逗号来表示值的位置。

    MyStructure struct
    x word 5
    y word 7
    MyStructure ends

    InstanceOfStruct MyStructure { 9, } ; Change x to 9 but keep y
    ; as 5 as per prototype

要从代码中更改以前实例化的结构的值,可以使用句点,类似于在 C++ 中访问结构元素的方式。

    mov person1.X, 25 ; Moves 25 into person1's X
    mov person2.ID, 90 ; Moves 90 into person2's ID

| | 注意:当结构从 C++ 传递给函数时,它们不是通过引用传递的。根据结构的大小,它们被复制到寄存器和栈中。如果一个结构有两个整数,那么这个结构的整个实例将被复制到 RCX(因为两个 32 位的 dwords 适合 64 位的 RCX)。这很尴尬,因为当结构的各个元素在寄存器中时,不能引用它们。例如,没有办法参考 RCX 的顶级 dword。因此,将 C++ 中的结构作为指针传递可能更容易。 |

您可以使用 LEA 指令(加载有效地址)加载先前实例化结构的有效地址。要使用寄存器(本例中为 RCX)作为指向结构实例的指针,您必须告诉 MASM 地址、指向的结构类型和字段。

    lea rcx, person1 ; Loads the address of person1 into RCX
    mov [rcx].ExampleStructure.X, 200 ; Moves 200 into person1.X using
    ; RCX as a pointer

中央处理器不会检查以确保 RCX 实际上指向一个示例结构实例。RCX 可能指向任何东西。[RCX].ExampleStructure.X简单的意思是找到 RCX 所指的东西,并把 X 在示例结构原型中的偏移量加到这个地址上。换句话说,[RCX].ExampleStructure.X翻译成RCX+0,因为在范例结构的原型中,X 的字节数是 0。[RCX].ExampleStructure.Y翻译为RCX+2,因为 Y 是两个字节单词 X 之后的第二个元素

要将结构的实例作为参数传递给函数,通常会传递其地址,并按照前面的观点对其进行操作。这是通过引用传递的,初始对象会发生变化,但这比用 C++ 的方式将结构的数据复制到寄存器和栈要快得多。

    ; This is the function that is initially called
    Function1 proc
    lea rcx, person2 ; Load *person2 into RCX to be passed to Fiddle
    call Fiddle ; Call Fiddle with RCX param 1
    ret
    Function1 endp

    ; Fiddle, RCX = *ExampleStructure
    Fiddle proc
    mov [rcx].ExampleStructure.Y, 89 ; Change something
    ret
    Fiddle endp

结构的结构

若要定义具有较小子结构作为其成员变量的结构,请先声明较小的子结构。然后将子结构的实例放在更大结构的声明中。

    ; This is the smaller sub-structure
    Point struct
    X word 0
    y word 0
    Point ends

    ; This is the larger structure that owns a Point as one of its parameters:
    Square struct
    cnr1 Point { 7, 4 } ; This one uses 7 and 4
    cnr2 Point { } ; Use default parameters!
    Square ends

若要声明包含数据段中子结构的结构实例,可以使用嵌套的大括号。

    MySquare Square { { 9, 8 }, { ?, 7 } }

| | 注意:如果不想设置结构的任何值,可以使用{}来表示所有值的默认值,即使结构中有子结构。 |

若要设置结构子结构的值,请附加一个句点来指定要更改的变量。

    mov MySquare.cnr1.Y, 5

您可以使用寄存器作为指针,并引用嵌套结构的元素,如下所示:

    mov word ptr [rcx].Square.cnr1.X, 10

工会

联合类似于结构,只是联合中每个元素使用的实际内存物理上位于内存中的相同位置。联合是一种在内存中引用同一地址作为多种数据类型的方式。

    MyUnion union
    w1 word 0
    d1 dword 0
    MyUnion ends ; Note that it is ends, not endu!

这里明 w1明 w2 的地址完全一样。dword 版本为 4 字节长,字只有 2 字节,但两者的最低有效字节具有相同的地址。

记录

记录是 MASM 的另一种复杂数据类型。它们就像结构一样,但它们是在位级别上工作和定义的。定义的语法如下:

[name] RECORD [fldName:sz], [fldName:sz]...

其中[name]是记录的名称,[fldName]是字段的名称,[sz]是字段的大小(以位为单位)。

    color RECORD blBit:1, hueNib:4

数据段中的样本代码是名为颜色的记录的原型。然后可以通过以下方式访问该记录:

    mov cl, blBit

这将把 4 移入 CL,因为 blBit 被定义为记录中的第 4 位。hueNib 取位 0、1、2 和 3,blBit 在此之后。

您不能使用记录直接访问位。

    mov [rax].color.blBit, 1 ; Won't change the 4th bit from RAX to 1

记录只是指令的一种形式;它定义了一组用于按位运算的常数。常数是位索引。您可以使用唱片进行旋转。

    mov cl, blBit ; Move the index of the record's blBit into cl
    rol rax, cl ; Use this to rotate the bits in RAX

您可以在数据段中定义记录,并初始化位字段的值,就像使用结构一样。这是唯一一次不使用位操作就可以设置记录的每个元素。

  .data
  color RECORD qlBit:3, blBit:1, hueNib:4  ;
  Defines a record

  ; Following defines a new byte with the bits set as specified
  ; by the record declaration:
  ; qlBit gets 0, blBit gets 1 and the hueNib gets 2
  ; So MyColor will actually be a byte set to  00010010b
  MyColor color { 0, 1, 2 }  ; Declare a
  color record with initializers

  .code _text
  Function1 proc
         mov cl, MyColor      ; Moves 000:1:0010b, or 18 in
  decimal
         ret
  Function1 endp

| | 注:前一条记录中的 qlBit、blBit 和 hueBit 成为其位索引的常数:hueBit = 0,blBit = 4,qlBit = 5。 |

通过使用 MASM 的WIDTH指令,可以得到记录中字段的宽度(以位为单位)。

  mov ax, WIDTH color.hueNib

您可以使用 MASM 的MASK指令获得记录字段的位掩码。

  and al, mask myCol.blBit; AND contents of AL with bit mask
  of defined color 
                          ; record

您可以在MASK指令之前指定NOT来翻转位掩码。

  and al, NOT MASK myCol.blBit

使用等于的常数

您可以使用=符号定义数值常数,也可以使用equ指令定义数值和文本常数。这是“等同于”的缩写。

  Somevar = 25           ; Somevar becomes a constant
  immediate value 25
  name equ 237         ; "name" is the symbol for
  the constant
  mov eax, name        ; Translates to “mov eax, 237”
  moc ecx, SomeVar        ; Sets ECX to 25

您也可以使用EQU指令通过用三角形大括号包围该值来定义文本常量。

  quickMove equ <mov eax, 23>
  quickMove         ; Translates to “mov eax, 23”

通过在值中使用db(定义字节),可以使用 equality 指令来定义机器代码。

  NoOperation equ <db 90h>     ; 90h is machine code
  for the NOP instruction
  NoOperation                    ; Translates to NOP or 90h

在代码段中使用db没有问题,因为db只是将您指定的精确字节值放在文件中的位置。在代码段中使用db有效地使我们能够用纯机器代码编程。

  ; This procedure returns 1 if ECX is odd
  ; otherwise it returns 0, it is programmed
  ; in pure machine code using db.
  IsOdd proc
  db     83h, 0E1h, 01h,            ; and ecx, 1
         8Bh, 0C1h,                 ; mov eax, ecx
         0C3h                       ; ret
  IsOdd endp

使用纯机器码的意义在于,有时汇编程序可能无法理解一些 CPU 能够理解的指令。较老的汇编程序可能不理解 SSE 指令。通过以前面描述的方式使用EQUdb,程序员可以定义他或她自己指定 SSE 指令的方式,不管汇编程序是否自然理解它们。

您可以使用宏指令定义宏函数。

[name] MACRO [p1], [p2]...

; Macro body

ENDM

其中[name]是与宏相关联的符号,MACROENDM是关键字,[p1][p2]和任何其他符号是参数,因为它们在宏的主体中被引用。

  Halve macro dest, input    ; dest and input are the
  parameters
         mov dest, input      ;; Refer to parameters in body
         shr dest, 1
  endm                       ; endm with no macro name
  preceding

  ; And later in your code:
  Halve ecx, 50 ; Moves 25 into ecx
  Halve eax, ecx       ; Moves 12 into eax
  Halve ecx, ecx       ; Moves 12 into ecx
  Halve 25, ecx ; Error, ecx/2 cannot be stored in 25!

每当 MASM 在汇编时找到宏名时,符号名就被替换为相应的代码。这意味着,如果宏代码中有标签(如果代码跳转到其代码中的其他点),MASM 将一次又一次地写标签。每次使用宏时,标签都会出现。由于 MASM 不允许重复标签,并且仍然知道跳转到哪里,因此您可以在宏定义中将标签定义为本地标签。定义为本地的标签实际上将被自动生成的唯一标签替换。

  SomeMacro macro dest, input
  local label1, label2
         test dest, 1
         jnz label1
         jz label2     
  label1:                    ;; Automatically renamed ??0000
         mov eax, 3
  label2:                    ;; Automatically renamed ??0001
         mov ecx, 12         ;; Each label each time
  SomeMacro is
                             ;; called will increment the
  counter,
                             ;; next will be ??0002 the
  ??0003 etc.
  Endm

| | 注意:你可能已经注意到了”;;"宏正文中的注释;这些是宏观评论。它们在生成列表文件时很有用,因为这些注释不会每次在代码中引用宏函数时都出现,只会在宏的定义中出现一次。如果使用单个“;”注释相同的注释将在整个生成的列表文件中反复出现。 |

在您的宏定义中,您可以为任何参数指定默认值,允许您在不指定每个参数的情况下调用宏(将:=和默认值放在参数名称后,somevariable:=<eax>)。您也可以指出需要特定的参数(在冒号后面加上req, somevariable:req)。

| | 注意:当指定默认值时,语法类似于“equ”指令;您必须使用“”来代替“eax”。 |

  SomeMacro macro p1:=<eax>, p2:req, p3:=<49>
         ;; Macro body
  Endm

示例代码中的宏定义允许我们省略第一个和第三个参数的值。只需要第二个,其他的可以默认。

  ; Specify all parameters:
  SomeMacro ecx, 389, 12     ; p1 = ecx
                              ; p2 = 389
                             ; p3 = 12

  ; Just specify parameter 2:
  SomeMacro , ebx,           ; p1 = eax from default
                             ; p2 = ebx
                             ; p3 = 49 from default