3.2 GUN as汇编(本文内容大部分引用原文,非原创)

  as86汇编仅仅用于编译内核中的boot/bootsect.s引导扇区程序和实模式下的设置程序boot/setup.s。内核中其余所有汇编语言程序(包括C语言产生的汇编程序)均使用gas来编译,并与C语言程序编译产生的模块链接。

3.2.2 GUN汇编语法与INTEL汇编语法的主要区别:

  *AT&T语法(即GUN汇编语法)中立即操作数前面要加一个字符'$';寄存器操作数前面要加字符百分号'%';绝对跳转/调用(相对于与程序计数器有关的跳转/调用)操作数前面要加星号'*'。而intel汇编语法没有这些限制。

  *AT&T语法与intel语法使用的源和目的操作数次序正好相反。例如:AT&T语句 “addl $4, %eax” 是将4和eax寄存器中的值相加,结果保存在eax寄存器中,而在intel中表示为"add eax, 4"。

  *AT&T中内存操作数的长度(宽度)由操作码最后一个字符来确定。'b','w','l'分别表示内存引用宽度为8位字节(byte)、16位字(word)和32位双字(long)。intel汇编通过在操作数前面加前缀"byte ptr","word ptr"和"dword ptr"来达到同样的目的。因此intel语句"mov al, byte ptr foo"对应于AT&T的语句"movb $foo, %al"。

  *AT&T语法中立即形式的远跳转和远调用为"ljmp/lcall $section, $offset",而intel的是"jmp/call far section, offset"。同样,AT&T中远返回指令"lret $stack-adjust"对应于intel的"ret far stack-adjust"。

  *AT&T汇编不提供对多代码段程序的支持,UNIX类操作系统要求所有代码在一个段中。

GUN as汇编程序预处理

  as汇编器内置对汇编语言程序的预处理功能。该预处理功能会调整并删除多余的空格字符和制表符;删除所有注释语句并使用单个的空格或一些换行符替换他们;转换字符常数为对应的数值。但是该功能不会对宏定义进行处理,也没有处理包含文件的功能。如果需要这方面的功能,可以让汇编语言程序使用大写的后缀'S'让as使用gcc的CPP预处理功能。

  由于as汇编语言程序除了使用C语言注释语句(/*和*/)以外,还是用井号"#"作为单行注释的开始字符,因此如果在汇编之前不对程序进行预处理,那么程序中包含的所有以井号"#"开始的指示符或者命令都会被当做注释部分。

符号、语句和常数

  符号(Symbol)是由字符组成的标识符,组成符号的有效字符取自于大小写字符集、数字和三个字符"_.$",不允许以数字开头,而且大小写含义不同。长度没有限制。

  语句(Statment)以换行符或者行分隔符(";")作为结束。文件最后的语句必须以换行符作为结束。若在一行的最后使用反斜杠"\"(在换行符前),就可以让一条语句使用多行。当as读到反斜杠加换行符时,就会忽略掉这两个字符。

  语句由零个或多个标号(Label)开始,后面可以跟随一个确定语句类型的关键符号。标号由符号后面跟随一个冒号(":")构成。关键符号确定了语句余下部分的语义,如果关键符号以一个"."开始,那么当前语句就是一个汇编命令(或者称为伪指令、指示符)。如果关键符号以一个字母开始,那么当前语句就是一条汇编语言指令语句。因此一条语句的通用格式为:

  标号:汇编命令  注释部分(可选)

或者

  标号:指令助记符  操作数1,操作数2    注释部分(可选)

  常数是一个数字,可以分为字符常数和数字常数两类。字符常数还可以分为字符串和单个字符,数字常数可以分为整数、大数和浮点数。

字符串必须用双引号括住,并且用反斜杠"\"来转移包含的特殊字符。单个字符常数可以写成在该字符前加一个单引号,如"'A"表示值65、"'C"表示67。

整数数字有4中表示方法,即使用"0b"或"0B"开始的二进制数;以"0"开始的八进制数;以非0数字开始的十进制数和使用"0x"或"0X"开始的十六进制数。负数直接在前面加"-"。

大数(Bignum)是位数超过32位的二进制的数,其表示方法与整数相同。

汇编对浮点数的表示方法和C语言基本一样。

3.2.3 指令语句、操作数和寻址

  指令(Instructions)是CPU执行的操作,通常指令也称作操作码(Opcode);

  操作数(Operand)是指令操作的对象;

  地址(Address)是指定数据在内存中的位置。

