C与汇编接口(一)

  与汇编语言相比,C语言的效率还是无法与之相媲美。因此在对效率或者硬件操作要求比较高的地方,可以采用将部分汇编语句嵌入到C语言中。

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

  在内嵌汇编中,可以将C语言表达式指定为汇编操作的指令数,而且不用去管如何将C语言表达式的值读入那个寄存器以及如何将计算结果写回C变量,用户只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可,GCC会自动插入代码完成必要的操作。

1 内嵌汇编的语法

  在gcc中可以使用"_asm_"表示后面的代码为内嵌汇编代码,"volatile"表示编译器不要优代码,后面的指令保留原样。

格式如下:

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

  注:汇编语句模板是必不可少的,其他的可选。如果使用了后面部分,前面部分为空,也需要用":"隔开,相应部分内容为空。

_asm_ _volatile_("cli" : : : "memory")

1.1 汇编语句模板

  汇编语句模板由汇编语句序列组成,语句之间使用":"、"\n"或"\n\t"分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:

       %0,%1,.......,%9

  指令中使用占位符表示操作数,总被视为long型(4个字节),但对其施加的操作根据指令可以使字或者字节,当把操作数当做字或者字节使用时,默认为低字或者低字节。对字节操作可以显示地指明是低字节还是次字节,方法是在%和序号直接插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1

1.2 输出部分

  输出部分描述输出操作数,不同的操作数描述符直接用逗号隔开,每个操作数描述符由限定字符串和C语言变量组成,每个输出操作数的限定字符串必须包含“=”,表示它是一个输出操作数,例如:

_asm_ _volatile_ ("pushf1;pop1 %0;cli":"=g"(x))

  描述字符串表示对该变量的限制条件,这样gcc就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的关系。

1.3 输入部分

  输入部分描述输入操作数,不同的操作数描述符直接用逗号隔开,每个操作数描述符由限定字符串和C语言变量组成,例如:

static _inline_ void _set_bit(int nr,void volatile *addr)
{
  _asm_( \
       "bts1 %1,%0" \
       : "=m" (ADDR) \
       : "Ir" (nr)); \
}

  这个例子的功能是将(*addr)的nr位设为1。第一个占位符%0与C语言变量ADDR对应,第二个占位符%1与C语言变量nr对应。因此上面的汇编语句代码与下面的伪代码等价。

btsl nr ADDR

  该指令的两个操作数不能全是内存变量,因此将nr的限定字符串指定为Ir,将nr与立即数或者寄存器相关联,这样两个操作数中只有ADDR是内存变量。

  注:在内嵌汇编的编写时,通常对这4部分分行编写,并用“\”表示换行。

1.4 限定字符

  作用是指示编译器如何处理其后的C语言变量与指令操作数的关系

分类

限定符

描述

通用寄存器

a

将输入变量放入eax,若eax已被使用,则gcc就会在起始处插入一条语句“push1%eax”,将eax内容保存到堆栈,然后在这段代码结束处再增加一条语句"push1%eax",恢复eax内容

b

将输入变量放入ebx

c

将输入变量放入ecx

d

将输入变量放入edx

s

将输入变量放入esi

d

将输入变量放入edi

q

将输入变量放入eax、ebx、ecx、edx中的一个

r

将输入变量放入通用寄存器,也就是eax、ebx、ecx、edx、esi、edi中的一个

A

把eax和edx合成一个64位的寄存器

内存

m

内存变量

o

操作数为内存变量,但是其寻址方式是偏移量类型,即基址寻址或者基址加变址寻址

V

操作数为内存变量,但寻址方式不是偏移量类型

p

操作数是一个合法的内存地址(指针)

寄存器或内存

g

将输入变量放入eax或ebx或ecx或edx中,或者作为内存变量

x

操作数可以使任何类型

立即数

I

0~31的立即数(用于32位移位指令)

J

0~63的立即数(用于64位移位指令)

N

0~255的立即数(用于out指令)

i

立即数

n

