GCC inline assembler 基本说明

GCC inline assembler:

1]基本格式和说明:

内联汇编格式 __asm__("asm statements" : outputs : inputs : registers-modified);

插入到 C 代码中的汇编语句是以 ":" 分隔的四个部分,其中第一部分就是汇编代码本身,通常称为指令部,其格式和在汇编语言中使用的格式基本相同.指令部分是必须的,而其它部分则可以根据实际情况而省略.

在将汇编语句嵌入到C代码中时,操作数如何与C代码中的变量相结合是个很大的问题.GCC采用如下方法来解决这个问题.程序员提供具体的指令,而对寄存器的使用则只需给出"样板"和约束条件就可以了,具体如何将寄存器与变量结合起来完全由GCC和GAS来负责.

在GCC内联汇编语句的指令部中,加上前缀'%'的数字 (如%0,%1) 表示的就是需要使用寄存器的"样板"操作数.指令部中使用了几个样板操作数,就表明有几个变量需要与寄存器相结合,这样GCC和GAS在编译和汇编时会根据后面给定的约束条件进行恰当的处理.由于样板操作数也使用'%'作为前缀,因此在涉及到具体的寄存器时,寄存器名前面应该加上两个'%',以免产生混淆.

紧跟在指令部后面的是输出部,是规定输出变量如何与样板操作数进行结合的条件,每个条件称为一个"约束",必要时可以包含多个约束,相互之间用逗号分隔开就可以了.每个输出约束都以'='号开始,然后紧跟一个对操作数类型进行说明的字后,最后是如何与变量相结合的约束.凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行完嵌入的汇编代码后均不保留执行之前的内容,这是GCC在调度寄存器时所使用的依据.

输出部后面是输入部,输入约束的格式和输出约束相似,但不带'='号.如果一个输入约束要求使用寄存器,则GCC在预处理时就会为之分配一个寄存器,并插入必要的指令将操作数装入该寄存器.与输入部中说明的操作数结合的寄存器或操作数本身,在执行完嵌入的汇编代码后也不保留执行之前的内容.

有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样就难免会破坏原有寄存器的内容.在GCC内联汇编格式中的最后一个部分中,可以对将产生副作用的寄存器进行说明,以便GCC能够采用相应的措施.

在内联汇编中用到的操作数从输出部的第一个约束开始编号,序号从0开始,每个约束记数一次,指令部要引用这些操作数时,只需在序号前加上'%'作为前缀就可以了.需要注意的是,内联汇编语句的指令部在引用一个操作数时总是将其作为32位的长字使用,但实际情况可能需要的是字或字节,因此应该在约束中指明正确的限定符.

 

限定符 意义
"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)

 

举例详解:

======================================================================

本节是第一次在内核源程序中接触到C语言中的嵌入式汇编代码。由于我们在通常的C语言程序的编制过程中一般是不会使用嵌入式汇编程序的,因此这里有必要对其基本格式进行简单的描述,详细的说明可参见GNU gcc手册中[5]第4章的内容(Extensions to the C Language Family),或见参考文献[20](Using Inline Assembly with gcc)。


  具有输入和输出参数的嵌入汇编的基本格式为:

asm(“汇编语句”
   : 输出寄存器
   : 输入寄存器
   : 会被修改的寄存器 );

  其中,“汇编语句”是你写汇编指令的地方;“输出寄存器”表示当这段嵌入汇编执行完之后,哪些寄存器用于存放输出数据。此地,这些寄存器会分别对应一C 语言表达式或一个内存地址;“输入寄存器”表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值。下面我们用例子来说明嵌入汇编语句的使用方法。

  我们在下面列出了前面代码中第22行开始的一段代码作为例子来详细解说,为了能看清楚我们将这段代码进行了重新编排和编号。

