NASM网际编译器手册(四)

第三章 NASM语言

3.1 NASM源码行的分布
象许多编译器一样,每个NASM的源码行(除非它是一个宏,一个预处理符或一个汇编定向符请见第四章和
第五章)都包含4个域
标号: 指令 操作符 ;注释
通常,这些域的一些为可选的:一个标号,一个指令和一个注释是可选的,当然,在指令域不存在时,操作符域
是必需的.
NASM在一行中是不限制空格的个数的:标号前可以有空格,指令前可以没有空格,标号后的冒号是可选的.(
注意,这意味着如果你愿意用一行来写lodsb,然后用lodab,那么这行只定义了一个标号而且是一个什么也
不做的有效代码行.运行用命令行-w+orphan-labels运行NASM,编译器将会提示你:一个没有冒号的标号行被
定义.)
标号里的有效字符为:字符,数字,_,$,#,@,~,.和?.定义标号的第一个字符必须为字母.,_和?,(详细信息
见第3.8节).一个标号如果带前缀$时,表示它将做为一个标号来处理而是一个保留字;因此,如果你用一个叫做
eax的符号来链接其它的模块,你应该在NASM的代码中用$eax来区别其它寄存器.
这个指令域可以包含什么机器指令:Pentium和P6的指令集,FPU指令,MMX指令和其它未公开的指令.这个指令集
通常也可以用前缀:LOCK,REP,REPE/REPZ或REPNE/REPNZ.编译器用前缀A16,A32,O16和O32来表示显式的地址
尺寸和操作符尺寸,其中第9章有关于这方面的一个例子.你也可以用段寄存器的名字做为一条指令的前缀:
coding 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-style语
法,这种语法中的寄存器名字前必须加a %sign),或有效地址(见第3.3节),常量(见3.4节)或表达式(
见3.5节)。
对于浮点数指令,NASM则有一个范围很广的语法:你可以象MASM一样用两个操作符形式,你也可以用NASM
本身的许多单操作符指令。所有支持的指令格式见附见A,例如你可以写:
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机器指令,但由于它们易于理解,所以经常在指令域中使用。当前的
伪操作符指令有:DB,DW,DD,DQ和DT,与其它指令一起用闹噶钣校篟ESB,RESW,RESD,RESQ和REST,
INCBIN命令,EQU命令,TIMES前缀。
3.2.1 DB和有关指令:定义初始化数据
DB,DW,DD,DQ和DT在MASM中用的很多,它们是用来定义输出文件的初始化数据了解的,它们使用的方式很
多:
db 0x55 ;只定义一个字节0x55
db 0x55,0x56,0x57 ;成功定义三个字节
db \'a\',0x55 ;定义字符常量
db \'hello\',13,10,\'$\' ;这是字串常量
dw 0x1234 ;0x34 0x12
dw \'a\' ;0x41 0x00(它只是一个数字)
dw \'ab\' ;0x41 0x42(字符常量)
dw \'abc\' ;0x41 0x42 0x43 0x00(字串)
dd 0x12345678 ;0x78 0x56 0x34 0x12
dd 1.234567e20 ;浮点数常量
dq 1.234567e20 ;双精度浮点数
dt 1.234567e20 ;扩展的双精度浮点数

DQ和DT不能接受数字常量和字符串常量做为操作数。
3.2.2RESB及相关符号:定义未初始化的数据
RESB,RESW,RESD,RESQ和REST被用在一个模块的BSS段:它们定义未初始化的存储空间。这些操作符中的
每一个都可以带一个操作数,这个操作数可以是一些字节,字,或双字来保存空间。这些在第2.2.7节中
描述。NASM不支持MASM/TASM用DW来定义未初始化的空间或类似的语法:RESB伪操作数是一个临界的表达式。
见第3.7节说明。
例如:
buffer: resb 64 ;保留64个字节
wordvar: resw 1 ;保留一个字
realarry resq 10 ;10个实数列表

