NASM 文档 - Section 3
这篇翻译是当时学习汇编语言的时候写的。当时课本用的是MASM,但可惜MASM没有Linux版,因此,用起来不太方便。于是,参考了Linux下一些汇编器的语法后,觉得NASM和MASM的比较相似,于是就试着看NASM的文档,也试着翻译。所以,这里只是翻译了第三节,关于NASM语法的文章。
关于NASM更多信息,请访问:http://www.nasm.us/
第三节:NASM语言结构
3.1 NASM 源码的单行结构
和很多汇编器一样,每一行 NASM 源码都包含四部分的组合(除了宏、预处理指令、汇编指令外,参看 第四节 和 第六节):
标签: 操作指令 操作数 ; 注释
像一些语言那样,大多数部分都是可选的;标签、指令和注释部分出现是允许的。当然,操作数部分是否出现视操作指令格式而定。
NASM 使用反斜线作为换行字符(直译是“行继续字符”(line continuation character));如果一行以反斜线结束,下一行会被认为上一行的一部分。
NASM 没有限制一行中空白部分空白字符的数目,标签前或者指令前可以有任意个空白字符或者没有空白字符。标签后的冒号也是可选的。(也就是说,如果你在一行中仅仅写上了 lodsb 或者意外地输入了 lodab,那么这行仍然会被认为是有效的源码行,不过编译器会认为这行代码定义了一个标签。为了编免这种情况,运行 NASM 的时候加入参数 -w+orphan-labels 会使 NASM 编译器在遇到这种情况的时候输出一句警告。)
× 注意:这里的“空白字符”指 tab 或者 空格。
标签中的有效字符包括字母、数字、_、$、#、@、~、.以及?(注意都是半角英文字符)。但一个标识符必须以字母或者“.”、“_”、“?”开始(以“.”开始的标识符有特殊意义,参见 3.9 节)。在标识符前面加上$可以指示它是标识符而不是一个保留字符;因此,如果你的源码要链接的某些模块中定义了一个叫eax的标识符,你可以通过$eax来区分这个标识符和寄存器eax。一个标识符最长可以有4095个字符。
指令部分可以包含任意的机器指令:奔腾指令、P6指令、浮点指令(FPU)、MMX指令甚至没有公开文档的指令。指令可以加入LOCK、REP、REPE/REPZ或者REPNE/REPNZ作为前缀。NASM也提供了显式的地址大小和操作数大小前缀 A16、A32、A64、064;在第 10 节给出了一个例子。你也可以使用一个段寄存器的名称作为一个指令的前缀:编写 es mov [bx],ax 等价于 编写 mov [es:bx],ax 。我们推荐后者的语法,因为它和 NASM 其他一些语法特征一致,但对于 LODSB 那些没有操作数的指令来说,这种写法是必须的。
一条指令不一定被要求使用一个前缀:对于某些操作,NASM 会制动生成这些前缀的字节码。
相对于机器码, NASM 也支持相当数量的伪指令,在 3.2 节中有描述。
指令操作数可能有一下集中形式:它们可以是寄存器、直接用名字来操作( NASM 对寄存器名字的处理和 Intel MASM的是一致的)或者,它们可以是有效的地址(参见 3.3 节),常量(参见 3.4 节)或者是表达式(参见 3.5 节)。
对于 x87 浮点运算指令,NASM 接受很多种语法:你可以使用像其它 MASM 支持的双操作数指令,或者,在很多情况下,你也可以使用 NASM 原生的单操作数指令。举例来说:
fadd st1 ; 把寄存器 st0 的值加上寄存器 st1 的值的和再传回 st0 fadd st0, st1 ; 效果和上面一句一样 fadd st1, st0 ; 把寄存器 st1 的值加上寄存器 st0 的值的和再传回 st1 fadd to st1 ; 效果和上一句一样
几乎所有 x87 浮点操作都必须使用前缀 DWORD 、 QWORD 或者 TWORD 来指明操作数的内存的大小。
3.2 伪操作指令
伪操作指令是指那些相对于真实的 x86 机器指令外的附加指令,它们被用在语句的指令部分只是因为那样比较方便。
当前编译器预定义的伪指令有 DB 、 DW、DD、DQ、DT、DO 和 DY(指明声明的数据在内存分配的空间大小);相似地,如果声明的是未初始化的数据,那么对应的指令是 RESB、RESW、RESD、RESQ、REST、RESO 和 RESY;INCBIN 命令、EQU 命令和 TIMES 前缀都是当前预定义的伪指令(参见下文)。
3.2.1 DB 以及其它类型:声明初始化的数据
DB 、 DW、DD、DQ、DT、DO 和 DY伪指令在 NASM 中的用法和 MASM 中大部分是一致的,就是用来在输出文件中声明初始化的数据。可以通过下面的例子来看看 NASM 支持的多种语法:
db 0x55 ; 像 byte 0x55 db 0x55,0x56,0x57 ; 连续声明 3 个字节 db 0x55 ; 就像 byte 0x55 db 0x55,0x56,0x57 ; 声明 3 个连续的字节 db 'a',0x55 ; 可以使用字符常量 db 'hello',13,10,'$' ; 字符串常量也可以 dw 0x1234 ; 解释为 0x34 0x12 dw 'a' ; 0x61 0x00 (只是数字) dw 'ab' ; 0x61 0x62 (字符常量) dw 'abc' ; 0x61 0x62 0x63 0x00 (字符串) dd 0x12345678 ; 0x78 0x56 0x34 0x12 dd 1.234567e20 ; 浮点数常量 dq 0x123456789abcdef0 ; 8 字节的常量 dq 1.234567e20 ; 双精度浮点数 dt 1.234567e20 ; 拓展精度的浮点数
注意的是 DT、 DO 和 DY 不接受树脂常量作为操作数。
3.2.2 RESB 以及其它: 声明未初始化数据
RESB、RESW、RESD、RESQ、REST、RESO 和 RESY 是用在一个模块的 BSS 段的:它们声明一些未初始化的存储空间。它们中的每一个都需要一个单操作来指明空间大小。像 2.2.7 节中指出的那样,NASM 不支持 MASM/TASM 的预留未初始化空间的语法(DW ?或者其它相似的语法)。RESB类型的伪指令操作数是一个重要的表达式(见 3.8 节)
举例来说:
buffer: resb 64 ; 保留 64 字节 wordvar: resw 1 ; 保留一个 word 类型空间 realarray resq 10 ; 10 个 real 类型的数据 ymmval: resy 1 ; 一个 YMM 寄存器
3.2.3 INCBIN:引入外部二进制文件
INCBIN 命令是老的 Amiga 汇编器 DevPac 中借来的:这条命令引入一个二进制文件到输出文件中。举例来说,这样可以很轻易地直接把一些图形和音频信息包含到一个游戏的可执行文件中。可以像下面三种方式之一来使用这条指令:
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 节对 $ 符号的解释)而不是每次使用这个常量的时候都把当前的 $ 的值代入计算一次。
3.2.5 TIMES:重复指令
TIMES 前缀可以使使指令重复汇编多次。这条指令的行为就像 MASM 兼容汇编器支持的 DUP 语法,在 NASM 你以这样编写:
zeorobuf: times 64 db 0
或者相似的写法;但 TIMES 的用法不仅仅局限于数据声明。TIMES 的参数不仅可以是一个数值常量,还可以是一个数值表达式,因此,你还可以这样写:
buffer: db 'hello, world' times 64-$+buffer db ' '
这句话将把 buffer 的内存空间定义为正好是 64 字节。最后,TIMES 还可以应用到普通的指令中,所以,你可以在其中写单调展开的循环:
times 100 movsb
要注意的是 times 100 resb 1 和 resb 100 是没有任何效果上的差别的,但由于汇编器内部实现的差异,后者汇编以后相对前者运行起来快 100 倍。
TIMES 命令后跟的操作数也是一个重要的表达式(见第 3.8 节)。
注意 TIMES 不能应用在宏中:原因是 TIMES 在宏预处理以后才被汇编,使得像 64-$+buffer这样的表达式作为 TIMES 命令的参数。要重复执行多行代码,或者重复执行一个复杂的宏,使用预处理命令 %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 会生成可能的最小形式。例如,两条完全不同的汇编形式 [eax*2+0] 和 [eax+eax] 进行寻址的话,NASM 会生成后者的形式,因为前者要求 4 个字节来存储一个 0 偏移量。
NASM 有一个提示机制可以把 [eax+ebx] 和 [ebx+eax] 生成不同的操作码;在某些时候,这会很有用,因为 [esi+ebp] 和 [ebp+esi] 有不同的默认端寄存器。
但是你也可以强制 NASM 生成一个特定格形式的有效地址,通过使用关键字 BYTE、WORD、DWORD 和 NOSPLIT 。如果你需要把 [eax+3] 变为使用双字类型的偏移而不是默认的单字节形式,则只需要编写 [dword eax+3] 来指明。相似地,要强制 NASM 对一些初次使用的比较小的数值使用单字节的偏移(参见 3.8 节的一段代码例子),可以使用 [bye eax+offset] 。特殊地, [byte eax] 等价于写 [eax+0] 把一个字节的 0 偏移编码进去,而 [dword eax] 会把一个双字的 0 偏移编码进去。而正常的形式,[eax] 则不会额外编码任何的偏移量。
3.4 常量
NASM 支持 4 种类型的常量:数字、字符、字符串和浮点数。
3.4.1 数值常量
一种数值常量就是一个简单的整数。NASM 允许程序员书写多种进制的数值。你可以在后面加 H 或者 X 来指定十六进制数,D 或者 T 指定十进制数,Q 或者 O 指定八进制数,B 或者 Y 指定二进制数。或者,你可以用 0x 前缀来书写十六进制数(C风格),前缀 $ 来写十六进制数(Pascal 风格或者 Motorola汇编风格)。注意的是,这里的 $ 作为前缀的时候有是嗯场含义。(看 3.1 节),所以一个十六进制数加一个 $ 前缀是,紧跟着 $ 的必须是数字而不是字母。另外,当前版本的 NASM 接受 0h 作为十六进制数的前缀,0d 或者 0t为十进制数前缀,0o 或者 oq 为八进制数前缀,以及 0b 或者 0y 作为二进制数的前缀。请 C 程序员注意,0 后面跟一个数并不代表这个常量数值是八进制的。
对于位较长的数值,NASM 支持用下划线(_)来分隔位数,而在数值中的下划线没有什么特别意义。
一些有效的常量:
mov ax,200 ; 十进制 mov ax,0200 ; 十进制 mov ax,0200d ; 显式声明十进制 mov ax,0d200 ; 也是十进制 mov ax,0c8h ; 十六进制 mov ax,$0c8 ; 十六进制,注意 $ 后面要跟一个数 mov ax,0xc8 ; 十六进制 mov ax,0hc8 ; 还是十六进制 mov ax,310q ; 八进制 mov ax,310o ; 八进制 mov ax,0o310 ; 也是八进制 mov ax,0q310 ; 还是八进制 mov ax,11001000b ; 二进制 mov ax,1100_1000b ; 二进制 mov ax,1100_1000y ; 二进制 mov ax,0b1100_1000 ; 二进制 mov ax,0y1100_1000 ; 二进制,且数值和上面一样
3.4.2 字符串
一个字符串由最多八个字符组成,可以被单引号(')、双引号(")或者反引号(`)括起来。在 NASM 中,单引号和双引号是等价的(不过,单引号括起来的字符串允许双引号出现,而双引号括起来的字符串允许单引号出现);而它们内容的单个字符就是代表单个字符。而在反引号括起来的字符串支持 C 语言类似的转义字符。
以 \u 或者 \U 指定的 Unicode 字符会被转义为 UTF-8 。举例来说,下面的字符串是等价的:
db `\u263a` ; UTF-8 smiley face db `\xe2\x98\xba` ; UTF-8 smiley face db 0E2h, 098h, 0BAh ; UTF-8 smiley face
3.4.3 字符常量
一个字符常量由一个最多 8 字节长的字符串组成,用于一个表达式上下文中。它会被视为一个整数。
一个多于一个字节长的字符常量会用小字序重新整理。如果你这样写
mov eax,'abcd'
那么这个常量并不等于 0x61626364 而等于 0x6636261 ,所以如果你把这些内容存储到内存中,你得到的是 dcba 而不是 abcd 。Pentium 的 CPUID 指令也是同样处理的字符串的。
3.4.4 字符串常量
字符串常量指的是在一些伪指令上下文中使用字符串,主要是 DB 系列伪指令和 INCBIN (被用作文件名)。它们也在一些预处理指令中使用。
一个字符串常量想一个字符常量,只是更长了。它被处理为一系列的最大长度的字符常量。
db 'hello' ; db 'h','e','l','l','o' ; 等价字符串
下面的字符串也是等价的:
dd 'ninechars' dd 'nine','char','s' ; 三个双字空间 db 'ninechars',0,0,0 ; 上面的内容实际上像这个定义
当在一个支持字符串常量的上下文中,括起来的字符串会被解释为字符串常量即使它们像字符常量那么短。因为,如果不是那样的话, db 'ab' 会和 db 'a' 一样,这样并不是我们所要得到的结果。相似地,三个字符或者四个字符常量会被视作字符串当它们是 DW 的操作数等等。
3.4.5 Unicode 字符串
特殊操作符 __utf16__ 和 __utf32__ 支持 Unicode 字符串的定义。它们把二进制按照 UTF-8 的规则来读取然后对应地转换为 UTF-16 或者 UTF-32 (小尾段)。
%define u(x) __utf16__(x) %define w(x) __utf32__(x) dw u('C:\WINDOWS'), 0 ; utf-16 形式的字符串 dd w(`A + B = \u206a`), 0 ; utf-32 形式的字符串
__utf16__ 和 __utf32__ 可以被应用在传递给 DB 系列伪指令的字符串参数或者在一个表达式中的字符常量。
3.4.6 浮点数常量
浮点数常量只能作为 DB、DWDD、DQ、DT 和 DO,或者作为特殊的操作符 __float8__、__float16__、__float32__、float64__、float80m__ 、__float80e___、__float128l__ 以及 __float128h__ 的参数。
浮点数常量会用传统意义的形式——数位、点然后是任意数位来表达,或者用 E 来表达一个科学记数法形式的实数。小数点是用于标示一个浮点数的,也就是说,dd 1 标示声明一个整数常量而 dd 1.0 声明一个浮点常量。
NASM 也支持 C99 形式的十六进制浮点数:0x + 十六进制数字 + 小数点 + 小数位 + P + 二进制表达的十进制数。额外地,NASM 也支持 0h 和 $ 前缀作为十六进制数,以及 0b 、 0y 和 Oo、Oq 相应地作为二进制、八进制前缀。
在 NASM 中,在浮点数间加下划线也是允许的。
下面的表达都是正确的:
db -0.2 ; "Quarter precision" dw -0.5 ; IEEE 754r/SSE5 half precision dd 1.2 ; an easy one dd 1.222_222_222 ; underscores are permitted dd 0x1p+2 ; 1.0x2^2 = 4.0 dq 0x1p+32 ; 1.0x2^32 = 4 294 967 296.0 dq 1.e10 ; 10 000 000 000.0 dq 1.e+10 ; synonymous with 1.e10 dq 1.e-10 ; 0.000 000 000 1 dt 3.141592653589793238462 ; pi do 1.e+4000 ; IEEE 754r quad precision
对于 8 位的 "quarter-precision" 浮点数格式是 符号:指数:位数 = 1:4:3 以及一个指数。这是最常用的 8 位浮点数格式,虽然它并没有被任何正式标准涵盖。它有时也被称为一个”mini float“。
在某些环境下,产生一个浮点数要用一些特殊操作符。它们把二进制的浮点数标示形式转换为一个整数,然后可以被所有支持整数参数的指令使用。__float80m__ 和 __float80e__ 产生 64位尾数和16位指数的的 80 位浮点数,而 __float128l__ 和 __float128h__ 产生一个平分 64 位的 128 位浮点数。(这里其实我也不很明白 128 位的浮点数是怎样组成的)
mov rax,__float64__(3.141592653589793238462)
会把二进制的 pi 解释为一个 64 位的浮点数,保存到 RAX 寄存器中。这行语句实际上等价于:
mov rax,0x400921fb54442d18
NASM 不对浮点常量做编译时的数学逻辑检查。这是因为 NASM 在设计的时候就考虑到可移植问题。虽然它总是产生在 x86 架构 CPU 下的代码,汇编器本身可以在任何支持 ANSI C 的系统上运行。因此,汇编器不保证哪个浮点数单元对 Intel 所定义的数字格式的能力,所以,要使 NASM 支持浮点数的运算,汇编器要有自己的一套完整的浮点数处理方法进行浮点数的计算,这样用很大的工作量来得到很少的利益并不值得。
一些特殊的字段如 __Infinity__、__QNaN__(或者 __NaN__)和 __SNaN__ 可以对应地被用于生成极限数、无效数和 Signalling NaN。 一般在宏中会用到:
%define Inf __Infinity__ %define NaN __QNaN__ dq +1.5, -Inf, NaN ; Double-precision constants
%use fp 标准宏报包含了一系列约定的宏定义。参见 5.3 节。
3.4.7 BCD 码常量
x87 风格的 BCD 码常量可以在 80 位的浮点数环境中使用。它们后缀一个 p 或者 前缀 0p 。可以包含最多 18 个十进制数位。
和其它数值常量一样,下划线也可以在 BCD 码常量中使用。
例如:
dt 12_345_678_901_245_678p dt -12_345_678_901_245_678p dt +0p33 dt 33p
最近在学习汇编,看到 NASM 的教程很少,于是顺便翻译了,也当检验一下自己的学习是否正确。不过有些地方还是没理解好。如果有什么错误希望大家请指出!
Apr 23, 2023 07:56:13 PM
crediblebh