首页 > 代码库 > AT&T汇编学习笔记

AT&T汇编学习笔记

AT&T汇编和intel汇编的区别

(1)在Intel格式中大多使用大写字母,而在AT&T格式中都是用小写字母。

(2)在AT&T格式中,寄存器名要加上“%”作为前缀,而在intel格式中则不带前缀。

(3)在AT&T的386汇编语言中,指令的源操作数与目标操作数的顺序与在intel的386汇编语言中正好相反。在intel格式中是目标在前,源在后;而在AT&T格式中则是源在前,目标在后。例如,将寄存器eax的内容送入ebx,在intel格式中为“MOVE EBX,EAX”,而在AT&T格式中则为”move %eax, %ebx”。

(4)在AT&T格式中,访问指令的操作数大小(宽度)有操作码名称的最后一个字母(也就是操作码的后缀)来决定的。用作操作码后缀的字母有b(表示8位),w(表示16位)和l(表示32位)。而在intel格式中,则是在表示内存单元的操作数前面加上“ BYTE PTR”,“ WORD PTR”,“ DWORD PTR”来表示。例如,将FOO所指内存单元中的字节取入8位的寄存器AL,在两种格式中不用的表示如下:

MOV AL, BYTE PTR FOO   (intel格式)

movb FOO, %al                   (AT&T格式)

(5)在AT&T格式中,直接操作数要加上” $“作为前缀,而在intel格式中则不带前缀。所以,intel格式中”push 4“,在AT&T格式中就变为”pushl $4”

(6)在AT&T格式中,绝对转移或者调用指令jump/call的操作数(也即转移或调用的目标地址),要加上“ *”作为前缀(不要以为是c语言中的指针,哈哈),而在intel格式中则不带。

(7)远程转移指令和子程序调用指令的操作码名称,在AT&T格式中为“ljmp”和“lcall”,而在intel格式中,则为“JMP FAR”和”CALL FAR”。当转移或者调用的目标为直接操作数时,两种不同的表示如下:

CALL FAR SECTION:OFFSET        (intel格式)

JMP FAR SECTION:OFFSET          (intel格式)

lcall $section, $offset                        (AT&T格式)

ljmp $section, $offset                  (AT&T格式)

与之相对应的远程返回指令,则为:

RET FAR STACK_ADJUST              (intel格式)

lret $stack_adjust                       (AT&T格式)

(8)间接寻址的一般格式,两者区别如下

SECTION:[BASE+INDEX*SCALE+DISP]        (intel格式)

section:disp(base, index, scale)                  (AT&T格式)

这种寻址方式常常用于在数据结构数组中访问特定元素内的一个字段,base为数组的起始地址,scale为每个数组元素的大小,index为下标。如果数组元素是数据结构,则disp为具体字段在结构中的偏移。

注意在AT&T格式中隐含了所进行的计算。例如,当SECTION省略,INDEX和SCALE也省略,BASE为EBP,而DISP(偏移)为4时,表示如下:

[ebp-4]        (intel格式)

-4(%ebp)     (AT&T格式)

在AT&T格式的括号中如果只有一项base,就可以省略逗号,否则不能省略,所以(%ebp)想当于(%ebp,,),进一步相当于(%ebp,0,0)。又如,当INDEX为EAX, SCALE为4(32位),DISP为foo,而其他均省略,则表示为:

[foo+EAX*4]        (intel格式)

foo(, %eax, 4)    (AT&T格式)


嵌入C代码中的386汇编语言程序段

当需要在C语言的程序中嵌入一段汇编语言程序段时,可以使用gcc提供的“asm”语句功能。例如:#define __SLOW_DOWN_IO __asm__ __volatile__ ("outb %al,$0x80")

这是一条8位输出指令,如前所述在操作符上加上后缀”b”表示这是8位的,而0x80因为是常数,即所谓的“直接操作数”,所以要加上前缀“ $”,而寄存器名al也加了前缀”%“。

上面那条汇编语句很好理解,在来看一个稍微困难点的例子:

static __inline__ void atomic_add(int i, atomic_t *v)
{
		__asm__ __volatile__(
			LOCK "addl %1,%0"
			:"=m" (v->counter)
			:"ir" (i), "m" (v->counter));
}

一般而言,往C代码中插入汇编语言的代码片段要比”纯粹“的汇编语言代码复制的多,因为这里有个怎样分配使用寄存器,怎样与C代码中的变量结合的问题。为了这个目的,必须对所用的汇编语言作更多的扩充,增加对汇编工具的指导作用。其结果是其语法实际上编程了既不同于汇编语言,也不同于C语言的某种中间语言。

插入C代码中的一个汇编语言片断可以分成四部分,以“:”号加以分隔,其一般形式为:

