Linux - AT&T汇编基础

在linux内核的源代码中,以汇编语言编写的程序或程序段,有两种不一样的形式。
第一种事彻底的汇编代码,这样的代码采用.s做为文件的后缀。事实上,尽管是彻底的汇编代码,现代的汇编工具也吸取了C语言的长处,也在汇编以前加上了一趟预处理,而预处理以前的文件则以.s为后缀。此类(.s)文件也和C程序同样,可使用#include、#ifdef等等成分,而数据结构也同样能够在.h的文件中加以定义。
linux

第二种是嵌在C程序中的汇编语言片段。虽然在ANSI的C语言标准中并无关于汇编片断的规定,事实上各类实际使用的C编译中都做了这方面的扩充,而 GNU的C编译gcc也在这方面做了很强的扩充。程序员

在DOS/windows领域中,386汇编语言都采用Intel定义的语句格式。但是,在Unix领域中,采用的倒是由AT&T定义的格式。windows

AT&T的汇编与Intel的汇编主要有如下的区别:
在Intel格式中大多使用大写字母,而在AT&T格式中都使用小写字母。
在AT&T格式中,寄存器名要加上“%”做为前缀 ,而在Intel格式中不带前缀。
在AT&T的386汇编语言中,指令的源操做数的顺序与在Intel的386汇编语言中正好相反。
在AT&T格式中,访问指令的操做数的宽度有操做码名称的最后一个字母(操做码的后缀决定)。用做操做码后缀的字母有b(8位)。 w(16位)和1(32位)。 而在Intel格式中,则是在表示内存单元的操做数前面加上“BYTE PTR”“WORD PTR”,“DWORD PTR”来表示。
在AT$T格式中,直接操做数要加上“$”做为前缀 ,而在Intel格式中则不带前缀。
在AT$T格式中,绝对转移和调用指令jump/call的操做数要加上“*”做为前缀 ,而在intel格式则不带。
远程的转移指令和子程序调用指令的操做码名称,在AT$T格式中为“ljump”和“lcall”, 而在intel格式中,则为“JMP FAR”和“CALL FAR”当转移和调用的目标为直接操做数时,两种不一样的表示以下:
 CALL FAR SECTION:OFFSET(Intel 格式)
 JMP FAR SECTION:OFFSET(Intel 格式)
 lcall $section,$offset (AT$T格式)
 lcall $secton,$offset (AT$T格式). 与之相应的远程返回指令,则为:
 RET FAR STACK_ADJUST (Intel 格式)
 Lret $stack_adjust (AT$T 格式)
间接寻址的通常格式,二者的区别以下:
 SECTION :[BASE+INDEX*SCALE+DISP](Intel 格式)
 Section: disp(base,index,scale)(AT$T 格式)
 当须要在C语言的程序中嵌入一段汇编语言程序时,可使用gcc提供的“asm”语句功能。
  通常而言,往C代码中插入汇编语言的代码片要比“纯粹”的汇编语言代码复杂的多,由于这里有个怎样分配使用寄存器,怎样与C代码中的变量结合的问题。为了这个目的,必须对所用的汇编语言做更多的扩充,增长对汇编工具的指导做用。其结果是其语法实际上变成了既不一样于汇编语言,也不一样于C语言的某种中间语言。
数据结构

插入C代码的一个汇编语言代码片断能够分为四个部分,以“:”号加以分隔,其通常形式为:指令部: 输出部:输入部:损坏部工具

 1:第一部分就是汇编代码自己,其格式和在汇编语言中使用基本相同。这一部分称为“指令部”,是必须有的,而其余部分可视具体状况而省略 。当将汇编语言代码片断嵌入到C代码中时,操做数与C代码中的变量如何结合显然是个问题。Gcc采用的策略是:程序员提供具体的指令,而对寄存器的使用则通常只提供一个“样板”和一些约束条件,而把到底如何与变量结合的问题留给了gcc和gas处理.优化

 

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


3:紧接在指令部后面的是“输出部”,用以规定对输出变量如何结合的约束条件。每一个这样的条件称为一个“约束”。必要是输出部能够有多个约束,互相以逗号分隔。每一个输出约束以“=”号开始 ,而后是一个字母表示对操做数类型的说明,而后是关于变量结合的约束 。凡是与输出部中说明的操做数相结合的寄存器或操做数自己,在执行嵌入的汇编代码后均不保留执行以前的内容,这就给gcc提供了调度这些寄存器的依据。.net