01  #define get_seg_byte(seg,addr) /
02  ({ /
03  register char __res; /
04  __asm__("push %%fs;  /
05           mov %%ax,%%fs;  /
06           movb %%fs:%2,%%al;  /
07           pop %%fs"  /
08           :"=a" (__res)  /
09           :"" (seg),"m" (*(addr))); /
10  __res;})

  这段10行代码定义了一个嵌入汇编语言宏函数。因为是宏语句,需要在一行上定义,因此这里使用反斜杠'/'将这些语句连成一行。这条宏定义将被替换到宏名称在程序中被引用的地方。第1行定义了宏的名称,也即是宏函数名称get_seg_byte(seg,addr)。第3行定义了一个寄存器变量 __res 。第4行上的__asm__ 表示嵌入汇编语句的开始 。从第4行到第7行的4条AT&T格式的汇编语句。

  第8行即是输出寄存器,这句的含义是在这段代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值,"=a"中的"a"称为加载代码"="表示这是输出寄存器 。第9行表示在这段代码开始运行时将seg放到eax寄存器中,""表示使用与上面同个位置的输出相同的寄存器。而(* (addr))表示一个内存偏移地址值。为了在上面汇编语句中使用该地址值,嵌入汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以"%0"开始,分别记为%0、%1、…%9。因此,输出寄存器的编号是%0(这里只有一个输出寄存器),输入寄存器前一部分("" (seg))的编号是%1,而后部分的编号是%2。上面第6行上的%2即代表(*(addr))这个内存偏移量。

  现在我们来研究4— 7行上的代码的作用。第一句将fs段寄存器的内容入栈;第二句将eax中的段值赋给fs段寄存器;第三句是把fs:(*(addr))所指定的字节放入 al寄存器中。当执行完汇编语句后,输出寄存器eax 的值将被放入__res,作为该宏函数的返回值。(注意这里的写法:跟在输入部分之后就是__res;也就是说应该是宏函数的{}内最后部分作返回值。注意编译器的支持)

  通过上面分析,我们知道,宏名称中的seg代表一指定的内存段值,而addr表示一内存偏移地址量。到现在为止,我们应该很清楚这段程序的功能了吧!该宏函数的功能是从指定段和偏移值的内存地址处取一个字节。

在看下一个例子。
01  asm("cld/n/t"
02      "rep/n/t"
03      "stol"
04      : /* 没有输出寄存器 */
05      : "c"(count-1), "a"(fill_value), "D"(dest)
06      : "%ecx", "%edi");

  1-3行这三句是通常的汇编语句,用以清方向位,重复保存值。第4行说明这段嵌入汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx寄存器中(加载代码是"c"),fill_value加载到eax中,dest放到edi中。为什么要让gcc编译程序去做这样的寄存器值的加载,而不让我们自己做呢?因为gcc在它进行寄存器分配时可以进行某些优化工作。例如fill_value值可能已经在eax中。如果是在一个循环语句中的话,gcc就可能在整个循环操作中保留eax,这样就可以在每次循环中少用一个movl语句。

  最后一行的作用是告诉gcc这些寄存器中的值已经改变了。很古怪吧?不过在gcc知道你拿这些寄存器做些什么后,这确实能够对gcc的优化操作有所帮助。

  下面列表中,是一些你可能会用到的寄存器加载代码及其具体的含义。


表4.1 常用寄存器加载代码说明

代码说明代码说明
-----------------------------------------------
a    使用寄存器eax                        m    使用内存地址
b    使用寄存器ebx                        o    使用内存地址并可以加偏移值
c    使用寄存器ecx                        I    使用常数0-31
d    使用寄存器edx                        J    使用常数0-63
S    使用esi                              K    使用常数0-255
D    使用edi                              L    使用常数0-65535
q    使用动态分配字节可寻址寄存器
    (eax、ebx、ecx或edx)               M    使用常数0-3
r    使用任意动态分配的寄存器             N    使用1字节常数(0-255)
g    使用通用有效的地址即可
    (eax、ebx、ecx、edx或内存变量)     O    使用常数0-31
A    使用eax与edx联合(64位)


  下面的例子不是让你自己指定哪个变量使用哪个寄存器,而是让gcc为你选择。

01asm("leal (%1, %1, 4), %0"
02: "=r"(y)
03: "0"(x));

  第一句汇编语句leal (r1, r2,4), r3语句表示 r1+r2*4  r3。这个例子可以非常快地将x乘5。其中"%0","%1"是指gcc自动分配的寄存器。这里"%1"代表输入值x要放入的寄存器,"%0"表示输出值寄存器。输出寄存器代码前一定要加等于号。如果输入寄存器的代码是0或为空时,则说明使用与相应输出一样的寄存器。所以,如果gcc将r指定为eax的话,那么上面汇编语句的含义即为:

"leal (eax,eax,4), eax"

  注意:在执行代码时,如果不希望汇编语句被gcc优化而挪动地方,就需要在asm符号后面添加volatile关键词:
  asm volatile (……);
或者更详细的说明为:
  __asm__ __volatile__ (……);

  下面在具一个较长的例子,如果能看得懂,那就说明嵌入汇编代码对你来说基本没问题了。这段代码是从include/string.h文件中摘取的,是 strncmp()字符串比较函数的一种实现。需要注意的是,其中每行中的"/n/t"是用于gcc预处理程序输出列表好看而设置的,含义与C语言中相同。