指令部:输出部:输入部:损坏部

第一部分就是汇编语句本身,其格式与在汇编语言程序中使用的基本相同,但也有区别。这一部分可以称为“指令部”,是必须有的,而其他各部分则可视具体的情况而省略。所以在最简单的情况加就与常规的汇编语句基本相同。

当将汇编语言代码片断嵌入到C代码中时,操作数与C代码中的变量如何结合显然是个问题。因为程序员在编写嵌入的汇编代码时,按照程序逻辑的要求很清楚应该选用什么指令,但是却无法确切地知道gcc在嵌入点的前后会把那一个寄存器分配用于哪一个变量,以及哪一个或哪几个寄存器是空闲着的。而且,光是被动地知道gcc对寄存器的分配情况也还是不够,还得有个手段把使用寄存器的要求告知gcc,反过来影响它对寄存器的分配。当然,如果gcc的功能非常强,那么通过分析嵌入的汇编代码也应该能够归纳出这些要求,再通过优化,最后也能达到目的。但是,即使这样,所引入的不确定性也还是个问题,更何况要做到这样还不容易,针对这个问题,gcc采取了一种折中的办法:程序员只提供具体的指令,而对寄存器的使用则一般只提供一个样板和一些约束条件,而把到底如何与变量结合的问题留给gcc和gas去处理。

在指令部中,数字加上前缀%,如%0、%1等等,表示需要使用寄存器的样板操作数。可以使用此类操作数的总数取决于具体CPU中通用寄存器的数量。这样,指令部中用到了几个不同的这样的操作数,就说明有几个变量需要与寄存器结合,由gcc和gas在编译和汇编时根据后面的约束条件自行变通处理。由于这些样板操作数也使用“%”前缀,在涉及到具体的寄存器时就要在寄存器名前面加上两个“%”符,以免混淆。

那么,怎样表达对变量结合的约束条件呢?这就是其余几个部分的作用。紧接在指令部后面的是“输出部”,用以规定对输出变量,即目标操作数如何结合的约束条件。每个这样的条件称为一个“约束”。必要时输出部中可以有多个约束,相互以逗号分隔。每个输出约束以“=”号开头,然后是一个字母表示对操作数类型的说明,然后是关于变量结合的约束。例如,在上面的例子中输出部为

:”=m”(v->counter)

这里具有一个约束,”=m”表示相应的目标操作数(指令部中的%0)是一个内存单元。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行嵌入的汇编代码后均不保留执行之前的内容,这就给gcc提供了调度使用这些寄存器的依据。

 

输出部后面是“输入部”。输入约束的格式与输出约束相似,但不带“=”号。在前面例子中的输入部有两个约束。第一个为”ir(i)”,表示指令中的%1可以是一个在寄存器中的直接操作数(i表示immediate),并且该操作数来自于C代码中的变量名(这里是调用参数)i。第二个约束为”m”(v->counter),意义与输出约束中相同。如果一个输入约束要求使用寄存器,则在预处理时gcc会为之分配一个寄存器,并自动插入必要的指令将操作数即变量的值装入该寄存器。与输入部中说明的操作数结合的寄存器或操作数本身,在执行嵌入的汇编代码以后也不保留执行之前的内容。例如,这里的1%要求使用寄存器,所以gcc会为其分配一个寄存器,并自动插入一条movl指令把参数i的数值装入该寄存器,可是这个寄存器原来的值就不复存在了。如果这个寄存器本来就是空闲的,那倒无所谓,可是如果所有的寄存器都在使用,而只好暂时借用一个,那就得保证在使用以后恢复其原有的内容。此时gcc会自动在开头处插入一条pushl指令,将该寄存器原来的内容保存在堆栈中,而在结束后插入一条popl指令,恢复寄存器的内容。

 

在有些操作能够中,除用于输入操作数和输出操作数的寄存器以外,还要将若干个寄存器用于计算和操作的中间结果,这样,这些寄存器原来的内容就损坏了,所以要在损坏部对操作的副作用加以说明,让gcc采取相应的措施。不过,有时候就直接把这些说明放在输出部了,那也并无不可。

 

操作数的编号从输出部的第一个约束(序号为0)开始,顺序数下来,每个约束计数一次。在指令部中引用这些操作数或分配用于这些操作数的寄存器时,就用序号前面加上一个“%”号。在指令部中引用一个操作数时总是把它当成一个32位的“长字”,但是对其实施的操作,则根据需要也可以是字节操作或字操作。对操作数进行的字节操作默认为对其低字节的操作,字操作也是一样。不过,在一些特殊的操作中,对操作数进行字节操作时也允许明确指出是对哪一个字节操作,此时在%与序号之间插入一个“b”表示最低字节,插入一个“h”表示次低字节。

