Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1691135
  • 博文数量: 206
  • 博客积分: 1450
  • 博客等级: 上尉
  • 技术积分: 2285
  • 用 户 组: 普通用户
  • 注册时间: 2008-09-11 11:38
个人简介

学习永无止境!!

文章分类

全部博文(206)

文章存档

2022年(5)

2019年(3)

2018年(8)

2017年(32)

2016年(7)

2015年(13)

2014年(44)

2013年(24)

2011年(6)

2010年(17)

2009年(46)

2008年(1)

分类: LINUX

2009-11-11 14:36:15

 

gcc中的内嵌汇编语言(Intel i386平台)

.声明
   虽然Linux的核心代码大部分是用C语言编写的,但是不可避免的其中还是有一部分是用汇编语言写成的。有些汇编语言代码是直接写在汇编源程序中的,特别是Linux的启动代码部分;还有一些则是利用gcc的内嵌汇编语言嵌在C语言程序中的。这篇文章简单介绍了gcc中的内嵌式汇编语言,主要想帮助那些才开始阅读Linux核心代码的朋友们能够更快的入手。
写这篇文章的主要信息来源是GNU的两个info文件:as.infogcc.info,如果你觉得这篇文章中的介绍还不够详细的话,你可以查阅这两个文件。当然,直接查阅这两个文件可以获得更加权威的信息。如果你不想被这两篇文档中的一大堆信息搞迷糊的话,我建议你先阅读一下这篇文章,然后在必要时再去查阅更权威的信息。

.简介
   Linux的核心代码中,还是存在相当一部分的汇编语言代码。如果你想顺利阅读Linux代码的话,你不可能绕过这一部分代码。在Linux使用的汇编语言代码中,主要有两种格式:一种是直接写成汇编语言源程序的形式,这一部分主要是一些Linux的启动代码;另一部分则是利用gcc的内嵌式汇编语言语句asm嵌在LinuxC语言代码中的。这篇文章主要是介绍第二种形式的汇编语言代码。
   首先,介绍一下as支持的汇编语言的语法格式。大家知道,我们现在学习的汇编语言的格式主要是Intel风格的,而在Linux的核心代码中使用的则是AT&T格式的汇编语言代码,应该说大部分人对这种格式的汇编语言还不是很了解,所以我觉得有必要介绍一下。

--AT&T and Intel 汇编语法对照

寄存器命名:
AT&T:   %eax
Intel: eax

AT&T 语法源地址在左侧,目的地址在右侧与Intel 方式语法相反
将eax值传入ebx
AT&T:   movl %eax, %ebx
Intel: mov ebx, eax

AT&T 语法在立即数前有前缀$.
AT&T:   movl $0x0h, %eax
Intel: mov eax,0x0h

AT&T 语法在操作符后跟表示操作数类型的后缀b,w,l分别表示字节,字,双字,相当于伪操作符ptr,如果不加的话GAS会guess
AT&T:   movw %ax, %bx
Intel: mov bx, ax

内存寻址方式
AT&T:   immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + immed32]

地址计算公式为:
immed32 + basepointer + indexpointer * indexscale

直接寻址

AT&T:   _a
Intel: [_a]

间接寻址
AT&T:   (%eax)
Intel: [eax]

相对寻址
AT&T: _variable(%eax)
Intel: [eax + _variable]

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

C 代码: *(p+1) p定义为char *
AT&T:   1(%eax) where eax has the value of p
Intel: [eax + 1]

结构体数组寻址,结构体长度为8,下标存于eax,结构体内偏移地址存于ebx,_array为结构体数组首地址

AT&T:   _array(%ebx,%eax,8)
Intel: [ebx + eax*8 + _array]
函数内部实现交换

   接着,主要介绍一下gcc的内嵌式汇编语言的格式。gcc的内嵌式汇编语言提供了一种在C语言源程序中直接嵌入汇编指令的很好的办法,既能够直接控制所形成的指令序列,又有着与C语言的良好接口,所以在Linux代码中很多地方都使用了这一形式。

