写宏定义:得到一个field在结构体(struct type)中的偏移量[转]

时间:2022-06-06 19:08:59

http://hi.baidu.com/tian_20032242/blog/item/77fd7afa5ffcc29d59ee90ba.html


#define OFFSETOF(type, field) ((size_t)&(((type *)0)->field))

(type *)0:把0地址当成type类型的指针。

((type *)0)->field:对应域的变量。

&((type *)0)->field:取该变量的地址,其实就等于该域相对于0地址的偏移量。

(size_t)&(((type *)0)->field):将该地址(偏移量)转化为size_t型数据。

ANSI C标准允许任何值为0的常量被强制转换成任何一种类型的指针,并且转换结果是一个NULL指针,因此((s*)0)的结果就是一个类型为s*的NULL指 针。如果利用这个NULL指针来访问s的成员当然是非法的,但&(((s*)0)->m)的意图并非想存取s字段内容,而仅仅是计算当结构 体实例的首址为((s*)0)时m字段的地址。聪明的编译器根本就不生成访问m的代码,而仅仅是根据s的内存布局和结构体实例首址在编译期计算这个(常 量)地址,这样就完全避免了通过NULL指针访问内存的问题。

1.

有人这样表达:

#define OFFSETOF(type, field) ((size_t) \

               ((char *)&((type *)0)->field - (char *)(type *)0))

我认为效果是一样的,多增加的那部分就是0地 址,相减后就是偏移量。

2.

为什么要增加size_t呢?

首先size_t的定义是什么呢,在文件 stddef.h中可以找到答案。

typedef unsigned int size_t;                      /*mine is 32bit machine*/

可见就是将偏移量转化为无符整型,其实32位 机器的地址就是无符号的32位整数。一般情况下,不进行size_t类型转化也是没有问题的(后面的实验可证)。我认为,只有偏移量足够大,当大于 0x80000000时才有影响,因为这时候的偏移量最高位是1,机器默认为是负数了。似乎上面宏定义OFFSETOF中更能说明这个问题,因为这个宏定 义是一个差值,最高位是1就肯定是负数了。使用printf("%d", &var);打印一个变量的地址就是个负数。这只是我的看法,网上基本没有什么人分析为什么添加size_t的强制类型转化。因为系统对数组长度 的大小是有限制的,所以也不能实验得到数据。

插一句数组长度的问题(引述):

理论上来说没有限制,但是内核一般配置允许每 个进程拥有有限的内存空间,可以用系统调用函数getrlimit(int resource, struct rlimit *rlim)

获得系统的资源限制。系统的资源限制分为软件 限制和硬件限制,软件限制最大值不能超过硬件限制。数组静态获得的存储空间是分配在stack,只要知道stack的限制就知道答案了。可以使用如下代码 获得:

struct rlimit resource_limit;

getrlimit(RLIMIT_STACK, &resource_limit);

printf("STACK: soft_limit - %ld hard_limit - %ld\n", resource_limit.rlim_cur, resource_limit.rlim_max);

分配大数量的数组,若是系统找不到该大小的一 段连续的存储空间,系统就会产生一个SIGSEGV信号,这时调用函数int sigaltstack(const stack_t *ss, stack_t *oss)来处理这个信号。sigaltstack储存信号SIGSEGV到一个alternate stack结构ss中,内核会先于进程运行前检查这个信号。

3.由此,到一个结构体中field所占用的字节 数就很简单了。

#define FIELD_SIZE(type, field) sizeof(((type *)0)->field)

4.其实,系统给提供了一个相同的宏定义,在文件 stddef.h中:

在嵌入式系统里,不同开发商,不同架构处理器 和编译器都有不同的offsetof定义形式:

/* Keil 8051 */

#define offsetof(s,m) (size_t)&(((s *)0)->m)

/* Microsoft x86 */

#define offsetof(s,m) (size_t)(unsigned long)&(((s *)0)->m)

/* Motorola coldfire */

#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))

/* GNU GCC 4.0.2 */

#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)

虽然定义形式不同,但功能都是返回成员在数据 结构中的偏移量,都是为了提高代码的可移植性。

5.

offsetof虽然同样适用于union结 构,但它不能用于计算位域(bitfield)成员在数据结构中的偏移量。

typedef struct

{

unsigned int a:3;

unsigned int b:13;

unsigned int c:16;

}foo;

使用offset(foo,a)计算a在 foo中的偏移量,编译器会报错。

6.应用(引述)

offsetof与EEPROM