3.2.3 INCBIN:包含外部的二进制文件
INCBIN是从旧的Amiga汇编器DevPac引用来的:它包含一个二进制文件verbatim到一个输出文件。这点对于
将一图象与声音数据直接加到一个关于游戏的程序是十分方便的。它可以有以下三种表示方式:
incbin "file.dat" ;包含整个文件
incbin "file.dat",1024 ;跳过前1024个字节
incbin "file.dat",1024,512 ;跳过前1024个字节,并且最多只包含512个字节

3.2.4 EQU:定义常量
EQU字义一个给定的符号为常数:当EQU被用时,源码行必须包含一个标号。EQU的用法是给一个数定义一个
标号名字。这个定义是固定的,不能在以后的代码改变它。例如:
message db \'hello,world\'
msglen equ $-message

定义msglen为常数12.msglen不能在以后重新定义。这也不是一个预处理定义:msglen是当时就被赋值的,
用$符号(见第三3.5节关于$的说明)定义时,意思是说它的值为后面字串的长度。要注意的是EQU也是一个
临界表达式。(说明见3.7)。


3.2.5 TIMES:重复指令和数据
TIMES前缀将使指定的指令多次进行编译。这个指令等价于在MASM及其兼容的编译器中的DUP指令的用法。
在你的代码中可以这样写:
buffer: db \'hello,world\'
times 64-$+buffer db \' \'
以上指令将会使用一个存储空间,这个空间会存储缓冲区上限到64时的总长度。最后,TIMES可以和普通指
令用, 你可以用以下指令来执行一个不用回退的动作:
times 100 movsb

要注意的是,指令times 100 resb 1和 resb 100之间是没有什么区别的,准确的说后者在编译器的内部运行
时将比前者快100倍左右。
TIMES指令与EQU,RESB及其它相关指令一样,是一个临界的表达式(见第3.7节说明)
注意,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] ;象汇编语句:[ebx*4+ebx]一样
mov eax,[label1*2-label2];等价于[label1+(label1-label2)]

有些有效地址格式有很多种形式,NASM在这种情况将会生成最小的一种格式,这将不同于32位的有效地址
[eax*2+0]和[eax+eax],NASM通常对后者将会用4 个字节来存储一个0偏移。
NASM有一个提示机制来使[eax+ebx]和[ebx+eax]来生成不同的操作数;由于[esi+ebp]和[ebp+esi]用不同的
默认值寄存器。
然而,你可以用关键字BYTE,WORD,DWORD和NOSPLIT来限制NASM在一个特殊的表格中生成一个有效地址。
如果你想用双字偏移来编译[eax+3]来代替NASM通常的一个字节, 你可以用[dword eax+3].同样,你可以
用[byte eax+offset]来限制NASM用一个字节偏移来编译一个第一遍没有编译的小值。(第3.7节有这样的
一个例子)[byte eax]将会用一个人偏移来编译[eax+0],而[dword eax]将用一个双字节的0偏移。
正常格式的[eax]是没有偏移的这个域的。
同样的,NASM由于允许偏移域和空间被存储,所以会将[eax*2]分成[eax+eax];事实上,它也会将[eax*2+
offset]分成[eax+eax+offset]。你也可以NOSPLIT关键字来阻止这种情况产生:[nosplit eax*2]将会
变成[eax*2+0]。


3.4 常量
NASM将常量分成4种类型:数字,字符,字串和浮点数。


3.4.1 数字常量
数字常量是简单的数字。NASM允许你指定不同进制来定义数字常量。你可以用后缀H,Q,B来分别表示十六
进制,八进制,二进制,或者你也可以用前缀0x来表示C形式的十六进制,或者你也可以用前缀$来表示
Pascal形式的十六进制。注意,前缀$有两种用法(见第3.1节),所以一个十六进制用前缀$来表示时,
必须用一个数字做为第一个字母。
例如:
mov ax,100 ;十进制
mov ax,0a2h ;十六进制
mov ax,$0a2 ;还是十六进制,0是需要的
mov ax,0xa2 ;还是十六进制
mov ax,777q ;十进制
mov ax,10010011b ;二进制

3.4.2 字符常量
一个字符常量为一个由单引号或以引号包含的最多面手个字符的形式。这种引用方式对NASM没有区别,除非
有后的常量用单引号引用允许在里面用双引号,反之亦然。
多于一个字符的字符常量将会以little-endian来排列,例如:
mov eax,\'abcd\'

