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中的有效的字符是字母,数字,’-’,’KaTeX parse error: Expected 'EOF', got '#' at position 4: ','#̲','@','~','.'和'…‘前缀,以表明它被作为一个标识符而不是保留字来处理。这样的话,
如果你想到链接进来的其他模块中定义了一个符号叫’eax’,你可以用’’ ; 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 ‘(参阅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'。 更复杂一些的有效地址,比如含有多个寄存器的,也是以同样的方式工作: mov eax,[ebx*2 ecx offset] mov ax, 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)。比如:''。有一种特殊情 况,‘'会被汇编成'[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 eax2]‘会强制[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-KaTeX parse error: Double superscript at position 32: … 'Where am I?'
'̲TIMES'的参数本来是可以合… 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的办法得到解决,或者强制有效地址的尺寸大
小,象这样写代码:
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
来源:CSDN
作者:Sa Rockefeller
链接:https://blog.csdn.net/weixin_45495531/article/details/104170997