理解 位域

 

有很多人对于位域的理解模糊,我用编译器代码来阐述它,希望对大家有些帮助
 
CODE:

struct{
    short a:4;
    short b:5;
    short c:7;
}t;

int main()
{
    t.a = 1;
    t.b = 2;
    t.c = 3;
   
    printf(size: %d/n, sizeof(t));
    printf(%d/n, t.a);
    printf(%d/n, t.b);       
    printf(%d/n, t.c);

    return 0;
}

我们看看编译器是怎样处理位域的,以下是例1 objdump出来的代码,(main()的主要代码)


 
CODE:
t.a = 1;  是这样的

80483a0:        0f b7 05 d4 96 04 08         movzwl 0x80496d4,%eax
80483a7:        66 89 45 e8                  mov    %ax,0xffffffe8(%ebp)
80483ab:        8b 45 e8                     mov    0xffffffe8(%ebp),%eax
80483ae:        83 e0 f0                     and    $0xfffffff0,%eax
80483b1:        83 c8 01                     or     $0x1,%eax
80483b4:        66 89 45 e8                  mov    %ax,0xffffffe8(%ebp)
80483b8:        8b 45 e8                     mov    0xffffffe8(%ebp),%eax
80483bb:        66 a3 d4 96 04 08            mov    %ax,0x80496d4

 


 
CODE:
1、movzwl 0x80496d4, %eax 

取得 t 值放在 eax 中,在这里可以看出,sizeof(t)为2, 也就是 word size

CODE:
2、and $0xfffffff0, %eax
or $0x1, %eax

保留低4位, 然后置为1


 
CODE:
3、mov %ax, 0x80496d4

回写 t 值,t.a 定义为4个位,经过这3步,从而使 t 的低4位置为1。


 
CODE:
t.b = 2;

80483c1:        0f b7 05 d4 96 04 08         movzwl 0x80496d4,%eax
80483c8:        66 89 45 d8                  mov    %ax,0xffffffd8(%ebp)
80483cc:        8b 45 d8                     mov    0xffffffd8(%ebp),%eax
80483cf:        25 0f fe ff ff               and    $0xfffffe0f,%eax
80483d4:        83 c8 20                     or     $0x20,%eax
80483d7:        66 89 45 d8                  mov    %ax,0xffffffd8(%ebp)
80483db:        8b 45 d8                     mov    0xffffffd8(%ebp),%eax
80483de:        66 a3 d4 96 04 08            mov    %ax,0x80496d4

除了第2步外,其它都一样的,我们看看第2步


 
CODE:
and $0xfffffe0f, %eax
or $0x20, %eax

作用是:保留 bit8-bit4 共5个位,
0x20也就是 0000 0000 0010 0000 所以结果将bit8-bit4置为2


 
CODE:
t.c = 3; 又如何呢?

80483e4:        0f b7 05 d4 96 04 08         movzwl 0x80496d4,%eax
80483eb:        66 89 45 c8                        mov    %ax,0xffffffc8(%ebp)
80483ef:         8b 45 c8                             mov    0xffffffc8(%ebp),%eax
80483f2:         25 ff 01 00 00                    and    $0x1ff,%eax
80483f7:         0d 00 06 00 00                   or     $0x600,%eax
80483fc:          66 89 45 c8                        mov    %ax,0xffffffc8(%ebp)
8048400:        8b 45 c8                              mov    0xffffffc8(%ebp),%eax
8048403:        66 a3 d4 96 04 08              mov    %ax,0x80496d4

 

CODE:
and $0x1ff, %eax
or %0x600, %eax

由于 t 是一个 word, 所以,保留了bit15-bit9 共7个位,
0x600 也就是 0000 0110 0000 0000,所以,结果是将 bit15-bit9 置为3

 

那么接下来的问题是,编译是如何读取这些值呢?
来看看以下就知道了:


 
CODE:
printf(%d/n, t.a); 是这样的:

804841d:        0f b6 05 d4 96 04 08         movzbl 0x80496d4,%eax
8048424:        c0 e0 04                     shl    $0x4,%al
8048427:        c0 f8 04                     sar    $0x4,%al
804842a:        0f be c0                     movsbl %al,%eax
804842d:        89 44 24 04                  mov    %eax,0x4(%esp)
8048431:        c7 04 24 c2 85 04 08         movl   $0x80485c2,(%esp)
8048438:        e8 7b fe ff ff               call   80482b8 printf@plt

 

CODE:
1、movzbl 0x80496d4, %eax

在这里,编译器是以 byte 的方式来读取 t 值,也就是读取的是 t的低8位
 
CODE:
2、shl $0x4, %al
sar $0x4, %al

在这里,大家是不是觉得有点奇怪,先向左移4位,再向右移4位,不是等于没移吗?
当然不是啦,这里的主要目的是:将 %al 的低4位的符号位扩展到高4位,这一步是必要的,否则会出错的!


CODE:
3、movsbl %al, %eax

经过上面的二个位移,保持了正确性,然后才能扩展到 %eax 去。才能得到正确的低4位值!


4、再下来就是输出 低4位的值了。J

CODE:
printf(%d/n, t.b); 是这样的:

804843d:        0f b7 05 d4 96 04 08         movzwl 0x80496d4,%eax
8048444:        c1 e0 07                     shl    $0x7,%eax
8048447:        98                           cwtl  
8048448:        c1 f8 0b                     sar    $0xb,%eax
804844b:        0f be c0                     movsbl %al,%eax
804844e:        89 44 24 04                  mov    %eax,0x4(%esp)
8048452:        c7 04 24 c2 85 04 08         movl   $0x80485c2,(%esp)
8048459:        e8 5a fe ff ff               call   80482b8 printf@plt