那么常量将不是0x61626364,而是0x64636261,所以如果你想将一个值存到内存中,它将读成abcd而不是
dcba。这也是字符常量被Pentium\'sR CPUID指令接受的原因。


3.4.3 字串常量
字串常量只能被命令为DB系列和INCBIN伪操作符处理,
一个字串常量看起来更象一个字符常量,只是长度不同。它的处理方式为根据实际情况将最大尺寸
的字符常量连接而成。所以下而的例子是等价的:
db \'hello\' ;字串常量
db \'h\',\'e\',\'l\',\'o\' ;等价的字符常量

下面的也是相互等价的:
dd \'ninechars\' ;双字字串常量
dd \'nine\',\'char\',\'s\';为三个双字常量
db \'ninechars\',0,0,0;看起来更合理些

注意当用db做为操作数时,一个象‘ab’这样的常量做为字串常量时将会尽量变成一个短的字符常量,
因为db \'ab\'将简单和\'db \'a\'等价。同样,三个字符和四个字符的常量用dw来处理时是一样的。


3.4.4 浮点数常量
浮点数只能用DD,DQ和DT伪操作来处理。他们将在一个传统的格式表达:数字,数值,然后是一系列可选项
的数字然后是一个以指数E结尾。所以NASM将会区分出dd 1 为定义一个整数常量而dd 1.0为定义一个浮点数
常量。
例子:
dd 1.2 ;一个简单例子
dd 1.e10 ;10,000,000,000
dd 1.e+10 ;与1.e10相同
dd 1.e-10 ;0.000 000 000 1
dd 3.141592653589793238462;pi

NASM不能对浮点数常量进行编译时算术运算。这是因为NASM被设计成便携的,虽然它经常生成可以在x86上
运行的代码,汇编程序将用ANSI C的编译器运行在任何系统上。然而,编译器将不能保证Intel的数字格式
上浮点数的兼容性,所以NASM只能处理它自己的浮点数运算,使汇编器由于一点好处而增加尺寸。

3.5 表达式
NASM里的表达式与C语言中的相似。
NASM不能保证编译时表达式的整数尺寸:因此NASM能够很轻松的在64位系统上编译并运行,
不能假设表达式等同与32位寄存器而故意使整型溢出。这样它或许不能工作。NASM能保证ANSI C做到的都
能做到:你到少在32位上工作。
NASM支持两种特殊的表达式, 允许计算当前汇编的位置:$和$$,$为到表达式行开始的位置且包含表达
式,所以你可以用JMP $来处理一个无限循环的操作。$$当前段的位置,所以你可以用($-$$)得到当前位置到
当前段有多远的。

按优等权的等级,将算术操作符列在下面

3.5.1 |:或操作
|操作给出了一个或逻辑运算,和OR机器指令一样。OR是NASM中算术运算级别最低的。


3.5.2 ^:异或运算
^提供一个异或运算操作。

3.5.3 & 与运算操作
&提供一个与运算操作。

3.5.4 <<和>>:位移动操作
<<给出了一个左移的操作,与C语言一样,所以5<<3等价与5 乘除或49。>>给出了一个位右移操作;在NASM中
移动操作经常是无符号的,所以从左边开始移位相当填入0而不是最高位的一个符号扩展。

3.5.5 +和-:加与减运算
+和-运算完全为最常见的加减运算。

3.5.6 *,/,//,%和%%:乘法与除法运算
* 是乘法运算符,/和//都是除法运算符:/是无符号除法运算,//是有符号除法运算。同样,%和%%提供了
无符号和有符号的取模运算。
NASM象ANSI C一样不能保证有答号取模的精度。
因此%在宏操作中用的更广泛一些,你应该尽量保证无论什么地方有符号与无符号取模后面都有空格。

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。
这样可能会变得复杂:由于16位段与组可能有叠加,你可能想用不同的段基址来引用一些符号。NASM允许你
这样做,用WRT(With Reference To)关键字。所你可以这样做:
mov ax,weird_seg ;weird_seg是一个段基址
mov ea,ax
mov bx,symbol wrt weird_seg

