当前位置:首页 > > 充电吧
[导读]AT&T汇编与Intel汇编的比较 文章作者:linuxkernel (newbie) 既然大家对汇编感兴趣,不妨我也来凑凑热闹。废话少说,言归正传。 Intel和AT&T语法的

AT&T汇编与Intel汇编的比较 文章作者:linuxkernel (newbie)


既然大家对汇编感兴趣,不妨我也来凑凑热闹。废话少说,言归正传。

Intel和AT&T语法的区别
Intel和AT&T汇编语言的语法表面上各不相同,这将导致刚刚学会INTEL汇编的人第一次见到AT&T汇编时
会感到困惑,或者反之。因此让我们从基础的东西开始。

前缀
在Intel汇编中没有寄存器前缀或者立即数前缀。而在AT&T汇编中寄存器有一个“%”前缀,立即数有
一个“$”前缀。Intel语句中十六进制和二进制数据分别带有“h”和“b”后缀,并且如果十六进制
数字的第一位是字母的话,那么数值的前面要加一个“0”前缀。
例如,
Intex Syntax
mov   eax,1
mov   ebx,0ffh
int   80h

AT&T Syntax
movl   $1,%eax
movl   $0xff,%ebx
int    $0x80
就像你看到的,AT&T非常难懂。[base+index*scale+disp] 看起来比disp(base,index,scale)更好理解。


操作数的用法
intel语句中操作数的用法和AT&T中的用法相反。在Intel语句中,第一个操作数表示目的,第二个
操作数表示源。然而在AT&T语句中第一个操作数表示源而第二个操作数表示目的。在这种情形下AT&T语法
的好处是显而易见的。我们从左向右读,也从左向右写,这样比较自然。
例如,
Intex Syntax
instr   dest,source
mov   eax,[ecx]
   
AT&T Syntax
instr    source,dest
movl   (%ecx),%eax

存储器操作数
如同上面所看到的,存储器操作数的用法也不相同。在Intel语句中基址寄存器用“[”和“]”括起来
而在AT&T语句中是用“(”和“)”括起来的。
例如,
Intex Syntax
mov   eax,[ebx]
mov   eax,[ebx+3]
AT&T Syntax
movl   (%ebx),%eax
movl   3(%ebx),%eax
AT&T语法中用来处理复杂的操作的指令的形式和Intel语法中的形式比较起来要难懂得多。在Intel语句
中这样的形式是segreg:[base+index*scale+disp]。在AT&T语句中这样的形式是
%segreg:disp(base,index,scale)。
Index/scale/disp/segreg 都是可选并且可以去掉的。Scale在本身没有说明而index已指定的情况下
缺省值为1。segreg的确定依赖于指令本身以及程序运行在实模式还是pmode。在实模式下它依赖于
指令本身而pmode模式下它是不需要的。在AT&T语句中用作scale/disp的立即数不要加“$”前缀。
例如
Intel Syntax
instr    foo,segreg:[base+index*scale+disp]
mov   eax,[ebx+20h]
add   eax,[ebx+ecx*2h]
lea   eax,[ebx+ecx]
sub   eax,[ebx+ecx*4h-20h]   
AT&T Syntax
instr   %segreg:disp(base,index,scale),foo
movl   0x20(%ebx),%eax
addl   (%ebx,%ecx,0x2),%eax
leal   (%ebx,%ecx),%eax
subl   -0x20(%ebx,%ecx,0x4),%eax

后缀
就像你已经注意到的,AT&T语法中有一个后缀,它的意义是表示操作数的大小。“l”代表long,
“w”代表word,“b”代表byte。Intel语法中在处理存储器操作数时也有类似的表示,
如byte ptr, word ptr, dword ptr。"dword" 显然对应于“long”。这有点类似于C语言中定义的
类型,但是既然使用的寄存器的大小对应着假定的数据类型,这样就显得不必要了。
例子:
Intel Syntax
mov   al,bl
mov   ax,bx
mov   eax,ebx
mov   eax, dword ptr [ebx]   
AT&T Syntax
movb   %bl,%al
movw   %bx,%ax
movl   %ebx,%eax
movl   (%ebx),%eax

注意:从此开始所有的例子都使用AT&T语法
系统调用
本节将介绍linux中汇编语言系统调用的用法。系统调用包括位于/usr/man/man2的手册里第二部分所有
的函数。这些函数也在/usr/include/sys/syscall.h中列出来了。一个重要的关于这些函数的列表是
在http://www.linuxassembly.org/syscall.html里。这些函数通过linux中断服务:int $0x80来被执行
小于六个参数的系统调用
对于所有的系统调用,系统调用号在%eax中。对于小于六个参数的系统调用,参数依次存放
在%ebx,%ecx,%edx,%esi,%edi中,系统调用的返回值保存在%eax中。
系统调用号可以在/usr/include/sys/syscall.h中找到。宏被定义成SYS_的形式,
如SYS_exit, SYS_close等。
例子:(hello world 程序)
参照write(2)的帮助手册,写操作被声明为ssize_t write(int fd, const void *buf, size_t count);
这样,fd应存放在%ebx中,buf放在 %ecx, count 放在 %edx , SYS_write 放在 %eax中,紧跟着是
int $0x80语句来执行系统调用。系统调用的返回值保存在%eax中。
$ cat write.s
.include "defines.h"
.data
hello:
   .string "hello world/n"