指令语句是程序运行时刻执行的一条语句,它通常包含4个部分:标号、操作码、操作数和注释。

一条指令语句可以含有0个或最多3个用逗号分开的操作数。对于具有两个操作数的指令语句,第一个是源操作数,第二个是目的操作数,指令结果保存在第二个操作数中。

操作数可以是立即数(即值是常数值的表达式),寄存器(值在CPU的寄存器中)或内存(值在内存中)。

AT&T语法通过在操作数前加一个"*"字符来指定一个间接操作数,只有调转/调用指令才能使用间接操作数。

  立即操作数前面要加一个"$"字符前缀。

  寄存器名前面要加一个"%"字符前缀。

  内存操作数由变量名或者含有变量地址的一个寄存器指定。

指令操作码的命名

  AT&T语法中指令操作码的名称(即指令助记符)由最后一个字符来指明操作数的宽度。'b','w','l'分别指定byte,word,long类型的操作数。如果指令名称中没有带这样的字符后缀,并且操作数中不含内存操作数,as编译器就会根据目的寄存器操作数来尝试确定操作数宽度。例如,"mov %ax, %bx"等同于"movw %ax, %bx"。

  AT&T与Intel语法中几乎所有操作码的名称都相同,但有几个例外。符号扩展和零扩展指令都需要2个宽度来指明,即要为源操作数和目的操作数同时指明宽度,通过使用两个操作码后缀来做到。AT&T语法中符号扩展和零扩展的基本操作码名称分别是"movs..."和"movz...",Intel中分别是"movsx"和"movzx"。例如,”使用符号扩展从%al移动到%edx“的AT&T语句是"movsbl %al, %edx",即从byte到long是bl。

  AT&T语法与Intel语法中转换指令的对应关系:

AT&T--------------Intel--------------说明

cbtw        cbw      把%al中的字节值符号扩展到%ax中

cwtl        cwde      把%ax符号扩展到%eax中

cwtd        cwd      把%ax符号扩展到%dx:%ax中

cltd        cdq        把%eax符号扩展到%edx:%eax中

指令操作码前缀

操作码前缀用户修饰随后的操作码。他们用于重复字符串指令提供区覆盖执行总线锁定操作、或指定操作数和地址宽度。通常操作码前缀可以作为一条没有操作数的指令独占一行并且直接位于所影响指令之前,最好与它修饰的指令在同一行上。例如串扫描指令"scas"使用前缀执行重复操作:repne scas %es:(%edi), %al。

  操作码前缀列举:

cs,ds,ss,es,fs,gs  ---  区覆盖操作码前缀,通过制定使用区:内存操作数,内存引用形式会自动添加这种前缀

data16,addr16     ---  操作数/地址宽度前缀。这两个前缀会把32位操作数/地址改变成16位的操作数/地址。但是,as并不支持16位寻址方式。

lock          ---  总线锁存前缀。用于在指令执行期间禁止中断(仅对某些指令有效,参见80x86手册)

wait          ---  协处理器指令前缀。等待协处理器完成当前指令的执行。对于80386/80387组合用不着这个前缀。

rep,repe,repne   ---  串指令操作前缀,使串指令重复执行%ecx中指定的次数。

内存引用

Intel语法的简介内存引用形式:section:[base + index * scale + disp]

AT&T语法形式:section:disp(base, index, scale)

另外,与指令计数器PC无关的间接调用和跳转的操作数必须有一个"*"前缀字符。若没有这个前缀"*",as汇编器就会选择与指令计数器PC先关的跳转/调用标号。其他任何具有内存操作数的指令都必须使用操作码后缀('b','w','l')指明操作数的大小。

