第三章 NASM语言

来源:互联网 发布:档案管理系统软件免费 编辑:程序博客网 时间:2024/05/17 23:36

【转】http://blog.csdn.net/hitop0609/archive/2009/07/07/4329454.aspx

 

3.1 NASM源程序行的组成。
 
就像很多其他的汇编器,每一行NASM源代码包含(除非它是一个宏,一个预处理操作
符,或一个汇编器操作符,参况第4,5章)下面四个部分的全部或某几个部分:

label:    instruction operands        ; comment

通常,这些域的大部分是可选的;label,instruction,comment存在或不存在都是允
许的。当然,operands域会因为instruction域的要求而必需存或必须不存在。

NASM使用反斜线(/)作为续行符;如果一个以一个反斜线结束,那第二行会被认为
是前面一行的一部分。

NASM对于一行中的空格符并没有严格的限制:labels可以在它们的前面有空格,或
其他任何东西。label后面的冒号同样也是可选的。(注意到,这意味着如果你想
要写一行'lodsb',但却错误地写成了'lodab',这仍将是有效的一行,但这一行不做
任何事情,只是定义了一个label。运行NASM时带上命令行选项'-w+orphan-labels'
会让NASM在你定义了一个不以冒号结尾的label时警告你。

labels中的有效的字符是字母,数字,'-','$','#','@','~','.'和'?'。但只有字母
'.',(具有特殊含义,参阅3.9),'_'和'?'可以作为标识符的开头。一个标识符还可
以加上一个'$'前缀,以表明它被作为一个标识符而不是保留字来处理。这样的话,
如果你想到链接进来的其他模块中定义了一个符号叫'eax',你可以用'$eax'在
NASM代码中引用它,以和寄存器的符号区分开。

instruction域可以包含任何机器指令:Pentium和P6指令,FPU指令,MMX指令还有甚
至没有公开的指令也会被支持。这些指令可以加上前缀'LOCK','REP','REPE/REPZ'
或'REPNE'/'REPNZ',通常,支持显示的地址尺寸和操作数尺寸前缀'A16','A32',
'O16'和'O32'。关于使用它们的一个例子在第九章给出。你也可以使用段寄存器
名作为指令前缀: 代码'es mov [bx],ax'等效于代码'mov [es:bx],ax'。我们推荐
后一种语法。因为它和语法中的其它语法特性一致。但是对于象'LODSB'这样的
指令,它没有操作数,但还是可以有一个段前缀, 对于'es lodsb'没有清晰地语法
处理方式
    
在使用一个前缀时,指令不是必须的,像'CS','A32','LOCK'或'REPE'这样的段前缀
可以单独出现在一行上,NASM仅仅产生一个前缀字节。

作为对实际机器指令的扩展,NASM同时提供了一定数量的伪操作指令,这在3.2节
详细描述。

指令操作数可以使用一定的格式:它们可以是寄存器,仅仅以寄存器名来表示(比
如:'ax','bp','ebx','cr0':NASM不使用'gas'的语法风格,在这种风格中,寄存器名
前必须加上一个'%'符号),或者它们可以是有效的地址(参阅3.3),常数(3.4),或
表达式。

对于浮点指令,NASM接受各种语法:你可以使用MASM支持的双操作数形式,或者你
可以使用NASM的在大多数情况下全用的单操作数形式。支持的所以指令的语法
细节可以参阅附录B。比如,你可以写:

              fadd    st1             ; this sets st0 := st0 + st1
              fadd    st0,st1         ; so does this
    
              fadd    st1,st0         ; this sets st1 := st1 + st0
              fadd    to st1          ; so does this

几乎所有的浮点指令在引用内存时必须使用以下前缀中的一个'DWORD',QWORD'
或'TWORD'来指明它所引用的内存的尺寸。

  3.2 伪指令。
 
伪指令是一些并不是真正的x86机器指令,但还是被用在了instruction域中的指
令,因为使用它们可以带来很大的方便。当前的伪指令有'DB','DW','DD','DQ'和
‘DT’,它们对应的未初始化指令是'RESB','RESW','RESD','RESQ'和'REST','INCBIN'
命令,'EQU'命令和'TIEMS'前缀。

  3.2.1 `DB'一类的伪指令: 声明已初始化的数据。

      在NASM中,`DB', `DW', `DD', `DQ'和`DT'经常被用来在输出文件中声明已初始化
      的数据,你可以多种方式使用它们:

            db    0x55                ; just the byte 0x55
            db    0x55,0x56,0x57      ; three bytes in succession
            db    'a',0x55            ; character constants are OK
            db    'hello',13,10,'$'   ; so are string constants
            dw    0x1234              ; 0x34 0x12
            dw    'a'                 ; 0x41 0x00 (it's just a number)
            dw    'ab'                ; 0x41 0x42 (character constant)
            dw    'abc'               ; 0x41 0x42 0x43 0x00 (string)
            dd    0x12345678          ; 0x78 0x56 0x34 0x12
            dd    1.234567e20         ; floating-point constant
            dq    1.234567e20         ; double-precision float
            dt    1.234567e20         ; extended-precision float

'DQ'和'DT'不接受数值常数或字符串常数作为操作数。

  3.2.2 `RESB'类的伪指令: 声明未初始化的数据。
 
`RESB', `RESW', `RESD', `RESQ' and `REST'被设计用在模块的BSS段中:它们声明
未初始化的存储空间。每一个带有单个操作数,用来表明字节数,字数,或双字数
或其他的需要保留单位。就像在2.2.7中所描述的,NASM不支持MASM/TASM的扣留未
初始化空间的语法'DW ?'或类似的东西:现在我们所描述的正是NASM自己的方式。
'RESB'类伪指令的操作数是有严格的语法的,参阅3.8。
比如:

      buffer:         resb    64              ; reserve 64 bytes
      wordvar:        resw    1               ; reserve a word
      realarray       resq    10              ; array of ten reals

  3.2.3 `INCBIN':包含其他二进制文件。
 
'INCBIN'是从老的Amiga汇编器DevPac中借过来的:它将一个二进制文件逐字逐句地
包含到输出文件中。这能很方便地在一个游戏可执行文件中包含中图像或声音数
据。它可以以下三种形式的任何一种使用:

          incbin  "file.dat"             ; include the whole file
          incbin  "file.dat",1024        ; skip the first 1024 bytes
          incbin  "file.dat",1024,512    ; skip the first 1024, and
                                         ; actually include at most 512

  3.2.4 `EQU': 定义常数。

'EQU'定义一个符号,代表一个常量值:当使用'EQU'时,源文件行上必须包含一个label。
'EQU'的行为就是把给出的label的名字定义成它的操作数(唯一)的值。定义是不可更
改的,比如:

message         db      'hello, world'
      msglen          equ     $-message

把'msglen'定义成了常量12。'msglen'不能再被重定义。这也不是一个预自理定义:
'msglen'的值只被计算一次,计算中使用到了'$'(参阅3.5)在此时的含义。注意
‘EQU’的操作数也是一个严格语法的表达式。(参阅3.8)

  3.2.5 `TIMES': 重复指令或数据。

前缀'TIMES'导致指令被汇编多次。它在某种程序上是NASM的与MASM兼容汇编器的
'DUP'语法的等价物。你可以这样写:

zerobuf:        times 64 db 0

或类似的东西,但'TEIMES'的能力远不止于此。'TIMES'的参数不仅仅是一个数值常
数,还有数值表达式,所以你可以这样做:

buffer: db      'hello, world'
times 64-$+buffer db ' '

它可以把'buffer'的长度精确地定义为64字节,’TIMES‘可以被用在一般地指令上,
所以你可像这要编写不展开的循环:

              times 100 movsb

注意在'times 100 resb 1'跟'resb 100'之间并没有显著的区别,除了后者在汇编
时会快上一百倍。

就像'EQU','RESB'它们一样, 'TIMES'的操作数也是严格语法的表达式。(见3.8)

注意'TIMES'不可以被用在宏上:原因是'TIMES'在宏被分析后再被处理,它允许
’TIMES'的参数包含像上面的'64-$+buffer'这样的表达式。要重复多于一行的代
码,或者一个宏,使用预处理指令'%rep'。

  3.3 有效地址
 
  一个有效地址是一个指令的操作数,它是对内存的一个引用。在NASM中,有效地址
  的语法是非常简单的:它由一个可计算的表达式组成,放在一个中括号内。比如:
 
      wordvar dw      123
              mov     ax,[wordvar]
              mov     ax,[wordvar+1]
              mov     ax,[es:wordvar+bx]

任何与上例不一致的表达都不是NASM中有效的内存引用,比如:'es:wordvar[bx]'。

更复杂一些的有效地址,比如含有多个寄存器的,也是以同样的方式工作:

              mov     eax,[ebx*2+ecx+offset]
              mov     ax,[bp+di+8]

NASM在这些有效地址上具有进行代数运算的能力,所以看似不合法的一些有效地址
使用上都是没有问题的:

          mov     eax,[ebx*5]             ; assembles as [ebx*4+ebx]
          mov     eax,[label1*2-label2]   ; ie [label1+(label1-label2)]

有些形式的有效地址在汇编后具有多种形式;在大多数情况下,NASM会自动产生
最小化的形式。比如,32位的有效地址'[eax*2+0]'和'[eax+eax]'在汇编后具有
完全不同的形式,NASM通常只会生成后者,因为前者会为0偏移多开辟4个字节。

NASM具有一种隐含的机制,它会对'[eax+ebx]'和'[ebx+eax]'产生不同的操作码;
通常,这是很有用的,因为'[esi+ebp]'和'[ebp+esi]'具有不同的缺省段寄存器。

尽管如此,你也可以使用关键字'BYTE','WORD','DWORD'和'NOSPLIT'强制NASM产
生特定形式的有效地址。如果你想让'[eax+3]'被汇编成具有一个double-word的
偏移域,而不是由NASM缺省产生一个字节的偏移。你可以使用'[dword eax+3]',
同样,你可以强制NASM为一个第一遍汇编时没有看见的小值产生一个一字节的偏
移(像这样的例子,可以参阅3.8)。比如:'[byte eax+offset]'。有一种特殊情
况,‘[byte eax]'会被汇编成'[eax+0]'。带有一个字节的0偏移。而'[dword
eax]'会带一个double-word的0偏移。而常用的形式,'[eax]'则不会带有偏移域。

当你希望在16位的代码中存取32位段中的数据时,上面所描述的形式是非常有用
的。关于这方面的更多信息,请参阅9.2。实际上,如果你要存取一个在已知偏
移地址处的数据,而这个地址又大于16位值,如果你不指定一个dword偏移,
NASM会让高位上的偏移值丢失。

类似的,NASM会把'[eax*2]'分裂成'[eax+eax]' ,因为这样可以让偏移域不存在
以此节省空间;实际上,它也把'[eax*2+offset]'分成'[eax+eax+offset]',你
可以使用‘NOSPLIT'关键字改变这种行为:`[nosplit eax*2]'会强制
`[eax*2+0]'按字面意思被处理。

  3.4 常数
 
NASM能理解四种不同类型的常数:数值,字符,字符串和浮点数。

  3.4.1 数值常数。
 
  一个数值常数就只是一个数值而已。NASM允许你以多种方式指定数值使用的
  进制,你可以以后缀'H','Q','B'来指定十六进制数,八进制数和二进制数,
  或者你可以用C风格的前缀'0x'表示十六进制数,或者以Borland Pascal风
  格的前缀'$'来表示十六进制数,注意,'$'前缀在标识符中具有双重职责
  (参阅3.1),所以一个以'$'作前缀的十六进制数值必须在'$'后紧跟数字,而
  不是字符。
 
  请看一些例子:

              mov     ax,100          ; decimal
              mov     ax,0a2h         ; hex
              mov     ax,$0a2         ; hex again: the 0 is required
              mov     ax,0xa2         ; hex yet again
              mov     ax,777q         ; octal
              mov     ax,10010011b    ; binary

  3.4.2 字符型常数。

一个字符常数最多由包含在双引号或单引号中的四个字符组成。引号的类型
与使用跟NASM其它地方没什么区别,但有一点,单引号中允许有双引号出现。

一个具有多个字符的字符常数会被little-endian order,如果你编写:

                mov eax,'abcd'

      产生的常数不会是`0x61626364',而是`0x64636261',所以你把常数存入内存
      的话,它会读成'abcd'而不是'dcba'。这也是奔腾的'CPUID'指令理解的字符常
      数形式(参阅B.4.34)

  3.4.3 字符串常数。
 
  字符串常数一般只被一些伪操作指令接受,比如'DB'类,还有'INCBIN'。

一个字符串常数和字符常数看上去很相像,但会长一些。它被处理成最大长
度的字符常数之间的连接。所以,以下两个语句是等价的:

            db    'hello'               ; string constant
            db    'h','e','l','l','o'   ; equivalent character constants

还有,下面的也是等价的:

            dd    'ninechars'           ; doubleword string constant
            dd    'nine','char','s'     ; becomes three doublewords
            db    'ninechars',0,0,0     ; and really looks like this

注意,如果作为'db'的操作数,类似'ab'的常数会被处理成字符串常量,因
为它作为字符常数的话,还不够短,因为,如果不这样,那'db 'ab'会跟
'db 'a''具有同样的效果,那是很愚蠢的。同样的,三字符或四字符常数会
在作为'dw'的操作数时被处理成字符串。

  3.4.4 浮点常量
 
  浮点常量只在作为'DD','DQ','DT'的操作数时被接受。它们以传统的形式表
  达:数值,然后一个句点,然后是可选的更多的数值,然后是选项'E'跟上
  一个指数。句点是强制必须有的,这样,NASM就可以把它们跟'dd 1'区分开,
  它只是声明一个整型常数,而'dd 1.0'声明一个浮点型常数。
 
一些例子:

            dd    1.2                     ; an easy one
            dq    1.e10                   ; 10,000,000,000
            dq    1.e+10                  ; synonymous with 1.e10
            dq    1.e-10                  ; 0.000 000 000 1
            dt    3.141592653589793238462 ; pi

NASM不能在编译时求浮点常数的值。这是因为NASM被设计为可移植的,尽管它
常产生x86处理器上的代码,汇编器本身却可以和ANSI C编译器一起运行在任
何系统上。所以,汇编器不能保证系统上总存在一个能处理Intel浮点数的浮
点单元。所以,NASM为了能够处理浮点运算,它必须含有它自己的一套完整
的浮点处理例程,它大大增加了汇编器的大小,却获得了并不多的好处。
 
  3.5 表达式
 
  NASM中的表达式语法跟C里的是非常相似的。
 
  NASM不能确定编译时在计算表达式时的整型数尺寸:因为NASM可以在64位系
  统上非常好的编译和运行,不要假设表达式总是在32位的寄存器中被计算的,
  所以要慎重地对待整型数溢出的情况。它并不总能正常的工作。NASM唯一能
  够保证的是:你至少拥有32位长度。

NASM在表达式中支持两个特殊的记号,即'$'和'$$',它们允许引用当前指令
的地址。'$'计算得到它本身所在源代码行的开始处的地址;所以你可以简
单地写这样的代码'jmp $'来表示无限循环。'$$'计算当前段开始处的地址,
所以你可以通过($-$$)找出你当前在段内的偏移。

NASM提供的运算符以运算优先级为序列举如下:

  3.5.1 `|': 位或运算符。
 
运算符'|'给出一个位级的或运算,所执行的操作与机器指令'or'是完全相
同的。位或是NASM中优先级最低的运算符。

  3.5.2 `^': 位异或运算符。

      `^' 提供位异或操作。

  3.5.3 `&': 位与运算符。

      `&' 提供位与运算。

  3.5.4 `<<' and `>>': 位移运算符。

      `<<' 提供位左移, 跟C中的实现一样,所以'5<<3'相当于把5乘上8。'>>'提
      供位右移。在NASM中,这样的位移总是无符号的,所以位移后,左侧总是以
      零填充,并不会有符号扩展。
    
  3.5.5 `+' and `-': 加与减运算符。
 
  '+'与'-'运算符提供完整的普通加减法功能。

  3.5.6 `*', `/', `//', `%'和`%%': 乘除法运算符。
 
  '*'是乘法运算符。'/'和'//'都是除法运算符,'/'是无符号除,'//'是带
  符号除。同样的,'%'和'%%'提供无符号与带符号的模运算。
 
  同ANSI C一样,NASM不保证对带符号模操作执行的操作的有效性。
 
  因为'%'符号也被宏预处理器使用,你必须保证不管是带符号还是无符号的
  模操作符都必须跟有空格。
 
  3.5.7 一元运算符: `+', `-', `~'和`SEG'

这些只作用于一个参数的一元运算符是NASM的表达式语法中优先级最高的。
'-'把它的操作数取反,'+'不作任何事情(它只是为了和'-'保持对称),
'~'对它的操作数取补码,而'SEG'提供它的操作数的段地址(在3.6中会有
详细解释)。

  3.6 `SEG'和`WRT'
 
  当写很大的16位程序时,必须把它分成很多段,这时,引用段内一个符号的
  地址的能力是非常有必要的,NASM提供了'SEG'操作符来实现这个功能。
 
  'SEG'操作符返回符号所在的首选段的段基址,即一个段基址,当符号的偏
  移地址以它为参考时,是有效的,所以,代码:

              mov     ax,seg symbol
              mov     es,ax
              mov     bx,symbol

总是在'ES:BX'中载入一个指向符号'symbol'的有效指针。

而事情往往可能比这还要复杂些:因为16位的段与组是可以相互重叠的,
你通常可能需要通过不同的段基址,而不是首选的段基址来引用一个符
号,NASM可以让你这样做,通过使用'WRT'关键字,你可以这样写:

              mov     ax,weird_seg        ; weird_seg is a segment base
              mov     es,ax
              mov     bx,symbol wrt weird_seg

会在'ES:BX'中载入一个不同的,但功能上却是相同的指向'symbol'的指
针。

通过使用'call segment:offset',NASM提供fall call(段内)和jump,这里
'segment'和'offset'都以立即数的形式出现。所以要调用一个远过程,你
可以如下编写代码:

              call    (seg procedure):procedure
              call    weird_seg:(procedure wrt weird_seg)

(上面的圆括号只是为了说明方便,实际使用中并不需要)


NASM支持形如'call far procedure'的语法,跟上面第一句是等价的。'jmp'
的工作方式跟'call'在这里完全相同。

在数据段中要声明一个指向数据元素的远指针,可以象下面这样写:

              dw      symbol, seg symbol

NASM没有提供更便利的写法,但你可以用宏自己建造一个。

  3.7 `STRICT': 约束优化。
 
  当在汇编时把优化器打开到2或更高级的时候(参阅2.1.15)。NASM会使用
  尺寸约束('BYTE','WORD','DWORD','QWORD',或'TWORD'),会给它们尽可
  能小的尺寸。关键字'STRICT'用来制约这种优化,强制一个特定的操作
  数为一个特定的尺寸。比如,当优化器打开,并在'BITS 16'模式下:
 
              push dword 33

      会被编码成 `66 6A 21',而

              push strict dword 33

会被编码成六个字节,带有一个完整的双字立即数`66 68 21 00 00 00'.

而当优化器关闭时,不管'STRICT'有没有使用,都会产生相同的代码。

  3.8 临界表达式。
 
  NASM的一个限制是它是一个两遍的汇编器;不像TASM和其它汇编器,它总是
  只做两遍汇编。所以它就不能处理那些非常复杂的需要三遍甚至更多遍汇编
  的源代码。
 
  第一遍汇编是用于确定所有的代码与数据的尺寸大小,这样的话,在第二遍
  产生代码的时候,就可以知道代码引用的所有符号地址。所以,有一件事
  NASM不能处理,那就是一段代码的尺寸依赖于另一个符号值,而这个符号又
  在这段代码的后面被声明。比如:

              times (label-$) db 0
            label:  db      'Where am I?'

'TIMES'的参数本来是可以合法得进行计算的,但NASM中不允许这样做,因为
它在第一次看到TIMES时的时候并不知道它的尺寸大小。它会拒绝这样的代码。

              times (label-$+1) db 0
              label:  db      'NOW where am I?'

在上面的代码中,TIMES的参数是错误的。

NASM使用一个叫做临界表达式的概念,以禁止上述的这些例子,临界表达式
被定义为一个表达式,它所需要的值在第一遍汇编时都是可计算的,所以,
该表达式所依赖的符号都是之前已经定义了的,'TIMES'前缀的参数就是一个
临界表达式;同样的原因,'RESB'类的伪指令的参数也是临界表达式。

临界表达式可能会出现下面这样的情况:


                       mov     ax,symbol1
       symbol1         equ     symbol2
       symbol2:

在第一遍的时候,NASM不能确定'symbol1'的值,因为'symbol1'被定义成等于
'symbols2',而这时,NASM还没有看到symbol2。所以在第二遍的时候,当它遇
上'mov ax,symbol1',它不能为它产生正确的代码,因为它还没有知道'symbol1'
的值。当到达下一行的时候,它又看到了'EQU',这时它可以确定symbol1的值
了,但这时已经太晚了。

NASM为了避免此类问题,把'EQU'右侧的表达式也定义为临界表达式,所以,
'symbol1'的定义在第一遍的时候就会被拒绝。

这里还有一个关于前向引用的问题:考虑下面的代码段:

              mov     eax,[ebx+offset]
      offset  equ     10

NASM在第一遍的时候,必须在不知道'offset'值的情况下计算指令
'mov eax,[ebx+offset]'的尺寸大小。它没有办法知道'offset'足够小,足以
放在一个字节的偏移域中,所以,它以产生一个短形式的有效地址编码的方
式来解决这个问题;在第一遍中,它所知道的所有关于'offset'的情况是:它
可能是代码段中的一个符号,而且,它可能需要四字节的形式。所以,它强制
这条指令的长度为适合四字节地址域的长度。在第二遍的时候,这个决定已经
作出了,它保持使这条指令很长,所以,这种情况下产生的代码没有足够的小,
这个问题可以通过先定义offset的办法得到解决,或者强制有效地址的尺寸大
小,象这样写代码:
      [byte ebx+offset]

  3.9 本地Labels

NASM对于那些以一个句点开始的符号会作特殊处理,一个以单个句点开始的
Label会被处理成本地label, 这意味着它会跟前面一个非本地label相关联.
比如:

      label1  ; some code
    
      .loop
              ; some more code
    
              jne     .loop
              ret
    
      label2  ; some code
    
      .loop
              ; some more code
    
              jne     .loop
              ret

上面的代码片断中,每一个'JNE'指令跳至离它较近的前面的一行上,因为'.loop'
的两个定义通过与它们前面的非本地Label相关联而被分离开来了。

对于本地Label的处理方式是从老的Amiga汇编器DevPac中借鉴过来的;尽管
如此,NASM提供了进一步的性能,允许从另一段代码中调用本地labels。这
是通过在本地label的前面加上非本地label前缀实现的:第一个.loop实际上被
定义为'label1.loop',而第二个符号被记作'label2.loop'。所以你确实需要
的话你可写:

      label3  ; some more code
               ; and some more
    
               jmp label1.loop

有时,这是很有用的(比如在使用宏的时候),可以定义一个label,它可以
在任何地方被引用,但它不会对常规的本地label机制产生干扰。这样的
label不能是非本地label,因为非本地label会对本地labels的重复定义与
引用产生干扰;也不能是本地的,因为这样定义的宏就不能知道label的全
称了。所以NASM引进了第三类label,它只在宏定义中有用:如果一个label
以一个前缀'..@'开始,它不会对本地label产生干扰,所以,你可以写:

      label1:                         ; a non-local label
      .local:                         ; this is really label1.local
      ..@foo:                         ; this is a special symbol
      label2:                         ; another non-local label
      .local:                         ; this is really label2.local
    
              jmp     ..@foo          ; this will jump three lines up

NASM还能定义其他的特殊符号,比如以两个句点开始的符号,比如
'..start'被用来指定'.obj'输出文件的执行入口。(参阅6.2.6)