立即数,有些系统不支持除字以外的立即数,这些系统应该使用n而不是i

匹配

0

表示用它限制的操作数与某个指定的操作数匹配

1

表示该操作数就是指定的那个操作数,例如“0”

9

描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为限定符字母的0~9与指令中的“%0”~“%9”的区别,前者描述操作数,后者代表操作数

&

该输出操作数不能使用和输入操作数相同的寄存器

操作数类型

=

操作数在指令中是只写的(输出操作数)

+

操作数在指令中是读写类型的(输入输出操作数)

浮点数

f

浮点寄存器

t

第一个浮点寄存器

u

第二个浮点寄存器

 

G

标准的80387浮点数

%

该操作数可以和下一个操作数交换位置,例如add1的两个操作数可以交换顺序(当然两个操作数都不能是立即数)

#

部分注释,从该字符到其后的逗号之间所有字母忽略

*

表示如果选用寄存器,则其后的字母被忽略

 

1.5 破坏描述部分

  破坏描述符用于通知编译器我们使用了那些寄存器或内存,由逗号隔开的字符串组成,每个字符串描述一种情况,一般是寄存器名;除寄存器外还要“memory”。例如“%eax”、“%ebx”、“memory”等

2 编译器优化

  由于内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存cache,加速对内存的访问。另外在现代CPU中指令的执行并不一定严格按照顺序执行,没有相关性的指令可以乱序执行,以充分利用CPU的指令流水线,提高执行速度,这就是硬件级别的优化。

  软件级别的优化有两种:一种是在编写代码时由程序员优化,另一种是由编译器进行优化。

  编译器优化常用的方法有:将内存变量缓存到寄存器和调整利用整理顺序充分利用CPU的指令流水线,常见的是重新排序读写指令。对常规内存进行优化的时候,这些优化是透明的而且效率很好。

  由编译器优化或者硬件重新排序引起的问题解决办法是以特定顺序执行的操作直接设置内屏障(memory barrier),Linux提供了一个宏用于解决编译器执行的顺序问题。

void Barrier(void)

  这个函数通知编译器插入一个内存屏障,但对硬件无效,编译后的代码会把当前CPU寄存器中的所有修改过的数据存入内存,需要这些数据的时候再从内存中读出。

3 C语言关键字volatile

  C语言关键字volatile(是用来修饰变量而不是上面的_volatile_)表明某个变量的值可能在外部被改变,因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新取出。

  volatile在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程。对应C编译器来说,它并不知道这个值会被其他线程修改,自然就把它cache在寄存器里面。

  注:C编译器是没有线程概念的,这时候就需要用到volatile

  volatile本意是指这个值可能会在当前线程外部被改变,也就是说我们要在threadFunc的intSignal前面加上volatile关键字,这时候编译器知道该变量的值会在外部改变,因此每次访问该变量是会被重新读取。

4 memory描述符

  memory描述符告知gcc以下内容:

  1. 不要将该段内嵌汇编指令与前面的指令重新排序,也就说在执行内嵌汇编代码之前,它前面的指令都执行完毕。

  2. 不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此gcc插入必要的代码先将缓存到寄存器的变量值写回到内存,如果后面又访问这些变量,需要重新访问内存。

  如果汇编指令修改了内存但是gcc本身却察觉不到,因为在输出部分没有描述,此时就需要在修改描述部分增加memory,告诉gcc内存已经被修改,gcc得知这个信息后,就会在这段指令之前插入必要的指令将前面为优化cache而到寄存器中的变量值先写回内存,如果以后又要使用这些变量,则在重新读取。

  当然使用volatile也可以达到这个目的,但是我们在每个变量前增加该关键字,不如使用memory方便。