3.2.4 区与重定位

  区(Section)也称为段、节或部分,用于表示一个地址范围,操作系统会一相同的方式对待和处理在该地址范围中的数据信息。区的概念主要是用来表示编译器生成的目标文件中不同的信息区域,例如正文区或数据区。

  链接器ld会把输入的目标文件中的内容按照一定的规律组合生成一个可执行程序。当as汇编器输出一个目标文件时,该目标文件中的代码被默认设置成从地址0开始。此后ld会在链接过程中为不同目标文件中的各个部分分配不同的最终地址位置。ld会把程序中的字节块移动到程序运行时的地址处。这些块是做为固定单元进行移动的,他们的长度和字节次序都不会被改变。这样的固定单元就被称为区。而为区分配运行时刻地址的操作就被称为重定向(Relocation)操作,其中包括调整目标文件中记录的地址,从而让他们对应于恰当的运行时刻地址上。

  as汇编器输出的目标文件中至少具有3个区,分别被称为正文(text)、数据(data)和bss区。如果没有使用汇编命令把输出放置在".text"或".data"区中,这些区会仍然存在,但是为空。在一个目标文件中,其text区从地址0开始,随后是data区,再后是bss区。

  当一个区被重定位时,为了让链接器ld知道哪些数据会发生变化以及如何修改这些数据,as汇编器也会往目标文件中写入所需要的重定位信息。为了执行重定位操作,在每次涉及目标文件中的一个地址时,ld必须知道:

  1.目标文件中对一个地址的引用是从什么地方开始算起的?

  2.该引用的字节长度是多少?

  3.该地址引用的是哪个区?(地址) - (区的开始地址)的值等于多少?

  4.对地址的引用与程序计数器相关吗?

  实际上,as使用的所有地址都可以表示为:(区) + (区中偏移)。另外,as计算的大多数表达式都有这种与区相关的特性。在下面的说明中,我们使用记号"{secname N}"来表示区secname中的偏移N。

  除了text,data,bss区,还需要了解绝对地址区(absolute区)。在链接器链接过程中,绝对地址区中的地址始终不变,所以目标文件中的absolute区可能会因为重叠而产生覆盖。

  “未定义的区”(Undefined section)。在汇编是所有不能确定所在区的地址都被设置为{undfined U},其中U将会在以后填上。

  链接器会把所有目标文件中的text区放在相邻的地址处,data和bss区也同样如此。

链接器涉及的区

链接器ld只涉及如下四个区:

1.text区、data区----这两个区用于保存程序。as和ld会分别独立而平等的对待他们。然而当程序运行的时候text区的内容是不会改变的。text区通常会被进程共享,其中含有指令代码和常数等内容,程序运行时data区的内容是变化的,C变量一般就存放在data区。

2.bss区------在程序开始运行时,这个区含有0值字节。该区用于存放未初始化的变量或作为公共变量存储空间。虽然程序每个目标文件bss区的长度信息很重要,但是由于该区中存放的是0值字节,因此无需在目标文件中保存bss区。设置bss的目的就是为了从目标文件中明确的排除0值字节。

3.absolute区------绝对地址区可以称作“不可重定位”区,在重定位操作期间他们不会改变。

4.undefined区-----对于不在先前所述各个区中对象的地址引用都属于本区。

子区

  汇编取得的字节数据通常位于text或data区中。有时候在汇编源程序某个区中可能分布着一些不相邻的数据组,但是你可能会想让他们在汇编后聚集在一起存放。as汇编器允许利用子区(subsection)来达到这个目的。在每个区中,可以有编号为0~8192的子区存在。编址在同一个区中的对象会在目标文件中与其他对象放在一起。例如,编译器可能想把常数存放在text区中,但是不想让这些常数散布在被汇编的整个程序中。在这种情况下,编译器就可以在每个会输出的代码区之前用“.text 0”子区,在每组会输出的常数之前使用“.text 1”子区。

  使用子区是可选的,如果没有使用子区,那么所有对象都会存放在子区0中。子区会以按编号由小到大的顺序出现在目标程序中,但是目标文件不包含表示子区的任何信息。处理目标文件的链接程序ld并不会看到子区的踪迹,他们只会看到由所有的text子区组成的text区;有所有data区组成的data区。

  每个区都有一个位置计数器(Location Counter),它会对没个汇编进该区的字节进行计数,由于子区仅供as汇编器使用方便而设置,因此并不存在子区计数器。虽然没有什么直接操作一个位置计数器的方法,但是汇编命令".align"可以改变其值,并且任何标号定义都会取用位置计数器的当前值。正在执行语句汇编处理的区的位置计数器被称为当前活动计数器。

bss区

bss区用于存储局部公共变量。可以在bss区中分配空间,但是在程序运行之前不能在其中放置数据。因为在程序刚开始执行时,bss区中的所有字节内容都将被清零。".lcomm"汇编命令用于在bss区中定义一个符号;".comm"可用于在bss区中声明一个公共符号。

3.2.5 符号

  在程序编译和链接的过程中,符号(Symbol)是一个比较重要的概念。程序员使用符号来命名对象,链接器使用符号进行链接操作,而调试器利用符号进行调试。