.gcc的内嵌汇编语言语句asm 
   利用gccasm语句,你可以在C语言代码中直接嵌入汇编语言指令,同时还可以使用C语言的表达式指定汇编指令所用到的操作数。这一特性提供了很大的方便。

内嵌汇编语法如下:

__asm__(
汇编语句模板:
输出部分:
输入部分:
破坏描述部分)


共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”隔开,相应部分内容为空。例如:

__asm__ __volatile__(
"cli":
:
:"memory")


  1.  由内嵌语法,知道要使用这一特性,首先要写一个汇编指令的模板(这种模板有点类似于机器描述文件中的指令模板),然后要为每一个操作数指定一个限定字符串。例如:
         extern __inline__ void change_bit(int nr,volatile void *addr) 
         {
           __asm__ __volatile__( LOCK_PREFIX
           "btcl %1,%0"
           :"=m" (ADDR)
           :"ir" (nr));
         }
       上面的函数中:
       LOCK_PREFIX:这是一个宏,如果定义了__SMP__,扩展为"lock;",用于指定总线锁定前缀,否则扩展为""
       ADDR:这也是一个宏,定义为(*(volatile struct __dummy *) addr)
       "btcl %1,%0":这就是嵌入的汇编语言指令,btcl为指令操作码,%1,%0是这条指令两个操作数的占位符。后面的两个限定字符串就用于描述这两个操作数。
       : "=m" (ADDR)第一个冒号后的限定字符串用于描述指令中的输出操作数。括号中的ADDR将操作数与C语言的变量联系起来。这个限定字符串表示指令中的“%0”就是addr指针指向的内存操作数。这是一个输出类型的内存操作数。
       : "ir" (nr)第二个冒号后的限定字符串用于描述指令中的输入操作数。这条限定字符串表示指令中的“%1”就是变量nr,这个的操作数可以是一个立即操作数或者是一个寄存器操作数。
       *注:限定字符串与操作数占位符之间的对应关系是这样的:在所有限定字符串中(包括第一个冒号后的以及第二个冒号后的所有限定字符串),最先出现的字符串用于描述操作数“%0”,第二个出现的字符串描述操作数“%1”,以此类推。

      汇编指令模板
       asm语句中的汇编指令模板主要由汇编指令序列和限定字符串组成。在一个asm语句中可以包括多条汇编指令。汇编指令序列中使用操作数占位符引用C语言中的变量。一条asm语句中最多可以包含十个操作数占位符%0%1...%9。汇编指令序列后面是操作数限定字符串,对指令序列中的占位符进行限定。限定的内容包括:该占位符与哪个C语言变量对应,可以是什么类型的操作数等等。限定字符串可以分为三个部分:输出操作数限定字符串(指令序列后第一个冒号后的限定字符串),输入操作数限定字符串(第一个冒号与第二个冒号之间),还有第三种类型的限定字符串在第二个冒号之后。同一种类型的限定字符串之间用逗号间隔。asm语句中出现的第一个限定字符串用于描述占位符“%0”,第二个用于描述占位符“%1”,以此类推(不管该限定字符串的类型)。如果指令序列中没有任何输出操作数,那么在语句中出现的第一个限定字符串(该字符串用于描述输入操作数)之前应该有两个冒号(这样,编译器就知道指令中没有输出操作数)。
       指令中的输出操作数对应的C语言变量应该具有左值类型,当然对于输出操作数没有这种左值限制。输出操作数必须是只写的,也就是说,asm对取出某个操作数,执行一定计算以后再将结果存回该操作数这种类型的汇编指令的支持不是直接的,而必须通过特定的格式的说明。如果汇编指令中包含了一个输入-输出类型的操作数,那么在模板中必须用两个占位符对该操作数的不同功能进行引用:一个负责输入,另一个负责输出。例如:
         asm ("addl %2,%0":"=r"(foo):"0"(foo),"g"(bar));
       在上面这条指令中,
       "%0”是一个输入-输出类型的操作数,
       "=r"(foo)用于限定其输出功能,该指令的输出结果会存放到C语言变量foo中;
       指令中没有显式的出现“%1”操作数,但是针对它有一个限定字符串"0"(foo),事实上指令中隐式的“%1”操作数用于描述“%0”操作数的输入功能,它的限定字符串中的"0"限定了“%1”操作数与“%0” 具有相同的地址。可以这样理解上述指令中的模板:该指令将“%1”“%2”中的值相加,计算结果存放回“%0”中,指令中的“%1”“%0”具有相同的地址。注意,用于描述“%1”"0"限定字符足以保证“%1”“%0”具有相同的地址。
       但是,如果用下面的指令完成,这种输入-输出操作就不会正常工作: 
         asm ("addl %2,%0":"=r"(foo):"r"(foo),"g"(bar)); 
       虽然该指令中“%0”“%1”同样引用了C语言变量foo,但是gcc并不保证在生成的汇编程序中它们具有相同的地址。
       还有一些汇编指令可能会改变某些寄存器的值,相应的汇编指令模板中必须将这种情况通知编译器。所以在模板中还有第三种类型的限定字符串,它们跟在输入操作数限定字符串的后面,之间用冒号间隔。这些字符串是某些寄存器的名称,代表该指令会改变这些寄存器中的内容。
       在内嵌的汇编指令中可能会直接引用某些硬件寄存器,我们已经知道AT&T格式的汇编语言中,寄存器名以“%”作为前缀,为了在生成的汇编程序中保留这个“%”号,在asm语句中对硬件寄存器的引用必须用“%%”作为寄存器名称的前缀。如果汇编指令改变了硬件寄存器的内容,不要忘记通知编译器(在第三种类型的限定串中添加相应的字符串)。还有一些指令可能会改变CPU标志寄存器EFLAG的内容,那么需要在第三种类型的限定字符串中加入"cc"
       为了防止gcc在优化过程中对asm中的汇编指令进行改变,可以在"asm"关键字后加上"volatile"修饰符。
       汇编语句序列语句之间使用“;”、“\n”或“\n\t”分开。
       
       操作数限定字符
       操作数限定字符串中利用规定的限定字符来描述相应的操作数,一些常用的限定字符有:(还有一些没有涉及的限定字符,参见gcc.info
         1."m":操作数是内存变量。
         2."o":操作数是内存变量,但它的寻址方式必须是偏移量类型的,也就是基址寻址或者基址加变址寻址。
         3."V":操作数是内存变量,其寻址方式非偏移量类型。 
         4." ":操作数是内存变量,其地址自动增量。 
         6."r":操作数是通用寄存器。 
         7."i":操作数是立即操作数。(其值可在汇编时确定) 
         8."n":操作数是立即操作数。有些系统不支持除字(双字节)以外的立即操作数,这些操作数要用"n"而不是"i"来描述。
         9."g":操作数可以是立即数,内存变量或者寄存器,只要寄存器属于通用寄存器。
         10."X":操作数允许是任何类型。
         11."0","1",...,"9":操作数与某个指定的操作数匹配。也就是说,该操作数就是指定的那个操作数。例如,如果用"0"来描述"%1"操作数,那么"%1"引用的其实就是"%0"操作数。
         12."p":操作数是一个合法的内存地址(指针)。
         13."=":操作数在指令中是只写的(输出操作数)。
         14."+":操作数在指令中是读-写类型的(输入-输出操作数)。
         22."f":浮点数寄存器。 
         23."t":第一个浮点数寄存器。 
         24."u":第二个浮点数寄存器。 
         27."I":0-31之间的立即数。(用于32位的移位指令) 
         28."J":0-63之间的立即数。(用于64位的移位指令) 
         29."N":0-255之间的立即数。(用于"out"指令
         30."G":标准的80387浮点常数。 
       注:还有一些不常见的限定字符并没有在此说明,另外有一些限定字符,例如"%","&"等由于我缺乏编译器方面的一些知识,所以我也不是很理解它们的含义,如果有高手愿意补充,不慎感激!不过在核心代码中出现的限定字符差不多就是上面这些了。
阅读(1569) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~