GCC扩展内联汇编clobber列表正确编写方法相关疑问
GCC内联汇编clobber列表相关疑问解答
学习背景
近期通过GCC 7.5.0官方扩展汇编文档以及过往相关问题学习了clobber的用法,参考资料中对clobber的核心描述如下:
clobber列表的作用是告知编译器,列表内的寄存器会被asm语句中的汇编代码隐式修改。由于clobber列表内的寄存器会被汇编代码隐式修改,编译器为输入、输出操作数选择寄存器时,不会选用clobber列表中指定的寄存器,以此避免数据覆盖等逻辑错误。
在实际编码使用中,存在若干实操层面的疑问,结合示例整理如下。
核心疑问
疑问1:显式指定操作数寄存器后是否还需要编写clobber列表
先看一组对比示例代码:
#include <stdio.h> int inc2(int src) { int dst; asm("mov %1, %0\n\t" "mov $3, %%eax\n\t" "add $1, %0" : "=r"(dst) : "r"(src)); return dst; } int inc3(int src) { int dst; asm("mov %1, %0\n\t" "mov $3, %%eax\n\t" "add $1, %0" : "=r"(dst) : "r"(src) : "%eax"); return dst; } int main(int argc, char *argv[]) { printf("inc2: %d\n", inc2(1)); printf("inc3: %d\n", inc3(1)); }
上述代码运行后inc2输出2,inc3输出4,原因是inc2里的mov $3, %eax直接修改了eax,而编译器给输入操作数分配的寄存器恰好就是eax,导致输入值被覆盖出错。
那如果显式绑定所有输入输出操作数的寄存器,是不是就完全不需要写clobber?比如下面的inc4实现:
int inc4(int src) { int dst; asm("mov %%rcx, %%rax\n\t" "add $1, %%rax" : "=a"(dst) : "c"(src) : ); return dst; }
这里显式指定输入用rcx、输出用rax,这种写法是不是可以完全省略clobber?还是说汇编里修改过的所有寄存器都必须加到clobber列表里?
疑问2:开源代码中clobber列表的写法逻辑
在阅读广泛使用的开源代码时,发现部分内联汇编的clobber写法和初步理解有偏差,以下面两个函数为例:
static inline uint32_t rdtscp() { uint32_t rv; asm volatile ("rdtscp": "=a" (rv) :: "edx", "ecx"); return rv; } static inline uint32_t memaccesstime(void *v) { uint32_t rv; asm volatile ( "mfence\n" "lfence\n" "rdtscp\n" "mov %%eax, %%esi\n" "mov (%1), %%eax\n" "rdtscp\n" "sub %%esi, %%eax\n" : "=&a" (rv): "r" (v): "ecx", "edx", "esi"); return rv; }
具体疑问点:
- 为什么这两个函数的clobber列表都包含edx和ecx?
- 是否因为输出用
=a约束绑定了eax,所以clobber里不需要加eax? - 第一个
rdtscp函数没有输入参数,只有一个绑定到eax的输出,标记edx、ecx的意义是什么?
解答
针对疑问1的解答
核心规则非常明确:所有被汇编代码修改、且没有被声明为输出操作数(含显式绑定输出、内存输出)的寄存器,都必须加入clobber列表,和是否显式指定操作数使用的寄存器没有直接关系。
- 你写的
inc4示例clobber为空是完全合法的:这段汇编仅修改了rax寄存器,而rax已经通过=a约束绑定为输出寄存器;rcx仅被读取源值、没有被修改,不存在其他被改动的寄存器,因此不需要额外添加clobber项。 - 要注意一个高频踩坑点:哪怕你通过约束显式绑定了输入寄存器,只要你在汇编里修改了这个输入寄存器,要么把它改成输入输出操作数(比如用
+c代替c约束),要么就必须把它加到clobber列表。编译器默认输入寄存器在asm语句执行前后值保持不变,如果你修改了寄存器又不告知编译器,会直接导致寄存器值状态混乱,产生难以排查的错误。 - 之前的
inc2出错、inc3正常的原因也完全符合这个规则:inc2里汇编修改了eax,但eax既没有被声明为输出,也没有加入clobber列表,编译器可能把需要保留的输入值放在eax中,被mov $3, %%eax直接覆盖导致结果错误;inc3把eax加入clobber后,编译器就不会把任何需要跨asm语句保留的值放在eax中,自然避免了覆盖问题。
针对疑问2的解答
你观察到的规律是正确的:如果寄存器已经被绑定为输出操作数,就不需要再放到clobber列表里,编译器已经明确知道这个寄存器会被修改。
rdtscp函数需要clobber edx和ecx的原因很直接:根据x86指令集规范,rdtscp指令执行后,会把时间戳计数的低32位存在eax、高32位存在edx,同时把IA32_TSC_AUX寄存器的值存在ecx。也就是说这条指令会隐式修改edx和ecx,哪怕你的代码里没用到这两个寄存器的值,这两个寄存器也被指令改写了,必须告知编译器。否则编译器可能在执行rdtscp之前把某个要保留的值放在edx或者ecx里,指令执行后值就被冲掉,会引发逻辑错误。这段代码里只用到了eax存储的低32位返回值,所以只把eax绑定为输出,edx、ecx是被指令隐式修改、代码又不使用其值的寄存器,自然要放到clobber列表里。- 第二个
memaccesstime函数的clobber逻辑完全一致:- 函数内两次调用
rdtscp,每次都会隐式改写ecx、edx,这两个寄存器没有被用来存储输入输出值,所以必须加入clobber列表; - 汇编中用
mov %%eax, %%esi把第一次读取的tsc低32位临时存在esi里,供后续计算差值使用,esi既不是输入也不是输出,但确实被汇编代码修改了,所以也要加到clobber列表里; - 输出已经通过
=&a约束绑定了eax,所以eax不需要加到clobber中。这里约束里的&是早期破坏标记,作用是告知编译器:eax这个输出寄存器会在asm语句用完所有输入操作数之前就被修改,不要把输入操作数分配到eax里,避免输入值被提前覆盖。
- 函数内两次调用
内容的提问来源于stack exchange,提问作者Gerrie
相关产品推荐
相关产品推荐

