FASM 第一章 – 简介

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

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

    1.1 编译器概述
    1.1.1 系统需求
    1.1.2 编译器使用
    1.1.3 编译器选项
    1.1.4 在命令行下执行编译器
    1.1.5 命令行编译器消息
    1.1.6 输出格式
    1.2 汇编语法
    1.2.1 指令语法
    1.2.2 数据定义
    1.2.3 常数和标号
    1.2.4 数值表达式
    1.2.5 跳转和调用
    1.2.6 操作数尺寸设置

    这一章包含开始使用FASM前的所有必须知识,在使用FASM前应至少阅读此章。

    1.1 编译器概述

    FASM是一个x86体系处理器下的汇编语言编译器,它可以通过多遍扫描来优化生成的机器码。

    这篇文档还描述了用于windows系统的IDE版本,这个版本带有界面,并且有一个集成的编辑器。但从编译的角度,它和命令行版本是一样的。IDE版本的可执行文件为fasmw.exe,命令行的为fasm.exe

    1.1.1 系统需求

    所有版本都需要x86平台32位处理器(至少80386),虽然可以生成x86体系处理器16位程序。Windows控制台版本需要任意Win32操作系统;GUI版本需要Win32 GUI 4.0或更高版本,所以它可以运行在任何兼容Windows 95的系统上。

    这个版本提供的example代码需要设置INCLUDE变量为FASM包目录下的include目录才能正确编译。 比如FASM包位置为d:fasm:右键点击【我的电脑】->【属性】->【高级】->【环境变量】,在弹出的对话框中,在下面的系统变量中,如果里面存在INCLUDE环境变量,那么双击其并将d:fasminclude添加到变量值中,注意必须用分号分隔变量;如果不存在INCLUDE环境变量,点击添加INCLUDE环境变量。

    如果你使用FASMW来编译,还有另一个方法,你可以在d:fasmfasmw.ini文件末尾添加下面的内容:

    [Environment]
    Include = c:fasmwinclude
    

    如果不设置好INCLUDE环境变量,当include文件的时候,就必须提供完整的include文件路径。

    1.1.2 编译器使用

    开始使用FASM,可以简单的双击fasmw.exe文件图标,或者拖拽一个源码文件到此图标。你也可以打开fasmw.exe后使用菜单【文件】->【打开】来打开源码文件,或者拖拽文件到编辑窗口。你可以一次编辑多个文件,每一个文件都在编辑窗口底部占用一tab按钮,点击相应的按钮就可以切换到该文件。FASM默认将编译当前编辑的文件,但你可以通过右键点击该文件的tab按钮,让编译器来强制每次编译此文件。一次只能有一个文件可以指派给编译器。

    当你的源码文件都准备好后,你可以执行运行菜单中的编译来执行此文件。编译成功后,编译器将显示编译过程总结;否则将显示发现的错误。编译总结包括编译了多少遍、消耗的时间、写入多少字节到目标文件。它还包含一个【显示】文本框,用来显示任何源码中的display指令。错误总结至少包含错误信息和一个显示文本框。如果错误和源码中的某些行有关,总结将包含指令段,用来显示预处理后导致错误的指令,和源码列表,显示和错误相关的源码行位置,如果你从列表中选择一行,那么编辑窗口也将选择相应的行。(如果此行的文件还没有加载,那么将自动加载。)

    运行命令也调用执行编译器,并且在编译成功后如果此格式能在Windows环境下执行的话执行编译的程序;否则将弹出消息提示此类型文件不能执行。如果发生错误,编译器显示和编译命令相同的提示。

    如果编译器运行超出内存,你可以在【选项】菜单中的【编译器设置】对话框中增加内存分配。你可以设置编译器应当使用多少KB字节,以及编译线程的优先级。

    1.1.3 编辑器选项

    在【选项】菜单中还包含一些编辑器选项,用来影响编辑器行为的开关。这一节中将描述此选项。

    安全选择 – 当打开此选项的时候,当开始键入的时候,选择的文本将不会被删除。当你做任何文本修改操作时,选择部分将被撤销,不会影响任何选中的文本,并且之后会执行那个命令。当这个选项关闭的时候,当你键入的时候,选中的文本将被删除,Del键也会删除选中的块(当安全选中开启时,你必须使用Ctrl+Del才能删除此文件)。

    自动填充 – 当你键入任何开始括号的时候,编辑器将自动键入关闭括号。

    自动缩进 – 当你键入回车开始新行时,光标停在和上一行第一个非空格所在的位置。当你分割行时,新的行也会开始在相同的缩进位置,任何新行后面的空白字符将被忽略掉。

    智能制表键 – 当你按下Tab键的时候,将移动到上一行非空白字符开始处的下一个tab位置。如果在上一行没有找到相应的位置,将缩进8个字符。

    保存优化 – 如果允许此选项,当保持文件的时候,空白区域将被优化的tab和空格填充来减少文件的大小。如果关闭此选项,空白区域将填充为空格(不保存最后一行的空格)。

    Revive dead keys – left to do.

    1.1.4 在命令行下执行编译器

    在命令行下执行编译需要运行fasm.exe。fasm接受两个参数 – 第一个提供源码文件,第二个提供目标文件。如果没有给定第二个文件,输出文件名称将自动猜测一个。当显示简短的程序名称和版本后,编译器从源码文件中读取数据并且编译它。当编译成功,编译器将写入生成的文件到目标文件,并且显示编译过程总结;否则将显示发生的错误信息

    源码文件必须是文本格式的,行结束符接受DOS(CR+LF)和Unix(LF)两种格式,tab将被当做空格处理。

    在命令行中你可以指定-m选项用来指定fasm汇编器最大使用的内存(KB)。在DOS版本中,这个选项仅用来限定扩展内存的使用。-p选项后面用来指定汇编器要执行的遍数。如果代码不能再指定的遍后生成,汇编器将结束并给出错误信息。最大值为65536,默认值为100。这个参数可以用来限制汇编器最多执行的遍数,-p参数跟随一指定的最大遍数即可。

    没有命令行参数来影响输出,flat汇编器仅需要源码文件来包含真正需要的信息。例如,为了制定输出格式你可以在源码文件开头使用format指令。

    1.1.5 命令行编译器消息

    如上面描述的那样,当成功编译后,编译器将显示编译总结。它包含执行了多少遍,消耗的时间,以及写入了多少字节到目标文件。下面是一个编译总结例子:

    flat assembler version 1.66
    38 passes, 5.3 seconds, 77824 bytes.
    

    当编译错误时,程序将显示错误信息。比如,当编译器找不到收入文件时,将显示下面的信息:

    flat assembler version 1.66
    error: source file not found.
    

    如果错误和部分源码相关,导致错误的源码行将被显示。相应行的位置也会给出,以帮助你快速定位错误,比如:

    flat assembler version 1.66
    example.asm [3]:
    mob ax,1
    error: illegal instruction.
    

    意思是example.asm的第三行编译时遇到了无法识别指令。如果导致错误行包含一个宏指令,生成错误指令的宏指令定义也将显示。比如:

    flat assembler version 1.66
    example.asm [6]:
    stoschar 7
    example.asm [3] stoschar [1]:
    mob al,char
    error: illegal instruction.
    

    它的意思是example.asm的第六行宏指令生成了一个无法识别的指令,以及宏指令的第一行定义。

    1.1.6 输出格式

    当源码中没有format指令时,flat简单的把生成的代码到输出文件中,创建flat二进制文件。默认生成的是16位代码,你可以通过use16或use32指令打开16位或者32位模式。一些选择一些输出格式时将切换到32位模式 – 更多你可以选择的格式可以参考 2.4节。

    输出文件的扩展名编译器将根据输出格式自动选择

    所有输出代码顺序将和源码文件的顺序一样。

    1.2 汇编语法

    下面的信息主要是给使用过其他汇编器的汇编程序员看的。如果你是初学者,你应当寻找汇编编程的教程。

    Flat汇编器默认采用Intel的语法,虽然你可以使用预处理(宏指令和符号常量)来定制。它也包含一套伪指令 – 编译器的指令。

    源码中定义的所有符号都区分大小写的。

    操作符字节
    byte81
    word162
    dword324
    fword486
    pword486
    qword648
    tbyte8010
    tword8010
    dqword12816
    表1.1:size操作符

    类型 位数
    通用寄存器 8 al cl dl bl ah ch dh bh
    16 ax cx dx bx sp bp si di
    32 eax ecx edx ebx esp ebp esi edi
    段寄存器 16 es cs ss ds fs gs
    控制寄存器 32 cr0 cr2 cr3 cr4
    调试寄存器 32 dr0 dr1 dr2 dr3 dr6 dr7
    FPU 80 st0 st1 st2 st3 st4 st5 st6 st7
    MMX 64 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7
    SSE 128 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7
    表1.2:寄存器

    1.2.1 指令语法

    指令在汇编语言中是用行结束来分割的,一条指令占用一行文本。如果一行包含分号,除了双引号字符串中的分号外,这行剩余部分为一注释,编译器将忽略掉。如果一行为“”字符(后面可能出现分号和注释),下一行将被连在“”所在位置。

    源码中每一行都是一些元素序列,其中可能为3中方式的一种。一种是符号字符,用来分割元素即使它们没有空格分开。任意的 +-*/=<>()[]{}:,|&~#‘ 为符号字符。其它字符,用空格或者符号字符串分割为符号。如果符号的第一个字符为单引号或双引号,其后的任意字符序列,甚至特殊字符,将被当做引用字符串。不为符号字符和引用字符串,可以用作名称,也叫做名称符号。

    每一个指令包含助记符和一些用逗号分隔的操作数。操作数可以为寄存器、立即数或者内存中的数据,在操作数之前可以跟着size操作符,用来定义或重写大小(表1.1)。表1.2列出了可用的寄存器的名称,他们的大小是不能覆盖的。立即数可以指定为任意数值表达式。

    当操作数为内存中数据是,数据的地址(也可以为任意数值表达式,但必须包含寄存器)必须用中括号括起来或者之前包含ptr运算符。例如:
    指令 mov eax, 3 将把立即数3送到eax寄存器;
    指令 mov eax, [7] 将把32位数据从地址7送到eax;
    指令 mov byte [7], 3 将把立即数3赋值给地址7,也可以写成:mov byte ptr 7, 3。
    为了指定寻址所用的段寄存器,段寄存器紧跟冒号,放在地址值前面(在中括号中或ptr运算符后面)。

    1.2.2 数据定义

    定义数据或保留空间,可以使用表1.3中的伪指令。数据定义伪指令必须跟着一个或多个逗号分隔的数值表达式。这些表达式定义的数据单元大小取决于使用的伪指令。例如:db 1,2,3将分别定义3个字节数据1, 2, 3。

    db和du指示符还接受任意长度的字符串。当使用db时,将被转换为字节序列;使用du的时候,将被转换为高字节为0的字序列。例如db ‘abc’将定义三个字节数据61,62和63。

    dp指示符和其等价的df接受两个用冒号分隔的数字表达式为参数,第一个为高字,第二个将变成far指针值的低DWORD。dd也允许两个用冒号分隔的word指针,dt只允许接受一个浮点参数并以扩展双精度浮点格式创建数据。

    上面的任意伪指令都允许使用dup操作符来重复拷贝给定的值。重复次数必须在该操作符前面,后面为要重复的值 – 也可以为一串用逗号分隔的值,如果这样的话必须用括号将这些值括起来,如db 5 dup(1,2)定义了五份给定两个字节序列的拷贝。

    file是特殊的伪指令,其语法也是不同的。这个伪指令包含来自文件的字节流,它后面必须跟着文件名,然后是可选的文件偏移数值表达式(前面有一冒号),然后也是可选的逗号和要包含多少字节的数值表达式(如果没有指定的话将包含文件中的所有数据)。例如:
    file ‘data.bin’将包含这个文件为二进制数据。
    应该是file ‘data.bin’:10h,4将只包含从10h文件偏移开始后的4个字节。

    大小(字节)定义数据保留数据
    1db
    file
    rb
    2dw
    du
    rw
    4ddrd
    6dp
    df
    rp
    rf
    8dp
    df
    rp
    rf
    10dtrt
    表1.3 数据伪指令

    数据保留伪指令值允许跟着一个数值表达式,这个值定义了多少个指定大小单元空间将被保留。所有的数据定义伪指令都允许“?”值,意思是这个单元不应初始化为任何值,效果和数据保留伪指令相同。未初始化的数据可能没有存在于输出文件中,所以其值应当总是被认为是不可知的。

    1.2.3 常数和标号

    在数值表达式中你可以使用常数或者标号来替代数字。常数或标号定义应当使用特殊的伪指令。每一个标号只允许定义一次,它可以在源码中的任何地方使用(即使在定义前)。常数可以定义多次,此时它只能在定义后才能使用,而且其值总是等于使用位置前最后一次定义的值。当常数在源码中只定义了一次,那么和标号相同可以在源码中的任何位置使用。

    常数定义包含常数名后面跟着“=”字符以及数值表达式,这个在常数定义时计算数据表达式的值将成为常数的值。例如你可以使用伪指令“count=17”定义count常数,然后再汇编指令中使用它,比如mov cx, count – 编译时将变成mov cx, 17。

    有不同的几种方式来定义标号。最简单的是在标号名后面跟着冒号,这条伪指令同行的后面甚至可以跟着其他指令。它定义的标号的值为定义位置的偏移。这种方式通常用来定义代码中的标号。另一种方式是标号名(没有冒号)后面跟着一些数据伪指令。它定义的标号的值为定义数据起始位置的偏移,并作为一个标号记住这个数据,其单元大小由表1.3中的数据伪指令指定。

    标号可以当作标记代码或数据位置偏移的常数值。例如当你使用标号伪指令“char db 224”定义了数据,为了将这个数据的偏移放到bx寄存器,你应当使用“mov bx, char”指令,为了将char处的字节数据移动到dl寄存器,你应当使用“mov dl,[char]”(或者“mov dl, ptr char”)。当当你试图汇编“mov ax, [char]”,将会产生错误,因为FASM会比较操作数的尺寸,以确保它们是相等的。你可以通过size覆盖来强制汇编那条指令:“mov ax, word [char]”,但记住这条指令将在char位置读取两个字节,而实际上char只定义了一个字节。

    最后也是最灵活的定义标号的方式是使用label伪指令。这条伪指令后面为标号名,然后是可选的size操作符,后面是可选的at操作符,以及标号定义地址的数值表达式。例如:“label wchar word at char”将为char地址的16位数据定义一个新的标号。现在“mov ax, [wchar]”将和“mov ax, word[char]”编译后的结果相同。如果没有指定任何地址,label伪指令就在当前位置定义标号。因此“mov [wchar], 57568”将拷贝两个字节,而“mov [char], 224”将拷贝一个字节到同一的地址。

    以”.”开头的标号被认为是局部标号,它附加在最后一个全局标号的后面(名称不以点开头的)来组成完整的标号名。所以你可以在定义另一个全局标号前使用这个标号的短名称(以点开头的),在其他位置你就必须使用完整的标号名。以两个点开头的“..”标号是个特例 – 它们如同全局变量,但它们不会成为局部标号的前缀。

    @@为匿名标号,你可以在源码中多次定义它们。符号@b(或者等价于@r)引用最近的前面的匿名标号,符号@f引用最近的后面的匿名标号。这些特殊标号都不区分大小写。

    1.2.4 数值表达式

    在上面的例子中所有的数值表达式的都是简单的数字、常数或标号。通过编译期间计算的算术或者逻辑操作符也可以变得更复杂些。所有这些操作符和他们的优先级都列在表1.4中。高优先级运算操作先计算,当然你可以通过将某部分表达式用括号括起来来改变它的优先级。+、-、*和/是标准的算术运算操作,mod计算除操作后的余数。and、or、xor、shl、shr和not执行和汇编指令中同名指令相同的逻辑操作。rva用来转换一个地址到重定位的偏移,特定于某些输出格式。(见2.4)

    优先级操作符
    0+
    1*
    /
    2mod
    3and
    or
    xor
    4shl
    shr
    5not
    6rva
    表1.4:算术和逻辑操作符优先级

    表达式中的数字默认为十进制的,二进制数字可以在后面跟着字母b,八进制的跟着字母o,十六进制的以0x字母开头(如果C语言)或者以$开头(如果Pascal语言)或者以h字母结尾。当在表达式中遇到字符串时将被转换为数字 – 第一个字符将成为数字的最低位。

    数值表达式用作地址可以用任意通用寄存器来寻址。它们可以加上或者乘以某个合适的值。

    数值表达式中也可以使用一些特殊符号。第一是“$”,其值等于当前偏移值,而“$$”和当前地址空间的基地址相等。还有“%”,表示在某部分代码中使用特殊伪指令(见2.2)时当前重复次数。还有%t符号,等同于当前的时间戳。

    任何数值表达式都可以包含科学计算法表示的单浮点数值(FASM不允许编译期间的浮点运算),它们可以以字母f结尾,否则它们必须至少包含字符”.”或”E”。所以”1.0″,”1E0″和”1f”定义了相同的浮点数据,而简单的”1″定义了一个整型值。

    1.2.5 跳转和调用

    任何的jmp和call指令操作数前面不仅放size操作符,也可以放跳转类型操作符:short,near或far。例如当汇编器在16位模式下,指令jmp dword [0]将成为远跳转,而当在32位模式下,它将成为near跳转。为了强制这条指令来区分对待,可以使用jmp near dword [0]或者jmp far dword[0]格式。

    当near跳转的操作数为立即数时,如果可能的话,汇编器将生成最短格式的跳转指令(但不要在16位模式下创建32位指令,也不要在32位模式下创建16位代码,除非它前面有size操作符)。通过指定跳转类型你可以强制生成长格式(例如“jmp near 0”)或者生成短格式并且如果不可能的话将产生错误(例如“jmp short 0”)。

    1.2.6 操作数尺寸设置

    当指令使用内存寻址时,如果寻址值在某个范围内,默认将使用短偏移来生成最短格式的指令。这可以通过在中括号中地址前面的word或dword操作符(或者ptr操作符后面)来重写,以强制使用长偏移。当地址不基于任何寄存器时,这些操作符将选择绝对寻址的合适模式。

    指令adc、add、cmp、or、sbb、sub和xor第一个操作数为16位或者32位默认生成段的8位格式,如果第二个操作数为带符号字节范围内的立即数,将产生短格式的指令。可以通过在立即数前面放置word或dword操作符来重写。imul指令简单的规则是最后一个操作数为立即数。

    push指令后面如果为立即数且没有size操作前缀的话,在16位模式下将当作一word值,在32位模式下将作为一dword值,如果可能将使用这条指令的短的8位格式,word或者dword size操作符强制push指令生成指定大小的长格式。pushw和pushd助记符强制汇编器生成16位或32位代码,而不是强制它使用长格式指令。

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