.globl   main
main:
   movl   $SYS_write,%eax
   movl   $STDOUT,%ebx
   movl   $hello,%ecx
   movl   $12,%edx
   int   $0x80

   ret
$
少于5个参数的系统调用的处理也是这样的。只是没有用到的寄存器保持不变罢了。象open或者fcntl这样
带有一个可选的额外参数的系统调用也就知道怎么用了。
大于5个参数的系统调用
参数个数大于五个的系统调用仍然把系统调用号保存在%eax中,但是参数存放在内存中,并且指向第一个
参数的指针保存在%ebx中。
如果你使用栈,参数必须被逆序压进栈里,即按最后一个参数到第一个参数的顺序。然后将栈的指针拷贝
到%ebx中。或者将参数拷贝到一块分配的内存区域,然后把第一个参数的地址保存在%ebx中。
例子:(使用mmap作为系统调用的例子)。在C中使用mmap():
#include
#include
#include
#include
#include

#define STDOUT   1

void main(void) {
   char file[]="mmap.s";
   char *mappedptr;
   int fd,filelen;

   fd=fopen(file, O_RDONLY);
   filelen=lseek(fd,0,SEEK_END);
   mappedptr=mmap(NULL,filelen,PROT_READ,MAP_SHARED,fd,0);
   write(STDOUT, mappedptr, filelen);
   munmap(mappedptr, filelen);
   close(fd);
}
mmap()参数在内存中的排列:
%esp   %esp+4   %esp+8   %esp+12   %esp+16   %esp+20
00000000   filelen   00000001   00000001   fd   00000000
等价的汇编程序:
$ cat mmap.s
.include "defines.h"

.data
file:
   .string "mmap.s"
fd:
   .long    0
filelen:
   .long    0
mappedptr:
   .long    0

.globl main
main:
   push   %ebp
   movl   %esp,%ebp
   subl   $24,%esp

//   open($file, $O_RDONLY);

   movl   $fd,%ebx   // save fd
   movl   %eax,(%ebx)

//   lseek($fd,0,$SEEK_END);

   movl   $filelen,%ebx   // save file length
   movl   %eax,(%ebx)

   xorl   %edx,%edx

//   mmap(NULL,$filelen,PROT_READ,MAP_SHARED,$fd,0);
   movl   %edx,(%esp)
   movl   %eax,4(%esp)   // file length still in %eax
   movl   $PROT_READ,8(%esp)
   movl   $MAP_SHARED,12(%esp)
   movl   $fd,%ebx   // load file descriptor
   movl   (%ebx),%eax
   movl   %eax,16(%esp)
   movl   %edx,20(%esp)
   movl   $SYS_mmap,%eax
   movl   %esp,%ebx
   int   $0x80

   movl   $mappedptr,%ebx   // save ptr
   movl   %eax,(%ebx)
      
//    write($stdout, $mappedptr, $filelen);
//   munmap($mappedptr, $filelen);
//   close($fd);
   
   movl   %ebp,%esp
   popl   %ebp

   ret
$
注意:上面所列出的源代码和本文结束部分的例子的源代码不同。上面列出的代码中没有说明其它的
系统调用,因为这不是本节的重点,上面列出的源代码仅仅打开mmap.s文件,而例子的源代码要读
命令行的参数。这个mmap的例子还用到lseek来获取文件大小。
Socket系统调用
Socket系统调用使用唯一的系统调用号:SYS_socketcall,它保存在%eax中。Socket函数是通过位于
/usr/include/linux/net.h的一个子函数号来确定的,并且它们被保存在%ebx中。指向系统调用参数
的一个指针存放在%ecx中。Socket系统调用也是通过int $0x80来执行的。
$ cat socket.s
.include "defines.h"

.globl   _start
_start:
   pushl   %ebp
   movl   %esp,%ebp
   sub   $12,%esp

//   socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
   movl   $AF_INET,(%esp)
   movl   $SOCK_STREAM,4(%esp)
   movl   $IPPROTO_TCP,8(%esp)

   movl   $SYS_socketcall,%eax
   movl   $SYS_socketcall_socket,%ebx
   movl   %esp,%ecx
   int   $0x80

   movl    $SYS_exit,%eax
   xorl    %ebx,%ebx
   int    $0x80

   movl   %ebp,%esp
   popl   %ebp
   ret
$

命令行参数
在linux中执行的时候命令行参数是放在栈上的。先是argc,跟着是一个由指向命令行中各字符串的
指针组成的数组(**argv)并以空指针结束。接下来是一个由指向环境变量的指针组成的
数组(**envp)。这些东西在asm中都可以很容易的获得,并且在例子代码(args.s)中有示范。