我们许多人可能都使用过一些非挥发性的存储器,如常见的EEPROM。我们经常使用它们在存储一些系统的配置参数和设备信息。在所有的EEPROM中,通 串过口访问的占了大多数。一般来说,对串口的访问都是按字节进行的,这使得我们不可避免会设计出下面的接口去访问EEPROM的信息:

/*从EEPROM 偏移量offset处读取nBytes到RAM地址dest*/

ee_rd(uint16_t offset, uint16_t nBytes, uint8_t * dest);

然而,这种接口必须要知道偏移量offset 和读取字节数nBytes。可能你会采用下面的方法解决方法解决这个问题:

定义一个数据结构和一个指向这个数据结构的指 针,并初始化这个指针为EEPROM的起始地址EEPROM_BASE.

#define EEPROM_BASE 0x0000000/*配置信息的起始地址*/

typedef struct

{  

     int    i;

     float f;

     char   c;

} EEPROM;

EEPROM * const pEE = EEPROM_BASE

ee_rd(&(pEE->f), sizeof(pEE->f), dest);

没错,这种方法的确可以达到访问指定地址的信 息。不过这种方法也存在下面的问题:

a.容易使代码维护人员人误以为在ee_rd 接口内部也存在EEPROM的数据结构。

b.当你编写一些自己感觉良好编译器不报错的 代码,比如pEE->f = 3.2,你可能意想不到灾难将要来临。

c.这个接口没有很好地体现EEPROM所隐 含的硬件特性。

到这里,有人可能会想到offsetof来解 决这个问题:

#define offsetof(type, f) ((size_t) \

    ((char *)&((type *)0)->f - (char *)(type *)0))

typedef struct

{

     int    i;

     float f;

     char   c;

} EEPROM;

ee_rd(offsetof(EEPROM,f), 4, dest);

如果让编译器来计算nBytes而不是我们自 己给出那就更好了。这时,一定有人会马上提到sizeof。可是怎么使用呢,我们不能用sizeof(EEPROM.f)来计算nBytes吧?!因为 EEPROM是数据类型,不是对象,没有办法操作f域呀。

/*类似于offsetof的定义*/

#define SIZEOF(s,m) ((size_t) sizeof(((s *)0)->m))

ee_rd(offsetof(EEPROM, f), SIZEOF(EEPROM, f), &dest);

其实还可以精简为下面的最终形式:

#define EE_RD(M,D)   ee_rd(offsetof(EEPROM,M), SIZEOF(EEPROM,M), D)

EE_RD(f, &dest);

哈哈,这样我们只用传递两个参数,不用再考虑 应该从那里读取数据以及读取多少的问题。

有人会说这种简化都是建立在 EEPROM_BASE为0x0000000基础之上的,可能会反问,如果配置信息不是从0地址开始的呢?

其实我们可以通过下面的方法解决。

#define EEPROM_BASE 0x00000a10

typedef struct

{

     char   pad[EEPROM_BASE];/*使数据结构的前EEPROM_BASE个字节填"空"*/

     int    i;

     float f;

     char   c;

} EEPROM;

使用offsetof简化EEPROM的串口 访问的确很妙。这里还有一个很好的例子。在嵌入式应用中,我们时常将一些I/O寄存器映射到内存地址空间进行访问。这种映射使原本复杂的寄存器访问变得象 访问普通的RAM地址一样方便。PowerPC 8250访问外部的ROM控制器(ROM controller)的寄存器就是通过这种方式实现的。ROM控制器所有的寄存器被映射到从I/O寄存器空间基地址 0x10000000(IO_BASE)偏移0x60000(ROMCONOffset)字节的一段内存。每个寄存器占用四个字节,并有一个数据结构与它 们对应。比如控制ROM控制器工作状态的寄存器对应数据结构ROMCON_ROM_CONTROL,配置PCI总线A的寄存器对应数据结构 ROMCON_CONFIG_A,下面先看看这些数据结构的定义:

#define IO_BASE      0x10000000

#define ROMCONOffset 0x60000

typedef unsigned int NW_UINT32;

typedef struct _ROMCON_CONFIG_A {

    union {

        struct {

            UINT32 pad4:21;         /* unused   */

            UINT32 pad3:2;          /* reserved */

            UINT32 pad2:5;          /* unused   */

            UINT32 EnablePCIA:1;

            UINT32 pad1:1;          /* reserved */

            UINT32 EnableBoot:1;        

            UINT32 EnableCpu:1;     /*bit to enable cpu*/

        } nlstruct;

        struct {

            UINT32 ConfigA;

        } nlstruct4;

    } nlunion;

} ROMCON_CONFIG_A, *PROMCON_CONFIG_A;

