Chinaunix首页 | 论坛 | 博客
  • 博客访问: 238216
  • 博文数量: 46
  • 博客积分: 1410
  • 博客等级: 上尉
  • 技术积分: 622
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-17 20:07
文章分类

全部博文(46)

文章存档

2011年(1)

2010年(23)

2009年(22)

我的朋友

分类: 嵌入式

2009-11-20 17:05:54

 

学习linux内核少不了要读at&t的汇编,这个对大多数使用intel汇编的人是一个不幸的消息,要另起炉灶。什么?你不是学得intel汇编?你是不是中国学生?记得我们有门课是学汇编语言>,学得挺感兴趣,窃以为这个就是学电脑的最高境界。 当时怎么也没人说这个是intel汇编语言格式,还有个什么at&t格式 ....

不扯远了,这两者之间还是有章可循的,我觉得要注意的是转移指令,这个要仔细琢磨一下才看的明白。内存间接寻址的格式看起来也比较费劲,其他的东西是到是可以看一次就明白的。如果您有耐心,希望查看更权威的资料建议您查阅as.infogcc.info,实际上,严格是说,gas只是众多遵循at&t格式汇编程序中的一个实现特例,而你寻找的汇编语言规则,只与你使用的汇编程序和编译器有关。
1
指令操作数的顺序是先源后目的,与Intel指令的先目的后源的顺序相反;
2
寄存器操作数总是以'%'作为前缀。
3
立即数前加前缀$
4
操作码加后缀以指明操作数的长度,这些后缀有b8位)、w16位)、l32位);
例:
  AT&T    Intel
 moww %bx, %ax  // mov ax, bx
 xorl %eax, %eax  // xor eax, eax
 movw $1, %ax   // mov ax,1
 movb X, %ah   // mov ah, byte ptr X
 movw X, %ax   // mov ax, word ptr X
 movl X, %eax   // mov eax, X

如果操作码中没有后缀来指定操作数的长度,并且该指令中没有内存操作数,gas
根据指令中指定的寄存器操作数补上相应的后缀字符。所以,下面两个指令是等价的(这是Gas的特性,而真正的AT&TUnix汇编程序会将将没有字长后缀的指令的操作数字长全部当作32位):

linux中,因为是使用gas进行汇编的,故此

           mov %ax,%bx 

等价于

           movw %ax,%bx

 


5
大部分指令的操作码都与Intel指令相同,只有下面几个是例外:
 movsSD     // movsx
,短到长,高位补符号位
movzSD     // movzx
,短到长,高位补0
这里,S 是代表源操作数长度的后缀,D 是代表目的操作数长度的后缀。如:
 movswl %ax, %ecx  // movsx ecx, ax 
cbtw           // cbw
,将AL中的一个字节扩充为一个字(AX),
//         AH
AL的符号位
cwtl           // cwde 
,将AX中的一个字扩充为一个双字(EAX),
      //    EAX
的高位是AX的符号位
 cwtd           // cwd 
,将AX中的一个字扩充为两个字(DXAX
      //    DX
AX的符号位
cltd           // cdq
,将EAX中的一个双字扩充为两个双字
      //   
EDXEAX),EDXEAX的符号位
lcall $S,$O    // call far S:O
 ljmp $S,$O     // jmp far S:O
 lret $V        // ret far V
6
操作码的前缀(如rep)要单独写一行,不要和它修饰的指令(如movsbstosb)写在同一行上;
7
内存间接寻址的格式不同
    AT&T        Intel
disp(base, index, scale)   [base + index*scale + disp]
例:
 movl  4(%ebp), %eax             // mov eax, [ebp+4]
 addl  (%eax,%eax,4), %ecx       // add ecx, [eax + eax*4]
 movb  $4, %fs:(%eax)            // mov fs:eax, 4
 movl  _array(,%eax,4), %eax     // mov eax, [4*eax + array]
 movw  _array(%ebx,%eax,4), %cx  // mov cx, [ebx + 4*eax + array])
8
局部标号可以用数字,而且可以重复。在以这些标号为目的的转移指令上,标号要带上后缀,b表示向前,f表示向后。
例:
 orw %bx,%bx
 jz 1f
1:
 movl $0x101000,%eax
 movl %eax,%cr3  /* set the page table pointer.. */
 movl %cr0,%eax
 orl $0x80000000,%eax
 movl %eax,%cr0  /* ..and set paging (PG) bit */
 jmp 1f    /* flush the prefetch-queue */
1:
 movl $1f,%eax
 jmp *%eax   /* make sure eip is relocated */
1:

