c语言内嵌汇编代码之Clobbers的用途到底是什么

2019-10-14 16:29:09 浏览数 (1)

在阅读本文之前,请先阅读gcc的相关文档,确保对如何在c中使用汇编语言有个基本的认识。

文档地址为:

https://gcc.gnu.org/onlinedocs/gcc-9.2.0/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C


1. Clobbers 是一个以逗号分隔的寄存器列表(该列表中还可以存放一些特殊值,用于表示一些特殊用途)。

2. 它的目的是为了告知编译器,Clobbers 列表中的寄存器会被该asm语句中的汇编代码隐性修改。

3. 由于 Clobbers 里的寄存器会被asm语句中的汇编代码隐性修改,编译器在为 input operands 和 output operands 挑选寄存器时,就不会使用 Clobbers 里指定的寄存器,这样就避免了发生数据覆盖等逻辑错误。

4. 通俗来讲,Clobbers 的用途就是为了告诉编译器,我这里指定的这些寄存器在该asm语句的汇编代码中用了,你在编译这条asm语句时,如果需要用到寄存器,别用我这里指定的这些,否则就都乱了。

5. Clobbers 里的特殊值可以为 cc,用于表示该平台的 flags 寄存器会被隐性修改(比如 x86 平台的 eflags 寄存器)。

6. Clobbers 里的特殊值也可以为 memory,用于表示某些内存数据会被隐性使用或隐性修改,所以在执行这条asm语句之前,编译器会保证所有相关的、涉及到内存的寄存器里的内容会被刷到内存中,然后再执行这条asm语句。在执行完这条asm语句之后,这些寄存器的值会再被重新load回来,然后再执行这条asm语句后面的逻辑。这样就保证了所有操作用到的数据都是最新的,是正确的。

下面看个例子:

代码语言:javascript复制
#include <stdio.h>

int inc1(int src) {
  int dst;

  asm("mov %1, %0nt"
      "add $1, %0"
      : "=r"(dst)
      : "r"(src));

  return dst;
}

int inc2(int src) {
  int dst;

  asm("mov %1, %0nt"
      "mov $3, %�xnt"
      "add $1, %0"
      : "=r"(dst)
      : "r"(src));

  return dst;
}

int inc3(int src) {
  int dst;

  asm("mov %1, %0nt"
      "mov $3, %�xnt"
      "add $1, %0"
      : "=r"(dst)
      : "r"(src)
      : "�x");

  return dst;
}

int main(int argc, char *argv[]) {
  printf("inc1: %dn", inc1(1));
  printf("inc2: %dn", inc2(1));
  printf("inc3: %dn", inc3(1));
}

上面代码中三个inc方法的意图都是对src参数加1,然后再返回,所以理论上来说,最终的输出会是三个2。

但真是这样吗?让我们来运行看看:

代码语言:javascript复制
$ gcc main.c && ./a.out
inc1: 2
inc2: 4
inc3: 2

inc2方法居然返回的不是2,而是4,奇怪吧。但为什么呢,让我们反编译看下。

先看inc1方法:

代码语言:javascript复制
$ gcc -O3 main.c && objdump --disassemble=inc1 a.out
0000000000001190 <inc1>:
    1190:  89 f8                  mov    �i,�x
    1192:  83 c0 01               add    $0x1,�x
    1195:  c3                     retq

以汇编角度看,这个方法没什么问题,就是先把src的值拷贝到eax寄存器中,然后再将eax寄存器的值加1,最后将eax中的结果返回给上层。

再看inc2方法:

代码语言:javascript复制
$ gcc -O3 main.c && objdump --disassemble=inc2 a.out
00000000000011a0 <inc2>:
    11a0:  89 f8                  mov    �i,�x
    11a2:  b8 03 00 00 00         mov    $0x3,�x
    11a7:  83 c0 01               add    $0x1,�x
    11aa:  c3                     retq

从汇编代码角度就看出这个方法的问题了,我们在inc2方法里加入的汇编代码mov $3, �x里使用到了eax寄存器,而inc2方法里的asm语句中的其他汇编代码用到的寄存器居然也是eax,这样就导致了我们加入的mov语句把eax里原来的值给覆盖掉了,所以最终返回了4,而不是2。

但是,我们既然已经在汇编代码里用到了eax寄存器,为什么gcc还会分配eax给其他汇编代码用呢?

这是因为,gcc在编译时,根本就不会分析asm里的汇编代码,所以它也就不知道我们已经使用了eax寄存器,所以才导致的最终冲突。

那我们怎样才能告诉gcc,我们已经用了eax寄存器,让它别再用了呢?

对,就是通过 Clobbers。

看下上面的inc3方法,它在Clobbers字段位置指定了eax,即告知gcc,eax寄存器已经被我们用了,你就不要再用了,所以inc3方法返回的结果是正确的。

看下inc3的汇编代码再确认下:

代码语言:javascript复制
$ gcc -O3 main.c && objdump --disassemble=inc3 a.out
00000000000011b0 <inc3>:
    11b0:  41 89 f8               mov    �i,%r8d
    11b3:  b8 03 00 00 00         mov    $0x3,�x
    11b8:  41 83 c0 01            add    $0x1,%r8d
    11bc:  44 89 c0               mov    %r8d,�x
    11bf:  c3                     retq

确实和我们想的是一样的。

好,到这里我相信大家应该对Clobbers字段的用途都明白了,本文到这里也就结束了。

希望对你们有所帮助。

0 人点赞