字符串1与字符串2的前count个字符进行比较。
// 参数:cs - 字符串1,ct - 字符串2,count - 比较的字符数。
// %0 - eax(__res)返回值,%1 - edi(cs)串1指针,%2 - esi(ct)串2指针,%3 - ecx(count)。
// 返回:如果串1 > 串2,则返回1;串1 = 串2,则返回0;串1 < 串2,则返回-1。
107 extern inline int strncmp(const char * cs,const char * ct,int count)
108 {
109 register int __res __asm__("ax");    // __res是寄存器变量(eax)。
110 __asm__("cld/n"                      // 清方向位。
111         "1:/tdecl %3/n/t"            // count--。
112         "js 2f/n/t"                  // 如果count<0,则向前跳转到标号2。
113         "lodsb/n/t"                  // 取串2的字符ds:[esi]al,并且esi++。
114         "scasb/n/t"                  // 比较al与串1的字符es:[edi],并且edi++。
115         "jne 3f/n/t"                 // 如果不相等,则向前跳转到标号3。
116         "testb %%al,%%al/n/t"        // 该字符是NULL字符吗?
117         "jne 1b/n"                   // 不是,则向后跳转到标号1,继续比较。
118         "2:/txorl %%eax,%%eax/n/t"   // 是NULL字符,则eax清零(返回值)。
119         "jmp 4f/n"                   // 向前跳转到标号4,结束。
120         "3:/tmovl $1,%%eax/n/t"      // eax中置1。
121         "jl 4f/n/t"                  // 如果前面比较中串2字符<串2字符,则返回1,结束。
122         "negl %%eax/n"               // 否则eax = -eax,返回负值,结束。
123         "4:"
124         :"=a" (__res):"D" (cs),"S" (ct),"c" (count):"si","di","cx");
125 return __res;                        // 返回比较结果。
126 }
参考:
我们都知道gcc的-S开关可以用来生成汇编代码,
但有时候,单有汇编文件是不够的,我们希望的是将C语言程序的源代码和汇编语言文本交错在一起查看,
这是LISTING功能,在gcc中并没有专门的FAQ说明,
区区在网上查了很多资料才知道怎么实现,所以特此记下。
gcc -c -g -Wa,-adlhn ee.c >  ee.anno.s      //   注意Wa,-adlhn   之间没有空格
=================================================================================
GCC Inline Assembly HOWTO 中文摘录
=================================================================================
1.GCC语法
采用AT&T语法格式(而不是Intel语法格式),如下几点
1)指令中Source-Destination的顺序:Op-code src dst
2)寄存器命名: %reg
        3)立即数的表示: $0xAB 或者 $1 或者 $label
        4)操作数大小:用b,w,l来做后缀表示byte,word(16位),long(32位)
5)内存的表示: disp(base, index, scale) 
2.基本inline
    语法:  asm("assembly code");
           或者  __asm__("assembly code");
    基本上就是照抄汇编程序,用asm("...")直接包起来。由于gcc 把每个指令当成字符串发送给as,所以每行必须用/n/t来格式化。譬如:
   由于这种inline汇编中只有指令,连操作数也无法指定,而且会无意中修改寄存器,造成灾难,因此很少使用。
3. 扩展inline
   由GCC自动分配寄存器,可指定input/output 操作数,会修改的寄存器。语法如下
  举例说明:
 
  • "b" 是 output operand,用 %0 表示,"a" 是 input operand,用 %1表示.
  • "r" 是操作数约束条件. 表示GCC可以使用任意寄存器来存储这个操作数。 output operand约束条件应该带一个"=". 表明是输出、只写。
  • 寄存器前用两个%. 
  • 要被修改的寄存器,指出asm中将会修改所列的寄存器,从而通知gcc不要使用这些寄存器存储任何值。有时候改变了条件状态,需要用“cc”表示此类hardware register (譬如指令jne,会找相应的flag)

 

再如:

 

 

上述“0”表示操作数1(x)和操作数0(five_times_x)合用一块空间(别名)。 同理 在input operands中可以用数字来代表output operands。

 

 

 

4. volatile 防止优化

asm volatile ("...")

或者 __asm__ __volatile__("...")

 

 

5. 经常使用的constraints(约束)

1) Register operand constraint(r)

        2) Memory operand constraint(m) :导致memory的读写

3) Matching(Digit) constraints: 某个变量既做input又做output使用

4) constraint modifiers:

               “=”指明了write-only属性,以前的至丢弃,现在的output值代替它

               “&”指明了这个output operand绝对不会和任何input operand享有同样的register

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值