GCC内联汇编
本节中GCC内联汇编仅涉及x86的应用程序。操作数约束会和其它处理器上的有所不同。关于这部分
的说明放在本文的最后。
gcc中基本的内联汇编非常易懂,如
__asm__("movl   %esp,%eax");   // look familiar ?

或者是
__asm__("
        movl   $1,%eax      // SYS_exit
        xor   %ebx,%ebx
        int   $0x80
   ");
如果指定了用作asm的输入、输出数据并指出哪一个寄存器会被修改,会使程序的执行效率提高。
input/output/modify都不是必需的。格式如下:
__asm__("" : output : input : modify);
output和input中必须包含一个操作数约束字符串,并紧跟一个用圆括号括起来的C语言表达式。
输出操作数约束的前面必须有一个“=”,表示这是一个输出。可能会有多个输出,多个输入和
多个修改过的寄存器。每个“入口”应该用“,”分隔开,并且入口的总数不多有10个。
操作数约束字符串可以是包含整个寄存器的名称也可以是简写。
Abbrev Table
Abbrev   Register
a   %eax/%ax/%al
b   %ebx/%bx/%bl
c   %ecx/%cx/%cl
d   %edx/%dx/%dl
S   %esi/%si
D   %edi/%di
m   memory
例如:

   __asm__("test   %%eax,%%eax", : /* no output */ : "a"(foo));


或者是

   __asm__("test   %%eax,%%eax", : /* no output */ : "eax"(foo));
你可以在__asm__后使用关键字__volatile__:“你可以利用在__asm__后使用关键字__volatile__的
方法防止一条‘asm’指令被删除、移动或者被重新组合。”(出自gcc的info文件中"Assembler
Instructions with C Expression Operands" 部分)
$ cat inline1.c
#include

int main(void) {
   int foo=10,bar=15;
   
   __asm__ __volatile__ ("addl    %%ebxx,%%eax"
      : "=eax"(foo)      // ouput
      : "eax"(foo), "ebx"(bar)// input
      : "eax"        // modify
   );
   printf("foo+bar=%d/n", foo);
   return 0;
}
$
你可能已经注意到现在寄存器使用“%%”前缀而不是“%”。这在使用output/input/modify域时是必要的,
这是因为此时基于其它域的寄存器的别名的使用。我马上来讨论这个问题。
你可以很简单的指定“a”而不是写“eax”或者强制使用一个特殊寄存器如"eax"、"ax"、"al",
这同样适用于其它一般用途的寄存器(在Abbrev表中列出的)。当你在当前的代码中使用特殊的寄存器
时这好像毫无用处,因此gcc提供了寄存器别名。最多有10个别名(%0—%9),这也是为什么只允许10个
输入/输出的原因。
$ cat inline2.c
int main(void) {
   long eax;
   short bx;
   char cl;

   __asm__("nop;nop;nop"); // to separate inline asm from the rest of
           // the code
   __volatile__ __asm__("
      test   %0,%0
      test   %1,%1
      test   %2,%2"
      : /* no outputs */
      : "a"((long)eax), "b"((short)bx), "c"((char)cl)
   );
   __asm__("nop;nop;nop");
   return 0;
}
$ gcc -o inline2 inline2.c
$ gdb ./inline2
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnulibc1"...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
... start: inline asm ...
0x8048427 : nop
0x8048428 : nop
0x8048429 : nop
0x804842a : mov 0xfffffffc(%ebp),%eax
0x804842d : mov 0xfffffffa(%ebp),%bx
0x8048431 : mov 0xfffffff9(%ebp),%cl
0x8048434 : test %eax,%eax
0x8048436 : test %bx,%bx
0x8048439 : test %cl,%cl
0x804843b : nop
0x804843c : nop
0x804843d : nop
... end: inline asm ...
End of assembler dump.
$
就像你看到的,由内联汇编生成的代码将变量的值放入它们在input域中指定的寄存器中,然后继续
执行当前的代码。编译器自动根据变量的大小来侦测操作数的大小,这样相应的寄存器就被
别名%0, %1 和 %2代替了(当使用寄存器别名时在存储器里指定操作数的大小回导致编译时发生错误)
在操作数约束里也可以使用别名。这不允许你在输入/输出域中指定多于10个的入口。我能想到的这样
做的唯一用法是在你指定操作数约束为“q”以便让编译器在a,b,c,d寄存器之间进行选择的时候。
当这个寄存器被修改时,我们不会知道选中了那个寄存器,因而不能在modify域中指定它。
这种情况下你只需指定""。
例子:
$ cat inline3.c
#include

int main(void) {
   long eax=1,ebx=2;

   __asm__ __volatile__ ("add %0,%2"
      : "=b"((long)ebx)
      : "a"((long)eax), "q"(ebx)
      : "2"
   );
   printf("ebx=%x/n", ebx);
   return 0;
}
$

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