标号(Label)是后面紧随一个冒号的符号。此时符号代表活动位置计数器的当前值,并且可以做为指令的操作数使用。可以使用等号"="给一个符号赋予任意数值。

符号名以一个字母或"._"字符之一开始。局部符号用于协助编译器和程序员临时使用名称。在一个程序中共有10个局部符号名("0"..."9")可供重复使用。为了定义一个局部符号,需要写出形如"N:"的标号(N代表任意数字)。若是引用前面最近定义的这个符号,需要写成"Nb";若是引用下一个定义局部符号,则需要写成"Nf"(b-backwards,f-forwards)。局部标号在使用方面没有限制,但是在任何时候只能向前/向后引用最远10个局部标号。

特殊点符号

特殊点符号"."表示as汇编的当前地址。因此表达式"mylab: .long ."就会把mylab定义为包含它自己所处的地址值。给"."赋值就如同汇编命令".org"的作用。因此表达式". = . + 4"与".space 4"完全相同。

 符号属性

除了名字以外,每个符号都有“值”和“类型”属性。根据输出的格式不同,符号也可以具有辅助属性。如果不定义就使用一个符号,as会假设其所有属性均为0。这就表示该符号是一个外部定义的符号。

符号的值通常是32位的。对于标出text、data、bss或absolute区中的一个位置符号,其值是从区开始处到标号的地址值。对于text、data和bss区,一个符号的值通常会在链接过程中由于ld改变区的基地址而变化,absolute区中的符号的值不会改变。

ld会对未定义的符号的值进行特殊处理。如果未定义的符号的值为0,则表示该符号在汇编源程序中没有定义,ld会尝试根据其他链接的文件来确定它的值。在程序中使用了一个符号但没有对符号进行定义,就会产生这样的符号。如果未定义的符号的值不为0,那么该符号就表示是.comm公共声明的需要保留的公共存储空间字节长度。符号指向该存储空间的第一个地址处。

符号的类型属性含有用于链接器和调试器的重要定位信息、指示符号是外部的标志以及一些其他可选信息。

3.2.6 as汇编命令

汇编命令是指示汇编器操作方式的伪指令。汇编命令用于要求汇编器为变量分配空间、确定程序开始地址、指定当前汇编的区、修改位置计数器的值等。所有汇编命令的名称都以"."开始,其余是字符,并且大小写无关。但是通常都使用小写字符。

1. .align abs-expr1,abs-expr2,abs-expr3

  .align是存储器对齐汇编命令,用于在当前子区中把位置计数器的值设置(增加)到下一个指定的存储边界处。第一个绝对值表达式abs-expr1指定要求的边界对齐值。对于使用a.out格式目标文件的80x86系统,该表达式的值是位置计数器值增加后其二进制值最右边0值位的个数,即是2的次方值。例如,".align 3"就表示把位置计数器的值增加到8的倍数上。如果位置计数器的值本身就是8的倍数,则无需改变。但是对于使用ELF格式的80x86系统,该表达式的值直接就是要求对齐的字节数。例如,".align 8"就是把位置计数器增加到8的倍数上。

  第二个表达式给出用于对齐而填充的字节值。该表达式可以省略,若省略,填充的字节值是0。第三个可选表达式abs-expr3用于指示对齐操作允许跳过的最大字节数。如果对齐操作要求跳过的字节数大于这个最大值,那么该对齐操作就被取消。

2. .ascii "string"...

  该命令从位置计数器所指当前位置为字符串分配空间并存储字符串。可使用逗号分开写出多个字符串。例如,.ascii "Hellow world!","My assembler"。该汇编命令会把这些字符串汇编在连续的地址位置处,每个字符串后面不会自动添加0(NULL)字节。

3. .asciz "string"...

  该汇编命令与“.ascii”类似,但是每个字符串后面会自动添加NULL字符。

4. .byte expressions

  该汇编命令定义0个或多个用逗号分开的字节值。每个表达式的值是一个字节。

5. .comm symbol, length

  该命令在bss区中声明一个命名的公共区域。在ld链接过程中,某个目标文件中的一个公共符号会与其他目标文件中同名的公共符号合并。如果ld没有找到一个符号的定义,而只是一个或多个公共符号,那么ld就会分配指定长度length字节的未初始化内存。length必须是一个绝对值表达式,如果ld找到多个长度不同但同名的公共符号,ld就会分配长度最大的空间。

6. .data subsection

  该汇编命令通知as把随后的语句汇编到编号为subsection的data子区中。如果省略编号,则默认使用编号0。编号必须是绝对值表达式。

