Linux 0.12 版本 mm/swap.c
中有一个 bitop
宏用到了 bt/bts/btr
这三个指令:
#define bitop(name,op) \
static inline int name(char * addr,unsigned int nr) \
{ \
int __res; \
__asm__ __volatile__("bt" op " %1,%2; adcl $0,%0" \
:"=g" (__res) \
:"r" (nr),"m" (*(addr)),"0" (0)); \
return __res; \
}
bitop(bit,"")
bitop(setbit,"s")
bitop(clrbit,"r")
代码从第 11 行到第 13 行看起,可见预处理之后将产生三个函数,分别为 bit
,setbit
和 clrbit
。这三个函数都有两个参数,一个是地址,一个是位偏移。
下面具体说下这三条指令,毕竟这篇博文是针对这三条指令的,搜索资料和测试也花了我不少时间,所以就记录下:
bt
表示 Bit Test,测试并用原值设置进位值bts
表示 Bit Test and Set,设置比特位(设为 1)并用原值设置进位值btr
表示 Bit Test and Reset,复位比特位(设为 0)并用原值设置进位值bitop
宏也可以看得出来,因此这里只以 bts
为例。下面以 AT&T 的汇编语法介绍 bts
。bts
有两个操作数:bts %1,%2
,其中第一个操作数是位偏移 nr
,第二个操作数是位串。譬如某个 int
类型,它的值为 0x3f
,那么它的位串就是 0011,1111
,位偏移是从 0 开始计数的,如这个位串第 5 位是 1,第 6 位是 0。似乎很简单,迷惑的点主要是上面 bitop
代码的第 7 行,从参数来看,addr
是个 char
类型的数,占 1 个字节,即 8 个比特位,对其解引用,取出的当然就是一个占 8 个比特位的数了,按理说其最大位偏移就是 7,但是在使用如 setbit
函数时,位偏移是可以超过 7 的。这点感觉很迷惑,然后就查资料,写测试用例了。以下说明的 gcc
版本如下(win10 WSL 环境):这个主要和其位串操作数可以是寄存器,内存相关:
reg.c
#include
int main()
{
int val = 0xAB, nr, ret;
while ((scanf("%d", &nr)) == 1) {
__asm__ __volatile__("bts %1,%2; adcl $0,%0"
:"=g" (ret) \
:"r" (nr), "r"(val),"0" (0));
printf("ret is %d, val is 0x%X\n", ret, val);
}
return 0;
}
这里解释下第 6 行代码的 adcl
,这个指令是做加法,但是它会加上进位 CF
(Carry Flag) 的值,这里第 9 行将 ret
的初始值设置为了 0(“0” 表示和输出寄存器的第一个寄存器一样), adcl $0,%0
即是 0 + %0 + CF
得到的结果放到 %0
中,所以最终 %0
中的值就是 CF
的值,而 bt
系列指令又将测试比特位原先的值放在了 CF
中,所以 ret
的值就是原先比特位的值了。在测试用例中,设置位串为 0xAB
,即 1010,1011
,编译命令为:
gcc reg.c
,运行结果如下:
可以看出确实是环绕,不过有一点发现 val
的值一直是 0xAB
,没有因为 bts
指令而改变,这是为什么呢?这是因为在 C 嵌入式汇编中我们不能保证 val
的值一直用同一个寄存器,更重要的是不能保证 bts
命令测试 val
时没有将 val
的值从一个寄存器挪到另一处,但是并没有挪回来,这点可以从反汇编中看出来。(感兴趣的同学可以自己反汇编)。
2. 当位串操作数是在于内存中时,其位偏移值在我的操作系统上(win10,64 位)上超过 31 则延续在位串地址的下一位,如 32 就是在内存的 32 位。测试用例如下:
mem.c
#include
#include
#include
#define bitop(name,op) \
static inline int name(char * addr,unsigned int nr) \
{ \
int __res; \
__asm__ ("bt" op " %1,%2; adcl $0,%0" \
:"=g" (__res) \
:"r" (nr), "m"(*(addr)),"0" (0)); \
return __res; \
}
bitop(setbit,"s")
int main()
{
char *c = malloc(512);
int ret;
memset(c, 0, 512);
c[0] = 0x10;
printf("before setbit c[0] is 0x%X\n", c[0]);
ret = setbit(c, 2);
printf("after setbit c[0] is 0x%X, ret is %d\n", c[0], ret);
printf("before setbit c[511] is 0x%X\n", c[511]);
ret = setbit(c, 4088);
printf("after setbit c[511] is 0x%X, ret is %d\n", c[511], ret);
return 0;
}
编译命令为 gcc mem.c
,运行结果如下:
但是有意思的时当编译命令为 gcc mem.c -O2
时,运行结果如下(至少我的机器运行出来是这样):
当 O2
优化后,setbit
似乎也没有设置位成功,但其实是有设置成功了,只不过编译器将 c[0]
的值提前放到了一个寄存器中,但是 setbit
后,编译器没有意识到 c[0]
的值已经改变了,所以将之前的值给打印了出来,用 c[0]
做下加减法,你会发现编译器又重新从内存中取出正确的 c[0]
了,c[511]
也是如此。因此为了防止编译器做这些优化,要将第 11 行代码改成 __asm__ __volatile__
的形式。