CODE:
1、movzwl 0x80496d4, %eax

在这里以word的方式读取 t 值。因为接下来要处理的数据已经超出了byte的范围
 
CODE:
2、shl $0x07, %eax
cwtl
sar $0xb, %eax

向左移动7位,将word扩展到double word 从保留了 bit0-bit8 数据不变,
再向右带符号位移11位,从而去掉低4位。
这里几条代码的目的就是,取得bit4-bit8 位的数据
 
CODE:
3、movsbl %al, %eax

带符号位的移动,从而获得正确的第 bit4-bit8 的值。

4、接下来是输出值。
 
CODE:
最后:printf(%d/n, t.c);

804845e:        0f b6 05 d5 96 04 08         movzbl 0x80496d5,%eax
8048465:        d0 f8                        sar    %al
8048467:        0f be c0                     movsbl %al,%eax
804846a:        89 44 24 04                  mov    %eax,0x4(%esp)
804846e:        c7 04 24 c2 85 04 08         movl   $0x80485c2,(%esp)
8048475:        e8 3e fe ff ff               call   80482b8 printf@plt
 
CODE:
1、movzbl 0x80496d5, %eax

在这里,gcc 用了一个挺好的办法,就是先将 t的地址值 加了1(0x80496d4 + 1),然后再以 byte的方式读取,也就是下一个字节的值。
 
CODE:
2、sar %al
movsbl %al, %eax

由于已经得到了下一个字节的值,所以简得地向右移一位就得到了原来 t 值 bit15-bit9 的值

总结一下:
例子中,t 值的内存布局为:

0000 000     0 0000     0000
  t.c          t.b        t.a

如果,要读取每个位域值时,必须将位域转化为 整数值,即经过位移组合而成

至于,像 *(short *)&t 这种操作,大家应该不难理解吧。答案是:0x0621
 
现在更进一步了解位域!

将例1改一改,如下:

例二:

CODE:
struct {
        short a:4;
        short b:5
        short c:9;
} t;

int main() {
        t.a = 1;
        t.b = 2;
        t.c = 3;

        printf(size: %d/n, sizeof(t));
        printf(%d/n, t.a);
        printf(%d/n, t.b);
        printf(%d/n, t.c);
}

代码将c域 设为9, 这样t值就超出了 word size。

我们出看一看,编译器c域的处理是怎样的


 
CODE:
t.c = 3;

mov 0x80496d4, %eax
and $0xfe00ffff, %eax
or $0x30000, %eax
mov %eax, 0x80496d4

在这里可以看出:sizeof(t) 是等于 4, 因为a,b,c 三个域加起来已经超出了 short 的表示范围,t则护展为 4 个字节

and $0xfe00ffff, %eax
or $0x30000, %eax

这两条语句保留了 bit24 – bit16 共 9个位,然后将这块区域置为 3

现在,我们来看看,这时候 t 的内存布局

0000 000          0 0000 0000     0000 000     0 0000      0000
--------         --------------  -----------     --------      -------
(未用)                   t.c             (未用)             t.b          t.c


因此,此时的c域跨越 short ( 2 bytes) 界限时,它将越过 short而在下一字节开辟空间!

再来改一改:
 
CODE:
struct {
        short a:4;
        short b:5
        char c:9;
} t;

int main() {
        t.a = 1;
        t.b = 2;
        t.c = 3;

        printf(size: %d/n, sizeof(t));
        printf(%d/n, t.a);
        printf(%d/n, t.b);
        printf(%d/n, t.c);
}

将 c 改为char 型,c域将会产生溢出

如果,将 c 定义为: short c:1; 那么,t.c = 3;  c 域也将产生溢出

如果,我们企图对域进行以下操作,会怎样呢?

 
CODE:
printf(%p/n, &t.a);
printf(%p/n, &t.b);

编译器将产生错误!位域没有地址可取


接下来,我们用一般的int型变量值去代替位域,这样做法的好处是:我们直接控制位域,更加灵活!坏处是:我们需要手工去操作读取位域值,而使用位域结构,编译器帮我们代劳了!

我们用:
 
CODE:
#define get_FIELD_A(x) ((x) & 0x0f)
#define get_FIELD_B(x) (((x)>>4) & 0x1f)
#define get_FIELD_C(x) (((x)>>16) & 0x1ff)

#define set_FIELD_A(x, n) (((x) & 0xfffffff0) | n)
#define set_FIELD_B(x, n) (((x) & 0xfffffe0f) | ((n) << 4))
#define set_FIELD_C(x, n) (((x) & 0xfe00ffff) | ((n) << 16))

来取代:
 
CODE:
struct {
        short a:4;
        short b:5:
        short c:7
} t;

就可以像以下这样使用它们
 
CODE:
int main()
{
i =  set_FIELD_A(0, 1) | set_FIELD_B(0, 2) | set_FIELD_C(0, 3);

printf(%d/n, get_FIELD_A(i));
printf(%d/n, get_FIELD_B(i));
printf(%d/n, get_FIELD_C(i));
}

当然,在以上的代码中,有两个致命错误:1、n 值范围不能控制,如果超过位域值范围的话,它检测不到!2、n为负数是会出错!

我们需要进行改良!n 值范围恐怕不能在代码级进行制,只能人为控制了。n 值为负时恐怕也不好处理,因此,如果需要人工操作位域时,像以上的宏定义并不是个好办法,另一个变通的方法是定义

get/set 的inline 函数,在函数中进行控制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值