7. .desc symbol, abs-expr

  该命令用表达式的值设置符号symbol的描述符字段n-desc的16位值。它仅用于a.out格式的目标文件。参见有关include/a.out.h文件的说明。

8. .fill repeat, size, value

  该汇编命令会产生数个(repeat个)大小为size字节的重复拷贝。大小值size可以为0或某个值,但是若size大于8,则限定为8。每个重复字节内容取自一个8字节数。高4字节为0,低4字节是数值value。这3个参数值都是绝对值,size和value是可选的。如果第2个逗号和value省略,value默认为0值;如果后两个参数都省略的话,则size默认为1.

9. .global symbol(或者 .globl symbol)

  该汇编命令会使得链接器ld能看见符号symbol。如果我们在目标文件中定义了符号symbol,那么它的值将能被链接过程中的其他目标文件使用。若目标文件中没有定义该符号,那么它的属性将从链接过程中的其他目标文件的同名符号中获得。这是通过设置符号symbol类型字段中的外部位N_EXT来做到的。

10. .int expressions

  该命令在某个区中设置0个或多个整数值(80386系统为4个字节,同.long)。每个用逗号分开的表达式的值就是运行时刻的值,例如 .int 1234,567,0x89AB。

11. .lcomm symbol, length

  该命令为符号symbol指定的局部公共区域保留长度为length字节的空间。所在的区和符号symbol的值是新的局部公共块的值。分配的地址在bss区中,因此在运行时刻这些字节值被清零。由于符号symbol没有被声明为全局的,因此链接器ld看不见。

12. .long expressions

  该命令含义与 .int 相同。

13. .octa bignums

  这个汇编命令指定0个或多个用逗号分开的16字节大数(.byte, .word, long, .quad, .octa 分别对应1、2、3、8、16字节数)。

14. .org new_lc, fill

  这个汇编命令会把当前区的位置计数器设置为值new_lc。new_lc是一个绝对值(表达式),或者是具有相同区作为子区的表达式,也就是说不能使用.org跨越各区。如果new_lc的区不对,那么.org就不会起作用。注意:位置计数器都是基于区的,即以每个区作为计数起点。当位置计数器的值增长时,所跳跃的字节将被填入值fill。该值必须是绝对值。如果省略了逗号和fill,则fill默认为0值。

15. .quad bignums

  这个汇编命令指定0个或多个用逗号分开的8字节大数bignum。如果大数放不进8个字节中,则取低8字节。

16. .short expressions(同 .word expressions)

  这个汇编命令指定某个区中0个或多个用逗号分开的2字节数。对于每个表达式,在运行时刻都会产生一个16位的值。

17. .space size,fill

  该命令产生size个字节,每个字节都填值fill。这个参数均为绝对值。如果省略了逗号和fill,那么fill的默认值就是0。

18. .string "string"

  该命令定义一个或多个用逗号分开的字符串。在字符串中科院使用转义字符。每个字符串都自动附加一个NULL字符结尾。例如 .string "\n\nStaring", "other strings"。

19. .text subsection

  该命令通知as把随后的语句汇编进编号为subsection的子区中。如果省略了subsection,则默认使用编号值0。

20. .word expressions

  对于32位机器,这个汇编命令与.short相同。

3.2.7 编写16位代码

  虽然as通常用来编写纯32位的80x86代码,但是1995年后它对编写运行于实模式或16位保护模式的代码也提供有限的支持。为了让as汇编是产生16位代码,需要在运行于16位模式的指令语句之前添加汇编命令".code16",并且使用汇编命令".code32"让as汇编器切换回32位代码汇编方式。

  as不区分16位和32位汇编语句,在16位和32位模式下每条指令的功能完全一样而与模式无关。as总是为汇编语句产生32位的代码而不管指令将运行在16位还是32位模式下。如果使用汇编命令".code16"让as处于16位模式下,那么as会自动为所有指令加上一个必要的操作数宽度前缀而让指令运行在16位模式。请注意:由于as为所有指令添加了额外的地址和操作数宽度前缀,所以汇编代码长度和性能上将会受到影响。

  由于1991年开发Linux内核0.11时as汇编器还不支持16位代码,因此在编写和汇编0.11内核实模式下的引导启动代码和初始化汇编程序时使用as86汇编器。

posted @ 2014-01-14 11:20  萧瑟秋风_cyz  阅读(1555)  评论(0编辑  收藏  举报