取ES:BX的方法不同,但实现的功能相同,指针都指向符号symbol。

NASM支持一个远调用(内部段)及一个段:偏移形式的跳转。这里段和偏移都是立即数。所以进行一个远
调用。你也可以用以下代码:
call (seg procedure):procedure
call weird_seg:(procedure wrt weird_seg)

(为了更详细说明括号里的为注释,可以不要).
NASM支持远调用的语法,和上面第一种用法相同。JMP和CALL工作方法相同。
在数据段里为了定义一个远指针,你必须这样写:
dw symbol,seg symbol

虽然你可以经常构造一些宏观世界处理,但NASM支持象上面的不方便的方式。


3.7 临界表达式
NASM的一个局限性在于它是一个两遍的编译器;不象TASM或其它的编译器,
它只做两遍编译。因而它不能应付哪些要二遍以上的复杂源码文件。
第一遍编译用来检查被编译数据和代码的尺寸,而第二遍编译用来生成物所有代码,已知的符号地址和代
码引用地址。所以NASM不能处理的一件事情是代码的尺寸由一个在定义在代码后的决定的情况。例如:
times (label-$) db 0
label: db \'Where am I?\'

在这个例子中,TIME后的参数根本什么都不等于。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的symbol2没有被NASM找到。第二遍编译时,
它遇到了mov ax,symbol1,它由于仍然不知道symbol1的值所以仍然不能生成代码。当它处理下一行时,它
找到EQU并且得到解决symbol1的值,但这已经太晚了。
NASM通过定义一个EQU的右边临界表达式来解决这个问题。这样在第一遍编译时symbol1将会被拒绝。
这有一些相关的例子:
mov eax,[ebx+offset]
offest equ 10

NASM在第一遍编译时,必须计算出指令:mov eax,[ebx+offset]的尺寸,虽然不知道offset 的值。它无法
知道offset是一个1字节的小值还是短整型格式有效地址的编码。它所能知道的就是在第一遍编译中,offset
应为一个代码 的一个符号,它可能用4个字节来填充。所以它为了适应4字节地址部分来记算指令的尺寸。
在第二遍编译中,为了维持这个判断,它要被迫使指令变得很大,所以这种方式产生的代码将不再是以前
哪么小。这种问题可以通过在offset前定义它或用[byte ebx+offset]来限制有效地址的尺寸。

3.8 本地标号
NASM对标号开始的部分进行了特殊处理,一个在单周期开始的标号为一个本地标号,也就是说它与以前的非
本地标号有关。所以例如:
label1 ;一些代码
.loop ;一些代码
jne .loop
ret
label2 ;一些代码
.loop ;更多的代码
jne .loop
ret

在上面的代码中,每个JNE都跳到它前面的行中,这是因为两个.loop的定义被前面的非本地标号分开了。
本地标号的处理方式是从旧Amiga汇编器DevPac中借鉴来的。然而NASM对其进一步发展了,允许访问其它代
码中的本地标号。这意味着在前面非本地标号中定义标号是可以的。上面的第一个.loop的定义是定义一个
符号label1.loop,第二个定义是定认一个符号label2.loop所以你可以这样写代码:
label3 ;一些代码
;更多代码
jmp label1.loop

这样做是可以的-在宏中,例如可以定义一个在任何地方被访问的标号但不能被正常本地标号机制所打扰。
这样的标号不能是非本地标号,因为它将被除数后来本地标号的定义,引用所打扰;它也不能是本地的,因
为定义它的宏不知道这个标号的全名字。NASM因此引用了第三种标号,它只在宏定义中有用;如果一个标号
用一个前缀:..@,那么它在本地标号机制中什么都不做。所以你可以写:
label1: ;一个非本地标号
.local: ;这是一个标号label1.local
..@foo:;这是第三种标号
label2: ;另一个非本地宏
.local: ;这是一个标号label2.local
jmp ..@foo ;这将向前跳三行。

NASM也兼容在双周期定义其它的指定符号例如:..start用来指定obj输出格式的入口点。(见第6.2.6节)。

原文地址:https://www.cnblogs.com/cnlmjer/p/4099883.html