绝对跳转/调用指令中的内存操作数必须以'*'为前缀,否则gas总是认为是相对跳转/调用指令,而且gas汇编程序自动对跳转指令进行优化,总是使用尽可能小的跳转偏移量。如果8比特的偏移量无法满足要求的话,as会使用一个32位的偏移量,as汇编程序暂时还不支持16位的跳转偏移量,所以对跳转指令使用'addr16'前缀是无效的。还有一些跳转指令只支持8位的跳转偏移量,这些指令是:

'jcxz','jecxz','loop','loopz','loope','loopnz''loopne'

如果你在汇编中使用了这些指令,用gas的汇编可能会出错,因为gcc在编译过程中不产生这些指令,所以在c语言中不必担心这些问题。


9
实模式下的语法与Intel指令语法基本相同;可以用上述格式的汇编单独写程序(有许多宏定义和它特有的文件格式),而后用gcc/gas将其汇编成目标代码。在linux中,这种形式的代码主要集中在启动部分。

 

10、还有一个比较特别的,就是gcc可以编译嵌入在c语言中的汇编段落。在Linux代码中很多地方都使用了这种形式的汇编语言,嵌入汇编程序的格式如下:
__asm__ __volatile__ (
asm statements
: outputs
: inputs
: registers-modified
);
 asm statements
是一组AT&T格式的汇编语言语句,每个语句一行,由\n分隔各行。所有的语句都被包裹在一对双引号内。其中使用的寄存器前面要加两个%%做前缀;转移指令多是局部转移,因此多使用数字标号。
 inputs
指明程序的输入参数,每个输入参数都括在一对圆括号内,各参数用逗号分开。每个参数前加一个用双引号括起来的标志,告诉编译器把该参数装入到何处。可用的标志有:“g”:让编译器决定如何装入它;“a”:装入到ax/eax“b”:装入到bx/ebx“c”:装入到cx/ecx“d”:装入到dx/edx“D”:装入到di/edi“S”:装入到si/esi“q”abcd寄存器等;“r”:任一通用寄存器;“i”:整立即数;“p”:有效内存地址;“=”:输出;“+”:既是输入又是输出;“&”:改变其值之前写;“%”:与下一个操作数之间可互换;“#”:忽略其后的字符,直到逗号;“*”:当优先选择寄存器时,忽略下面的字符;“0~9”:指定一个操作数,它既做输入又做输出。通常用“g”
 outputs
指明程序的输出位置,通常是变量。每个输出变量都括在一对圆括号内,各个输出变量间用逗号隔开。每个输出变量前加一个标志,告诉编译器从何处输出。可用的标志与输入参数用的标志相同,只是前面加“=”。如“=g”。输出操作数必须是左值,而且必须是只写的。如果一个操作数即做输出又做输入,那么必须将它们分开:一个只写操作数,一个输入操作数。输入操作数前加一个数字限制(0~9),指出输出操作数的序号,告诉编译器它们必须在同一个物理位置。两个操作数可以是同一个表达式,也可以是不同的表达式。
 registers-modified
告诉编译器程序中将要修改的寄存器。每个寄存器都用双引号括起来,并用逗号隔开。如“ax”。如果汇编程序中引用了某个特定的硬件寄存器,就应该在此处列出这些寄存器,以告诉编译器这些寄存器的值被改变了。如果汇编程序中用某种不可预测的方式修改了内存,应该在此处加上“memory”。这样以来,在整个汇编程序中,编译器就不会把它的值缓存在寄存器中了。
 __volatile__
是可选的,它防止编译器修改该段汇编语句(重排序、重组、删除等)。
 
输入参数和输出变量按顺序编号,先输出后输入,编号从0开始。程序中用编号代表输入参数和输出变量(加%做前缀)。
输入、输出、寄存器部分都可有可无。如有,顺序不能变;如无,应保留,除非不引起二意性。

 

看一个在C语言中使用at&t的嵌入汇编程序的例子,c语言中的3int变量,一般会是三个内存地址。每个操作数的长度则要根据操作系统和编译器来决定,一般32位操作系统为32位,则每个操作数占用4个字节:
 int i=0, j=1, k=0;
 __asm__ __volatile__("
  pushl %%eax\n               //asm statement
  movl %1, %%eax\n            //asm statement
  addl %2, %%eax\n            //asm statement
  movl %%eax, %0\n            //...
  popl %%eax"                 //...
 : "=g" (k)                   //outputs                                                    
 : "g" (i), "g" (j)           //inputs
 : "ax", "memory"             //registers modified
 );    

按照参数编号原则输出参数参数k%0,输入参数ij依次为%1%2。值得注意的是输出和输入标志都使用了"g",所以我们不必关心这些参数究竟是使用了寄存器还是内存操作数,编译器自己会决定。

 

阅读(1576) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~