5 gcc对内嵌汇编语言的处理方式

  (1)变量输入

      根据限定符的内容将输入操作数放入合适的寄存器,如果限定符指定为立即数(i)或内存变量(m),则该步被省略;如果限定符没有具体指定输入操作数类型(如常用的g),gcc会视需要决定是否需要将该操作数输入到某个寄存器。

      这样每个占位符都与某个寄存器、内存变量或立即数形成一一对应的关系。这就是对第二个冒号后的内容的解释。例如:"a"(foo)、"i"(100)、"m"(bar)表示%0对应eax寄存器,%1对应100,%2对应内存变量bar。

  (2)生成代码

      gcc再根据这种一一对应关系(还应包括输出操作符),用这些寄存器、内存变量或立即数来取代汇编代码中的占位符。注意在这一步骤中并不检查由这种取代操作所生成的汇编代码是否合法。例如有这一一条指令:

_asm_("mov1 %0,%1": : "m"(foo),"m"(bar));

      如果用户使用“gcc-c-S”选项编译该源文件,那么在生成的汇编文件中用户将会看到生成了“movl foo, bar”这样一条指令,这显然是错误的。这个错误在稍后的编译检查中会被发现。

  (3)变量输出

      按照输出限定符的指定将寄存器的内容输出到某个内存变量中,如果输出操作数的限定符指定为内存变量(m),则该步被省略。这就是对第一个冒号后的内容的解释。例如

_asm_("mov1 %0,%1":"=m"(foo),"=a"(bar):);

      编译后为:

#APP
    mov1 foo,eax
#NO_APP
    mov1 eax,bar

      这句很好的体现了gcc的运作方式

6 实例

_asm_(
"push1 %%edi\n\t"
"push1 %%ebp\n\t"
"lcall %%cs\n\t"
"setc %%al\n\t"
"add1 %1,%2\n\t"
"pop1 %%ebp\n\t"
"pop1 %%edi\n\t"
: "=a"(ea),"=b"(eb),
  "=c"(ec),"=d"(ed),"=S"(es)
: "a"(eax_in),"b"(ebx_in),"c"(ecx_in)
: "memory","cc");

编译后的汇编代码为:

    mov1 eax_in ,%eax
    mov1 ebx_in ,%ebx
    mov1 ecx_in ,%ecx
#APP
    push1 %edi
    push1 %ebp
    lcall    %cs:
    setc    %al
    add1   eb,ec
    pop1   %ebp
    pop1   %edi
#NO_APP
    mov1 %eax, ea
    mov1 %ebx, eb
    mov1 %ecx, ec
    mov1 %edx, ed
    mov1 %esi,  es

 7 格式

  输出部分:使用汇编语言时,可能把C语言的变量修改了,变量就放到输出部分

  输入部分:使用汇编语言时,需要C语言中去一些值作为参数,这些参数就是放到输入部分

  破坏描述部分:如果使用汇编时某些寄存器的值被修改了,就需要把这些寄存器列到破坏描述部分。如r0;破坏就是修改的意思

例1:

void write_cp15_c1(unsigned long value)
{
       __asm__(
       "mcr p15,0,%0,c1,c0,0\n"
       :
       :"r"(value)   //编译器选择一个r*寄存器
       );
}

  CP15写入值,%0表示一个0号参数,要写入到c1中;没有输出参数;r表示通用寄存器,系统自己选具体哪一个;value的值赋值给通用寄存器,然后写入c1;

  凡是读的参数都放到输入部分,写的参数都放到输出部分;

例2:

unsigned long read_cp15_c1(void)
{
        long value;
        __asm__(
       "mrc p15,0,%0,c1,c0,0\n"
       :"=r"(value)        //"="表示只写操作,用于输出
      :
      :"memory"
       );
       return value;
}    

  memory表示内存的值被修改,value在栈上,栈就在内存中

例3:优化

unsigned long old;
unsigned long temp;
__asm__"volatile"(
      "mrs %0,cpsr \n"
      "orr %1,%0,#128\n"
      "msr cpsr_s,%1\n"
      :"=r"(old),"=r"(temp)
      :
      :"memory"
);

  使用volatile告诉编译器,不要对接下来的这部分代码进行优化

posted @ 2019-03-08 09:36  dongry  阅读(1318)  评论(0)    收藏  举报