typedef struct _ROMCON_ROM_CONTROL {

    union {

        struct {

            UINT32 TransferComplete:1;

            UINT32 pad3:1;            /* unused */

            UINT32 BondPad3To2:2;

            UINT32 Advance:3;

            UINT32 VersaPortDisable:1;

            UINT32 pad2:1;            /* unused */

            UINT32 FastClks:1;

            UINT32 pad1:7;            /* unused */

            UINT32 CsToFinClks:2;

            UINT32 OeToCsClks:2;

            UINT32 DataToOeClks:2;

            UINT32 OeToDataClks:3;

            UINT32 CsToOeClks:2;

            UINT32 AddrToCsClks:2;        

            UINT32 AleWidth:2;

        } nlstruct;

        struct {

            UINT32 RomControl;

        } nlstruct4;

    } nlunion;

} ROMCON_ROM_CONTROL, *PROMCON_ROM_CONTROL;

typedef struct

{

    ROMCON_CONFIG_A     ConfigA;

    ROMCON_CONFIG_B     ConfigB;

    ROMCON_ROM_CONTROL RomControl;

    ...

}ROMCON, *PROMCON;

---------------------------- <-IO_BASE:0x10000000   

|   |   |   |   |   |   |...

----------------------------        

|   |   |   |   |   |   |...

...

---------------------------- <-ROMCONOffset(ROMCON):0x60000     

|   |   |   |   |   |   |...

---------------------------- <-ROMCON_ROM_CONTROL            

...

----------------------------

那么如何访问 ROMCON_ROM_CONTROL对应寄存器呢,比如ROMCON_ROM_CONTROL对应寄存器的VersaPortDisable位?

估计有人可能会这样做:

事先定义成员 RomControl(ROMCON中用ROMCON_ROM_CONTROL定义的实例)相对于ROMCON的偏移量,

#define ROMCONRomControlOffset 0x8

然后设计访问ROM的接口如下:

/*读取ROM控制器位于src位置的寄存器 数据到dest*/

typedef unsigned long dword_t;

void rom_read(dword_t* src, uint32_t* dest);

void rom_write(dword_t* src, uint32_t* dest);

最后利用这个偏移量做下面的操作:

ROMCON_ROM_CONTROL tRomCtrl={0};

dword_t* pReg=(dword_t*)(IO_BASE+ROMCONOffset+ROMCONRomControlOffset);

rom_read(pReg,(uint32_t)*(&tRomCtrl));

/*查看寄存器的 VersaPortDisable位,如果该位没有启用就启用它*/

if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)

{

tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;

rom_write(pReg,(uint32_t)*(&tRomCtrl));

}

这样做确实可以达到访问相应寄存器的目的。但 是,如果和ROM相关的寄存器很多,那么定义、记忆和管理那么多偏移量不是很不方便吗?到这里,如果你对前面关于offsetof还有印象的话,我想你可 能会作下面的优化:

#define ROMCON_ADDR(m)   (((size_t)IO_BASE+\

                         (size_t)ROMCONOffset+\

                         (size_t)offsetof(ROMCON,m))

ROMCON_ROM_CONTROL tRomCtrl={0};

dword_t* pReg=(dword_t*)ROMCON_ADDR(ConfigA);

rom_read(pReg,(uint32_t)*(&tRomCtrl));

/*查看寄存器的 VersaPortDisable位,如果没有启动就启动它*/

if(!tRomCtrl.nlunion.nlstruct.VersaPortDisable)

{

tRomCtrl.nlunion.nlstruct.VersaPortDisable = 1;

rom_write(pReg,(uint32_t)*(&tRomCtrl));

}

7.实验

#include "stdio.h"

#define OFFSET(s, m)        ((size_t)&(((s *)0)->m))

#define OFFset(s, m)        (&(((s *)0)->m))

typedef unsigned int size;

typedef struct node

{

    int aa;

    unsigned char bb;

    unsigned int cc[5];

    unsigned char dd[8];

    unsigned long ee;

    int ff;

    char gg;

    int hh;

}nn;

typedef struct node2

{

    char aa;

    unsigned char bb;

    unsigned char cc[8];

    int dd;

    int ee;

    int ff;

    unsigned int gg[5];

    unsigned long hh;

}mm;

int main(int argc, char * argv[])

