FASM 第二章 – 2.2 控制伪指令

    Author: 徐艺波  From: xuyibo.org  Updated: 2021-04-16

      建议  有什么好的建议,可以贴一下。
      捐助  你的支持,让我们做的更好。

    2.2 控制伪指令
    2.2.1 数值常量
    2.2.2 条件汇编
    2.2.3 重复块指令
    2.2.4 地址空间
    2.2.5 其他伪指令
    2.2.6 多遍扫描

    2.2 控制伪指令

    这章将讲述控制汇编流程的伪指令,它们在汇编时处理,并可能会引起某些指令块的不同汇编或完全不被汇编。

    2.2.1 数值常量

    =伪指令可以用来定义数值常量。它前面为常量名,后面为提供值的数值表达式。常量值可以为数字或地址,但不同于标号,数值常量不允许拥有基于寄存器的地址。除了这点不同外,数值常量和标号非常类似,它们甚至可以前置引用(在它们的定义前使用它们)。

    当你用已经定义的数值常量定义数值常量时,汇编器把常量当作汇编期间的变量,并且允许赋给新值,但不允许前置引用(很显然的原因)。让我们看一个例子:

    dd sum
    x = 1
    x = x+2
    sum = x

    这里x为汇编期间的变量,每次访问它的值为最后一次使用的值。因此如果在x定义前访问它,比如将“dd sum”指令改为“dd x”,将导致错误。当通过指令“x = x + 2” 重新定义x时,将用先前x的值来计算新的x值。所以当常量sum定义时,x值为3并将赋给sum。因为sum在源码中只定义了一次,它是标准的数值常量,能够前置引用。所以“dd sum”将被汇编为“dd 3”。可阅读2.2.6节来了解汇编器是如何做到解析的。

    数值常量值前可以放尺寸操作符,用来确保值在某个给定范围内,并能影响数值表达式中的这些计算是怎样执行的。例如:

    c8 = byte -1
    c32 = dword -1

    上面的语句将定义两个不同的常量,第一个存放8位,第二个存放于32位。

    当你用地址值(可以基于寄存器寻址)定义常量时,可以使用label伪指令的扩展语法(已在1.2.3节中描述)。例如:

    label myaddr at ebp+4

    上面的语句声明一个ebp+4地址处的标号。然而标号不同于数值常量,不能成为汇编期间的变量。

    2.2.2 条件汇编

    if伪指令导致跟着的指令块仅在特定条件下汇编。它必须跟着指定条件的逻辑表达式,当条件满足时汇编后一行的指令,否则跳过。可选的else if伪指令后面跟着指定条件的逻辑表达式,当前面的条件不满且这个条件满足时汇编后面的指令块。可选的else伪指令开始的指令块,当所有条件都不满足时才汇编。end if伪指令结束最后的指令块。

    应当注意的是if伪指令是在汇编阶段处理的,因此它不会影响任何预处理伪指令,比如符号常量和宏指令的定义 – 当汇编器识别出if伪指令时,所有的预处理已经完成。

    逻辑表达式由逻辑值和逻辑操作符组成。逻辑操作符有“~”逻辑非,“&”逻辑与,“|”逻辑或。逻辑非操作优先级最高。逻辑值可以为数值表达式,如果等于0为false,否则为true。两个数值表达式可以使用以下操作符做比较来生成逻辑值:=(等于),<(小于),>(大于),<=(小于或等于),>=(大于或等于),<>(不相等)。

    跟着符号名的used操作符为逻辑值,用来检查给定符号是否在某些地方使用过(即使在检查位置后使用它也能返回正确的结果)。defined操作符后面可以跟着任何表达式,通常为一符号名;它检查只包含唯一符号的给定表达式是否在源码中已定义并能在当前位置访问。

    下面例子中使用的count常量必须在源码中某处定义:

    if count>0
      mov cx,count
      rep movsb
    end if

    当count大于0时将汇编其中的两条指令。下面为更加复杂的条件结构:

    if count & ~ count mod 4
      mov cx,count/4
      rep movsd
    else if count>4
      mov cx,count/4
      rep movsd
      mov cx,count mod 4
      rep movsb
    else
      mov cx,count
      rep movsb
    end if

    当count不为零且能被4整除时将汇编第一个指令块,如果不满足,将评估跟在else if后面的逻辑表达式,如果为true将汇编第二个指令块,否则汇编else中的指令块。

    还有用来比较符号串操作符。eq比较两个值是否精确相等。in操作符检查给定值是否在其后面给的列表中,列表必须放在中括号中,列表项以逗号分隔。对于汇编器来说,当符号们有相同含义时被认为相同 – 例如对于汇编器pword和fword是相同的。“16 eq 10h”条件为true,而“16 eq 10+4”条件为false。

    eqtype操作符检查比较的两个值结构是否相同,以及结构的元素是否为相同类型。

    常见的类型有数值表达式,单独的字符串,浮点数,地址表达式(在中括号内或者前面有ptr操作符),指令助记符,寄存器,size操作符,跳转类型和代码类型操作符。并且每一个用作分隔符的特殊字符,如逗号或冒号,用来分隔类型。例如,两个由寄存器名和逗号和数值表达式构成的值,将被认为是相同类型,无论使用了什么寄存器和复杂的数值表达式;除非字符串和浮点数,它们为特殊类型的数值表达式,被认为为不同的类型。所以“eax,16 eqtype fs,3+7”条件为true,但“eax,16 eqtype eax,1.6”为false。

    2.2.3 重复块指令

    times伪指令重复一条指令到指定次数。它后面必须跟着重复次数数值表达式和重复指令(可选的冒号用来分隔数字和指令)。当在指令中使用特殊符号“%”时,其值等于当前的重复次数。例如“times 5 db %”将定义五字节的数据1,2,3,4,5。也允许递归使用times伪指令,所以“times 3 times % db %”将定义6字节数据1,1,2,1,2,3。

    repeat伪指令重复整个指令块。它后面必须跟着重复次数数值表达式。重复的指令出现在另一行,最后以end repeat伪指令结束,例如:

    repeat 8
      mov byte [bx],%
      inc bx
    end repeat

    上面的语句生成的代码将把从1到8存储到BX寻址的内存单元内。

    重复次数可以为0,此时不会汇编任何指令。

    break伪指令停止前面的重复,继续汇编end repeat后面的第一行。和if伪指令一起使用,可以在某些特殊条件下停止重复,如:

    s = x/2
    repeat 100
      if x/s=s
        break
      end if
      s = (s+x/s)/2
    end repeat

    当while伪指令后面的逻辑表达式为真时重复指令块。重复指令块必须以end while伪指令结束。在每一次重复前逻辑表达式都将评估,当值为false时,将汇编end while后第一行。此时“%”也用来保存当前重复值。break伪指令也可以像repeat指令那样结束循环。前面的例子可以用while指令重写为:

    s = x/2
    while x/s <> s
      s = (s+x/s)/2
      if % = 100
        break
      end if
    end while

    if,repeat和while定义的块可以任意嵌套,但它们必须以它们开始的顺序依次结束。break伪指令常用来停止处理repeat或while开始的块。

    2.2.4 地址空间

    org伪指令设置后面代码将被期望出现在内存中的地址。它必须跟着指定地址的数值表达式。这个伪指令开始新的地址空间,后面的代码并不会做任何移动,但将影响其中定义的所有标号和$符号,如同被放到给定地址一样。然而程序员必须负责将代码放到执行时正确的位置。

    load伪指令用来从已汇编代码的二进制值中定义常量。这条伪指令必须跟着常量名,可选的尺寸操作符,from操作符和指定了当前地址空间中有效地址的数值表达式。尺寸操作符此时有特殊含义 – 它声明了多少字节(最多为8)将被加载组成常量的二进制值。如果没有指定尺寸操作符,将载入一个字节(因此值的范围为0到255)。载入的数据不能超过当前的偏移。

    store伪指令通过替换前面生成的代码为给定数值表达式来修改已生成的代码。这个表达式可以放置可选的尺寸操作符来指定要定义多大的值。如果没有定义尺寸操作符默认为一个字节。然后at操作符和数值表达式定义了当前地址代码空间中的有效地址。这是个高级伪指令,应当小心使用。

    load和store伪指令操作都限制在当前地址空间中。$$总是等于当前地址空间的基地址,$为当前地址空间的当前地址,因此这两个值定义了load和store能够操作的区域界限。

    load和store一起使用可以为已生成的代码编码。例如在当前地址空间中编码生成的所有代码可以使用这样的伪指令块:

    repeat $-$$
      load a byte from $$+%-1
      store byte a xor c at $$+%-1
    end repeat

    其中代码中每个字节将和常量c异或。

    virtual在指定位置定义虚拟数据。这个数据不会包含到输出文件中,但定义的标号可以在源码中使用。这条伪指令可以跟着at操作符和指定虚拟数据地址的数值表达式(如果没有指定地址将使用当前地址,和“virtual at $”相同)。数据定义指令在另一行,最后以end virtual伪指令结束。virtual指令本身是一个独立的地址空间,当它结束后,将恢复前面的地址空间上下文。

    virtual伪指令可以用来创建一些变量的联合,例如:

    GDTR dp ?
    virtual at GDTR
      GDT_limit dw ?
      GDT_address dd ?
     end virtual

    它在GDTR地址处为48位的变量定义了2个标号。

    它也可以为寄存器寻址的结构定义标号:

    virtual at bx
      LDT_limit dw ?
      LDT_address dd ?
    end virtual

    有了上面的定义后,指令“mov ax, [LDT_limit]”将被汇编为“mov ax,[bx]”。

    在虚拟块中声明已定义的数据值或指令也是有用的,因为load伪指令可以从虚拟的生成代码中载入数据到常量。这条伪指令必须在载入的代码后,虚拟块结束前使用,因为它只能从当前地址空间载入数据。例如:

    virtual at 0
      xor eax,eax
      and edx,eax
      load zeroq dword from 0
    end virtual

    上面的代码将定义zeroq常量,包含4字节定义在虚拟块中的机器码指令。也可以用这个方法从外部文件载入二进制数据。例如:

    virtual at 0
      file ’a.txt’:10h,1
      load char from 0
    end virtual

    上面的语句从文件a.txt 10h文件偏移处载入一字节到char常量。

    2.4中描述的任何section伪指令都将开始一新地址空间。

    2.2.5 其他伪指令

    align伪指令对齐代码或数据到指定边界。它必须跟着对齐大小的数值表达式。边界值必须为2的指数。

    为了实现对齐,align伪指令将把跳过的字节用nop指令填充,同时标识这段区域为已初始化数据,所以它和其他未初始化数据一起将不会出现在输出文件中,对齐字节将执行相同方式。如果你需要填充对齐区域为其他值,可以用align和需要的对齐值virtual自己创建对齐,例如:

    virtual
      align 16
      a = $ - $$
    end virtual
    db a dup 0
    常量a在对齐后地址和virtual块地址之间定义不同(见上一部分),所以它等于需要对齐空间的大小。

    display伪指令在汇编期间显示信息。它必须跟着字符串或者字节数据,用逗号分隔。它也可以用来显示常量,例如:

    bits = 16
    display ’Current offset is 0x’
    repeat bits/4
      d = ’0’ + $ shr (bits-%*4) and 0Fh
      if d > ’9’
        d = d + ’A’-’9’-1
      end if
      display d
    end repeat
    display 13,10

    这个伪指令计算4个16位值的16进制数字,并且以字符显示。注意如果当前地址空间为可重定位的是不可用(比如可能出现在PE或COFF输出格式中),此时只能使用绝对的值。绝对的值可以通过相对地址的计算得到,如同$-$$,或rva $(PE格式中)。

    2.2.6 多遍扫描

    因为汇编器允许在标号或者常量实际定义前引用它们,所以它必须预测这些标号的值。只有有一种情形下怀疑预测失败,它将再次执行扫描,汇编所有源码,基于上遍扫描中得到的标号值来做更好的预测。

    标号值的变化将引起一些指令有不同长度的编码,并能再次引起标号值的变化。既然在表达式中可以使用影响控制伪指令的标号和常量,整个源码在新的一遍扫描中可能完全不同的处理,每一次都试图做更好的预测,直到所有的值都预测正确。它使用各种方法来预测值,对于大多数程序通过几遍扫描就能找到的最短路径。

    一些错误,如值不能符合要求的边界,不能在中间扫描中提示,它可能在某些值预测好后会才出现。如果汇编器遇到一些非法语法或未知指令,它将立即停止。此外多次定义一个标号也将导致错误,因为它会引起无效的预测。

    只有display伪指令创建的消息能在最后一次扫描中显示。当汇编器因为错误停止时,这些消息将返回还没有解析正确的预测值。

    扫描遍数有个上限,当汇编器达到上限后,它将停止并提示无法生成正确代码的消息。看看下面的例子:

    if ~ defined alpha
      alpha:
    end if

    当defined操作符后的表达式在此处能计算时将传给true值,此处意思为alpha标号在某处已定义。但上面的块仅当defined操作符给定的值为false时才会定义,将导致一个xx和是其不能解析那些代码。当处理if伪指令,汇编器必须预测标号alpha是否在某处定义(如果在这遍扫描前已定义就不需要预测了),无论预测出什么,总会发生相反的。一次汇编器将会失败,除非标号alpha在上面指令块之前某地定义 – 此时,正如已经提到的,不需要预测并且块将被跳过。

    上面的例子也可以写成当它没有定义时定义标号。如果失败,因为defined操作符将检查标号是否已定义,它包括在条件处理块中的定义。然后增加些条件就能让它解析:

    if ~ defined alpha | defined @f
      alpha:
      @@:
    end if

    @f总和源码后面最近的@@符号有相同的标号,所以上面的例子中,如果使用了任何唯一名称而不是匿名标号,意思是相同的。当alpha没有在源码其他地方定义时,唯一的可能是当这个块定义并且此时他不会导致XX,因为你们标号标号将会是这个块自行创建。为了更好的理解这点,看看下面这个除了自我创建外没有任何东西的块:

    if defined @f
      @@:
    end if

    这是一个可以有多个方案的源码,当这个块被处理或没有所有这些情形都正确。取决于汇编器算法 – 预测算法。回到前面的例子,当alpha没有在任何地方定义,if条件块不为false,所以你只有一种可能方案,我们希望汇编器能够到达。灵位,当alpha在某些地方定义时,我们将再次得到两种可能方案,但其中将导致alpha定义两次,那样的错误将导致汇编器立即终止汇编,这是一个极深的扰乱解析过程的错误种类,我们得到将取决于汇编器内部的选择。

    然而这些选择中存在某些确定的事实。当汇编器需要检查给定符号是否定义并且它已经在当前扫描中定义时,不需要任何预测 – 这已经在前面说明。当给定符号从未在以前定义,包括所有已经完成的扫描,汇编器将预测它没有定义。知道这点,我们可以期待如同上面的简单的自我创建块不被汇编,上个例子中在外面的条件块前当alpha在某地定义时将被正确解析,当它在前面没有定义时,它家那个定义alpha,因此潜在的导致错误,因为如果alpha在后面某处定义将有两个定义。

    used操作符类似,然而任何预测都不是简单的,你不应依赖它们。

  1. 建议:
    User:
Built on: 2021-5-12 14:12:42
Copyright © 2006-2008 xuyibo.org All rights reserved.