GNU C 内联汇编

内核代码中有很多内联汇编,为了更好的读懂内核,上网搜罗了一些内联汇编的文章在这里,方便自己随时查阅。这篇文章比较有价值,特别是后半段引用的中国科大BBS站的一些邮件列表,“对 《gcc中的内嵌汇编语言》一文的补充说明”中的一些例子描述的很好,能够帮助更好的理解内联汇编。这里摘抄一些。

参考二 参考三

基本形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__asm__ __volatile__(
    "xorps %%xmm0,%%xmm1\n\t"
    "1:\n\t"
    "movdqa %%xmm0,%%xmm1\n\t"
    "pcmpgtd (%1),%%xmm1\n\t"
    "andps %%xmm1,%%xmm0\n\t"
    "andnps (%1),%%xmm1\n\t"
    "orps %%xmm1,%%xmm0\n\t"
    "addq $16,%1\n\t"
    "subq $4,%2\n\t"
    "jnz 1b\n\t"
    "movdqu %%xmm0,(%3)\n\t"
    "movl (%3),%%eax\n\t"
    "cmpl 4(%3),%%eax\n\t"
    "cmovll 4(%3),%%eax\n\t"
    "cmpl 8(%3),%%eax\n\t"
    "cmovll 8(%3),%%eax\n\t"
    "cmpl 12(%3),%%eax\n\t"
    "cmovll 12(%3),%%eax\n\t"
    "movl %%eax,%0\n\t"
    :"=m"(mx)  //输出部分
    :"r"(a),"r"((long long)N),"r"(tmp) //输入部分
    :"eax");  //clobber

GNU C 内联汇编以关键字__asm__开头,表示后边部分为汇编代码; __volatile__表示告诉编译器,不要优化后边的代码,严禁将后边的汇编语句和其他语句进行重组优化。

  • 寄存器使用%%开头;
  • 立即数使用$开头;
  • %0 %1 %2用来引用输入输出部分的内容;

内联汇编一共分为四部分 : instruction \ output operand\ input operand \ clobber不同部分之间以冒号分割。

  • instruction : 是汇编指令,每条指令最好以"\n\t"结尾;
  • output operand : 是输出部分。 每个输出部分使用 , (逗号)分隔. “=“作为修饰符, “m"是约束符,表示存放位置, ()里面表示对应C程序变量。例如: 。
  • input operand : 是输入部分。 和输出部分规则一样。例如:。
  • clobber : 这个部分是告诉gcc在这条指令里面我们会修改什么值.

操作数限定字符

操作数限定字符串中利用规定的限定字符来描述相应的操作数,一些常用的限定字符有:(还有一些没有涉及的限定字符,参见gcc.info)

  • “m”:操作数是内存变量。

  • “o”:操作数是内存变量,但它的寻址方式必须是“偏移量”类型的, 也就是基址寻址或者基址加变址寻址。

  • “V”:操作数是内存变量,其寻址方式非“偏移量”类型。

  • " “:操作数是内存变量,其地址自动增量。

  • “r”:操作数是通用寄存器。

  • “i”:操作数是立即操作数。(其值可在汇编时确定)

  • “n”:操作数是立即操作数。有些系统不支持除字(双字节)以外的 立即操作数,这些操作数要用"n"而不是"i"来描述。

  • “g”:操作数可以是立即数,内存变量或者寄存器,只要寄存器属 于通用寄存器。

  • “X”:操作数允许是任何类型。

  • “0”,“1”,…,“9”:操作数与某个指定的操作数匹配。也就是说, 该操作数就是指定的那个操作数。例如,如果用"0"来描述”%1"操作 数,那么”%1"引用的其实就是”%0"操作数。

  • “p”:操作数是一个合法的内存地址(指针)。

  • “=":操作数在指令中是只写的(输出操作数)。

  • “+":操作数在指令中是读-写类型的(输入-输出操作数)。

  • “a”:寄存器EAX。

  • “b”:寄存器EBX。

  • “c”:寄存器ECX。

  • “c”:寄存器ECX。

  • “d”:寄存器EDX。

  • “q”:寄存器"a”,“b”,“c"或者"d”。-

  • “A”:寄存器"a"或者"d”。

  • “f”:浮点数寄存器。

  • “t”:第一个浮点数寄存器。

  • “u”:第二个浮点数寄存器。

  • “D”:寄存器di。

  • “S”:寄存器si。

  • “I”:0-31之间的立即数。(用于32位的移位指令)

  • “J”:0-63之间的立即数。(用于64位的移位指令)

  • “N”:0-255之间的立即数。(用于"out"指令)

  • “G”:标准的80387浮点常数。

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

gcc在编译内嵌汇编语言时,采取的步骤如下

  • 变量输入: 根据限定符的内容将输入操作数放入合适的寄存器,如果限定符指定为立即数(“1i”)或内存变量(“m”),则该步被省略,如果限定符没有具体指定输入操作数的类型(如常用的"g”),gcc会视需要决定是否将该操作数输入到某个寄存器.这样每个占位符都与某个寄存器,内存变量,或立即数形成了一一对应的关系.这就是对第二个冒号后内容的解释.如::"a"(foo),"i"(100),"m"(bar)表示%0对应eax寄存器,%1对应100,%2对应内存变量bar.
  • 生成代码: 然后根据这种一一对应的关系(还应包括输出操作符),用这些寄存器,内存变量,或立即数来取代汇编代码中的占位符(则有点像宏操作),注意,则一步骤并不检查由这种取代操作所生成的汇编代码是否合法,例如,如果有这样一条指令asm("movl %0,%1"::"m"(foo),"m"(bar));如果你用gcc -c -S选项编译该源文件,那么在生成的汇编文件中,你将会看到生成了movl foo,bar这样一条指令,这显然是错误的.这个错误在稍后的编译检查中会被发现.
  • 变量输出: 按照输出限定符的指定将寄存器的内容输出到某个内存变量中,如果输出操作数的限定符指定为内存变量(“m”),则该步骤被省略.这就是对第一个冒号后内容的解释, 如:asm("mov %0,%1":"=m"(foo),"=a"(bar):);编译后为
        #APP
        movl foo,eax
        #NO_APP
        movl eax,bar
    

该语句虽然有点怪怪的,但它很好的体现了gcc的运作方式.