表示约束条件的字母主要有:

“m”,”v”,”o”           ——表示内存单元
“r”                        ——表示任何寄存器
“q”                       ——表示寄存器eax,ebx,ecx,edx之一
“i”和”h”                ——表示直接操作数
“E”和”F”              ——表示浮点数
“g”                       ——表示任意
“a”,”b”,”c”,”d”      ——分别表示要求使用寄存器eax,ebx,ecx,edx
“s”,”d”                  ——分别表示要求使用寄存器esi或edi
“I”                        ——表示常数(0-31)

此外,如果一个操作数要求使用与前面某个约束中所要求的是同一个寄存器,那就把那个约束对应的操作数编号放在约束条件中。在损坏部常常会以”memory”为约束条件,表示操作完成后内存中的内容已有改变,如果原来某个寄存器的内容来自内存,则现在可能已经不一致。

 

还要注意,当输出部为空,即没有输出约束时,如果有输入约束存在,则须保留分隔标记“:”号。

 

回到上面的例子,这段代码的作用是将参数i的值加到v->counter上,代码中的关键字LOCK表示在执行addl指令时要把系统总线锁住,不让别的CPU打扰。将两个数相加是很简单的操作,C语言中明明有相应的语言成分,如:“v->counter+=I;”为什么要用汇编呢?原因就在于,这里要求整个操作只由一条指令完成,并且将总线锁住,以保证操作的“原子性”。相比之下,C语句在编译之后到底有几条指令是没有保证的,也无法要求在计算过程中对总线加锁。

再看一段嵌入汇编代码:

//取自include/asm-i386/bitops.h
static inline void set_bit(int nr, volatile void *addr)
{
       asm volatile(
       lock;
"bts %1,%0"
: "=m" (*(volatile long *) addr)
: "Ir" (nr)
: "memory");
}

这里的指令btsl将一个32位操作数中的某一位设置成1,参数nr表示该位的位置。


再来看一个复杂一点的例子:

//取自include/asm-i386/string.h
static __always_inline void * __memcpy(void * to, const void * from, size_t n)
{
int d0, d1, d2;
__asm__ __volatile__(

       "rep ; movsl/n/t"
       "testb $2,%b4/n/t"
       "je 1f/n/t"
       "movsw /n"
       "1:/ttestb $1,%b4/n/t"
       "je 2f/n/t"
"movsb /n"
       "2:"
       : "=&c" (d0), "=&D" (d1), "=&S" (d2)
       : "0" (n/4), "g" (n), "1" ((long) to), "2" ((long) from)
       : "memory");
return (to);
}

__memcpy是内核中对memcpy()的底层实现,用来复制一块内存空间的内容,而忽略其数据结构。这是使用非常频繁的一个函数,所以其运行效率十分重要。

 

先看约束条件和变量与寄存器的结合。输出部有三个约束,对应于操作数%0至%2。其中变量d0为操作数%0,必须放在寄存器ecx中,原因等下就会明白。同样,d1即%1必须放在寄存器edi中;d2即2%必须放在寄存器esi中。再看输入部,这里有四个约束,对应于操作数%3至%6。其中操作数%3与操作数%0使用同一个寄存器,所以也必须是寄存器ecx;并且要求由gcc自动插入必要的指令,实现将其设置成n/4,实际上是将复制长度从字节个数n换算成长字个数n/4。至于n本身,则要求gcc任意分配一个寄存器存放。操作数5%与6%,即参数to与from,分别与%1和%2使用相同的寄存器,所以也必须是寄存器edi和esi。

 

再看指令部,第一条指令是“rep”,表示下一条指令movsl要重复执行,每重复一遍就把寄存器ecx中的内容减1,直到变成0为止。所以,在这段代码中一共执行n/4次。那么movsl又干些什么呢?它从esi所指的地方复制一个长字到edi所指的地方,并使esi和edi分别加4。这样,当代码中的"rep ; movsl/n/t"执行完毕,所有的长字都已复制好,最多只剩下三个字节了,在这个过程中,实际上使用了ecx、edi以及esi三个寄存器。即%0(同时也是%3)、%2(同时也是%6)以及1%(同时也是%5)三个操作数,这些都隐含在指令中,从字面上看不出来。同时,这也说明了为什么这些操作书必须存放在指定的寄存器中。

 

接着就是处理剩下的三个字节了。先通过testb测试操作数%4,即复制长度n的最低字节中的bit1,如果这一位为1就说明至少有两个字节,所以通过movsw复制一个短字(esi和edi则分别加2),否则就把它跳过。在通过testb测试操作数%4的bit0,如果这一位为1就说明还剩下一个字节,所以通过指令movsb再复制一个字节,否则就把它跳过。到达标号2的时候,执行就结束了。