{

    printf("OFFSET of aa=%d\n", OFFSET(nn, aa));

    printf("OFFSET of bb=%d\n", OFFSET(nn, bb));

    printf("OFFSET of cc=%d\n", OFFSET(nn, cc));

    printf("OFFset=%d\n", OFFset(nn, cc));

    printf("OFFSET of dd=%d\n", OFFSET(nn, dd));

    printf("OFFSET of ee=%d\n", OFFSET(nn, ee));

    printf("OFFSET of ff=%d\n", OFFSET(nn, ff));

    printf("OFFSET of gg=%d\n", OFFSET(nn, gg));

    printf("OFFSET of hh=%d\n\n", OFFSET(nn, hh));

   

    printf("OFFSET of aa=%d\n", OFFSET(mm, aa));

    printf("OFFSET of bb=%d\n", OFFSET(mm, bb));

    printf("OFFSET of cc=%d\n", OFFSET(mm, cc));

    printf("OFFset=%d\n", OFFset(mm, cc));

    printf("OFFSET of dd=%d\n", OFFSET(mm, dd));

    printf("OFFSET of ee=%d\n", OFFSET(mm, ee));

    printf("OFFSET of ff=%d\n", OFFSET(mm, ff));

    printf("OFFSET of gg=%d\n", OFFSET(mm, gg));

    printf("OFFSET of hh=%d\n\n", OFFSET(mm, hh));

    return 0;

}

通过GCC编译、运行后,答案简略为:

0 4 8(8) 28 36 40 44 48

0 1 2(2) 12 16 20 24 44

结论:

1.

在32位机器中,long(long int)型数据占32bit,和short一样。long long是为了支持64位数据而产生的,表示64bit的数据。

在64位机器中,long型数据占 64bit,short占32bit。

2.

编译器要求对齐,造成了结构体“空洞”。所以 相同类型的数据还是靠近吧。空洞问题也可以解释为什么结构体不支持比较(支持==),“空洞”中的随机数据会导致失败。

附(摘录):

1. 字节对齐(byte alignment):

现代计算机中内存空间都是按照byte划分 的,从理论上讲对于任何类型的变量的访问都可以从任何地址开始。但是实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据 按照一定的规则在空间上排列,而不是顺序的一个接一个的牌坊,这就是对齐。

2. 对齐原因:

(1)有些CPU访问没有对齐的变量时候会发 生错误。比如Motorola 68000不允许将16位的字存储到奇数地址中, 将一个16位的字写到奇数地址将引发异常。

(2)不对数据进行对齐,会在存取效率上带来 损失。比如:有些CPU从偶数地址存储int数据,读取时需要一个机器周期,从偶数地址存储,就需要2个机器周期。

3.对齐规则:

实际上, 对于c中的字节组织, 有这样的对齐规则:   

(1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

(2) 结构体每个成员相对于结构首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

(3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

不同CPU的对其规则可能不同, 请参考手册。为什么会有上述的限制呢? 理解了内存组织, 就会清楚了。

CPU通过地址总线来存取内存中的数据,32 位的CPU的地址总线宽度既为32位置, 标为A[0:31]。在一个总线周期内,CPU从内存读/写32位。 但是CPU只能在能够被4整除的地址进行内存访问,这是因为: 32位CPU不使用地址总线的A1和A2(比如ARM,它的A[0:1]用于字节选择, 用于逻辑控制, 而不和存储器相连,存储器连接到A[2:31])。访问内存的最小单位是字节(byte), A0和A1不使用, 那么对于地址来说, 最低两位是无效的,所以它只能识别能被4整除的地址了。 在4字节中,通过A0和A1确定某一个字节。

4. 字节对齐对程序的影响:

(32bit、x86、gcc)(所占字节数 char:1, short:2, int:4, long:4, float:4, double:8)

struct A

{

    int a;

    char b;

    short c;

};

struct B

{

    char b;

    int a;

    short c;

};

sizeof(strcut A) == 8

sizeof(struct B) == 12

5.修改默认对齐:

(1)编程时,使用#pragma pack()。

#pragma pack (2)                          /*指定按2字节对齐*/

struct C

{

    char b;

    int a;

    short c;

};

#pragma pack ()                           /*取消指定对齐,恢复缺省对齐*/

sizeof(strcut C) == 8 == 2(b) + 4(a) + 2(c)

(2)修改编译器的默认对齐方式。

6.编程注意情况:

(1)为了字节对齐,显示声明冗余变量。当 然,不冗余声明,编译器也会自动对齐。

struct A

{

    char a;

    char reserved[3];                 /*使用空间换时间*/

    int b;

};

(2)带来隐患,尤其是对代码移植的影响。

unsigned int i = 0x12345678;

unsigned char *p=NULL;

unsigned short *p1=NULL;

p=&i;

*p=0x00;

p1=(unsigned short *)(p+1);

*p1=0x0000;

最后两句代码,从奇数边界去访问 unsigned short型变量,显然不符合对齐的规定。

在x86上,类似的操作只会影响效率,但是在 MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐。