bt/bts/btr 指令

起因

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 行看起,可见预处理之后将产生三个函数,分别为 bitsetbitclrbit。这三个函数都有两个参数,一个是地址,一个是位偏移。

bt/bts/btr 指令

下面具体说下这三条指令,毕竟这篇博文是针对这三条指令的,搜索资料和测试也花了我不少时间,所以就记录下:

  1. bt 表示 Bit Test,测试并用原值设置进位值
  2. bts 表示 Bit Test and Set,设置比特位(设为 1)并用原值设置进位值
  3. 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 环境):
    bt/bts/btr 指令_第1张图片

这个主要和其位串操作数可以是寄存器,内存相关:

  1. 当位串操作数是寄存器时,其位偏移值在我的操作系统上(win10,64 位)上超过 31 则环绕(对 32 取模,指令操作数大小默认 32 位,所以理应位偏移值最大应为 31),如 32 相当于 0,33 相当于 1。测试用例如下:
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,运行结果如下:
bt/bts/btr 指令_第2张图片
可以看出确实是环绕,不过有一点发现 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,运行结果如下:
mem O0.jpg
但是有意思的时当编译命令为 gcc mem.c -O2 时,运行结果如下(至少我的机器运行出来是这样):
mem O2.jpg
O2 优化后,setbit 似乎也没有设置位成功,但其实是有设置成功了,只不过编译器将 c[0] 的值提前放到了一个寄存器中,但是 setbit 后,编译器没有意识到 c[0] 的值已经改变了,所以将之前的值给打印了出来,用 c[0] 做下加减法,你会发现编译器又重新从内存中取出正确的 c[0] 了,c[511] 也是如此。因此为了防止编译器做这些优化,要将第 11 行代码改成 __asm__ __volatile__ 的形式。

你可能感兴趣的:(汇编,IA,汇编,linux)