4:输出部后面是“输入部”。输入约束的格式和输出约束类似,但不带“=”号 。若是一个输入约束要求使用寄存器,则在预处理时gcc会为之分配一个寄存器,并自动插入必要的指将操做数即变量的值装入该寄存器。与输入部中说明的操做数结合的寄存器或操做数自己,在执行嵌入汇编代码后也不保留执行以前的内容。指针

  在有些操做中,除用于输入数据操做和输出数的寄存器之外,还要将若干个寄存器用于计算或操做的中间结果。这样,这些寄存器原有的内容就损坏了,因此要在损坏部队操做的反作用加以说明,让gcc采起相应的措施。blog

  操做数的编号从输出部的第一个约束(序号为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”为约束条件,表示操做完成后内存中的内容已有改变,若是原来某个寄存器(也许在本次操做汇总并未使用到)的内容来自内存,则如今科能已经不一致。

  还要注意,当输出部为空,即没有输出约束时,若是有输入约束存在,则必须保留分隔标记“:”号。
备注:(举例)
一 基本语法
语法上主要有如下几个不一样.
★ 寄存器命名原则
AT&T: %eax Intel: eax

★源/目的操做数顺序
AT&T: movl %eax,%ebx Intel: mov ebx,eax

★常数/当即数的格式
AT&T: movl $_value,%ebx Intel: mov eax,_value
把_value的地址放入eax寄存器
AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d

★ 操做数长度标识
AT&T: movw %ax,%bx Intel: mov bx,ax

★寻址方式
AT&T: immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + imm32)

linux工做于保护模式下,用的是32位线性地址,因此在计算地址时不用考虑segment:offset的问题.上式中的地址应为:
imm32 + basepointer + indexpointer*indexscale

下面是一些例子:
★直接寻址
AT&T: _booga ; _booga是一个全局的C变量
注意加上$是表示地址引用,不加是表示值引用.
注:对于局部变量,能够经过堆栈指针引用.
Intel: [_booga]

★寄存器间接寻址
AT&T: (%eax)
Intel: [eax]

★变址寻址
AT&T: _variable(%eax)
Intel: [eax + _variable]

AT&T: _array(,%eax,4)
Intel: [eax*4 + _array]

AT&T: _array(%ebx,%eax,icon_cool.gif
Intel: [ebx + eax*8 + _array]

二 基本的行内汇编
·基本的行内汇编很简单,通常是按照下面的格式:
asm("statements");
例如:asm("nop"); asm("cli");

·asm 和 __asm__是彻底同样的.
·若是有多行汇编,则每一行都要加上 " ".例如:
asm( "pushl %eax "
"movl $0,%eax "
"popl %eax");

实际上gcc在处理汇编时,是要把asm(...)的内容"打印"到汇编文件中,因此格式控制字符是必要的.
再例如:
asm("movl %eax,%ebx");
asm("xorl %ebx,%edx");
asm("movl $0,_booga);

在上面的例子中,因为咱们在行内汇编中改变了edx和ebx的值,可是因为gcc的特殊的处理方法,即先造成汇编文件,再交给GAS去汇编,因此GAS并不知道咱们已经改变了edx和ebx的值,若是程序的上下文须要edx或ebx做暂存,这样就会引发严重的后果 .对于变量_booga也存在同样的问题.为了解决这个问题,就要用到扩展的行内汇编语法.

三 扩展的行内汇编
扩展的行内汇编相似于Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是被改变的寄存器.

下面是一个例子(为方便起见,我使用全局变量):
int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld "
"rep "
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}

获得的主要汇编代码为:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解释了.这几条语句的功能是向buf中写上count个value值.冒号后的语句指明输入,输出和被改变的寄存器.经过冒号之后的语句,编译器就知道你的指令须要和改变哪些寄存器,从而能够优化寄存器的分配.其中符号"c"(count)指示要把count的值放入ecx寄存器

几点说明:
1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器.使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器.
2.咱们没必要把编译器分配的寄存器放入改变的寄存器列表,由于寄存器已经记住了它们.
3."="是标示输出寄存器,必须这样用.
4.数字%n的用法:数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求的寄存器.若是咱们要重用"r"或"q"请求的寄存器的话,就可使用它们.
5.若是强制使用固定的寄存器的话,如不用%1,而用ebx,则asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );

6.注意要使用两个%,由于一个%的语法已经被%n用掉了.


FAQ:
一、变量加下划线和双下划线有什么特殊含义吗?
加下划线是指全局变量,但个人gcc中加不加都无所谓.

二、以上定义用以下调用时展开会是什么意思?#define _syscall1(type,name,type1,arg1) type name(type1 arg1) { long __res; /* __res应该是一个全局变量 */__asm__ volatile ("int $0x80" /* volatile 的意思是不容许优化,使编译器严格按照你的汇编代码汇编*/: "=a" (__res) /* 产生代码 movl %eax, __res */: "0" (__NR_##name),"b" ((long)(arg1))); /* 若是我没记错的话,这里##指的是两次宏展开.  即用实际的系统调用名字代替"name",而后再把__NR_...展开.  接着把展开的常数放入eax,把arg1放入ebx */if (__res >= 0) return (type) __res; errno = -__res; return -1; }

相关文章
相关标签/搜索