宏CONTAINING_RECORD学习

宏CONTAINING_RECORD的用处其实还是相当大的, 而且很是方便, 它的主要作用是:

    根据结构体中的某成员的地址来推算出该结构体整体的地址!

  下面从一个简单的例子开始说起:

  我们定义一个结构体, 同时类型化:

typedef struct{
    int a;
    int b;
    int c;
}ss;

  这是一个很简单的结构体, 没什么特殊的, 稍微分析下该结构体(假设在32位平台上):

    结构体的大小(字节):4+4+4=12字节

    成员a的偏移:0

    成员b的偏移:4

    成员c的偏移:8

  我们用ss来定义一个变量:

    ss s = {1,2,3};

  那么此时a,b,c的值分别为:a=1,b=2,c=3.

其实编译器在生成代码的时候其实是这样给成员变量赋值的:

  假定s的地址为:0x12000000, 则:

    *(int*)((char*)&s + 0) = 1;

    *(int*)((char*)&s + 4) = 2;

    *(int*)((char*)&s + 8) = 3;

  也就是说是在&s的地址基础上加上变量的偏移来确定成员的指针并赋值的, 所以:

    &s->a 将得到 0x12000000 + 0 = 0x12000000

    &s->b 将得到 0x12000000 + 4 = 0x12000004

    &s->c 将得到 0x12000000 + 8 = 0x12000008

  所以:    结构体的地址   +  成员变量的偏移 = 成员变量的地址

  移一下项:  成员变量的地址 -  成员变量的偏移 = 结构体的地址

  哇哇, 这就是我们想要的地址, 不就是做了个减法嘛~~~囧

  首先, 成员变量的地址是我们知道的.

  其次, 我们需要得到成员变量的偏移(假定为成员b的偏移).

    怎么办呢? 我们可以这样:

      &s->b - (unsigned long)&s, 这样就可以得到成员b的偏移了, 但是, 但是, &s 是我们需要的, 显然暂时是个未知数, 既然这样...

    那, 我们再做一次减法吧(非正确的C语言表达式, 不过结果没问题, 这里只是显得清楚点):

      &(s-s)->b - (unsinged long)&(s-s),

    其中, 为保证类型一致, 需要:

      s-s = (ss*)0

      (unsigned long)&(s-s) = (unsigned long)(ss*)0 = 0, 直接省略该部分就可以了

    那么, 化简得到: &((ss*)0)->b - (unsigned long)0

    最简结果: &((ss*)0)->b, 这就是b的偏移

  哈哈, 很简单吧, 0指针的妙用, 总共做了两次减法而已~ 对你们数学帝来说肯定不是问题啦~

  其中, 我们需要知道ss结构体的原型, ss结构体中的某个成员变量b(其实无论哪个都一样, 只是要和前面提供指针的那个变量要一致)

  总结下, 我们需要提供:结构体中某个成员变量的地址, 该结构体的原型, 该结构体中的某个成员变量(与前面要是同一个变量)

  最终的CONTAINING_RECORD的定义为:

#define CONTAINING_RECORD(addr,type,field) ((type*)((unsigned char*)addr - (unsigned long)&((type*)0)->field))
  addr:  结构体中某个成员变量的地址
  type:  结构体的原型
  field: 结构体的某个成员(与前面相同)

  好了, 所有的结论都出来了, 这是一个万能公式, 不管成员变量是哪一个结果都正确, 这是相对于知道第一个变量的地址而言的:

    如果知道的是第一个成员的地址(pa = &s->a)的话, 这是最简单的情况了:

    直接强制类型转换就可以了: (ss*)pa 即可, 此时 &((type*)0)->field 这部分恰好为0

    所以结果直接就是((type*)addr)了, 最简单的情况. 也是我们最容易想到的一种情况, 比如把链表元素放在结构体的最开始 ~~~

  

  到这里这个CONTAINING_RECORD宏就已经说完了~  

  现在, 我们在使用LIST_ENTRY等双向链表时, 不管把该链表放在结构体的哪个地方, 都可以在遍历链表时通过CONTAINING_RECORD宏来准确得到整个结构体的地址了~

记得移除链表中的某个元素的时候, 要free整个结构体的地址才行哦, WDK提供的操作函数只是把该链表元素脱离整个链表~~~

btw:

  把addr转换为 unsigned char*的原因是在指针计算时的计算单位为1, 也就是说 (unsigned char*)addr+1 = addr+1, 不转换的话肯定是错误的

  把&((type*)0)->field转换为(unsigned long)4个字节宽的同时是要保证表达式不是由两个指针的算术操作构成的, 因为C语言标准未定义那样的运算

时间: 2024-11-03 17:03:25

宏CONTAINING_RECORD学习的相关文章

C++宏的学习笔记

.C/C++中宏总结C程序的源代码中可包括各种编译指令,这些指令称为预处理命令.虽然它们实际上不是C语言的一部分,但却扩展了C程 序设计的环境.本节将介绍如何应用预处理程序和注释简化程序开发过程,并提高程序的可读性. ANSI标准定义的C语言预处理程序包括下列命令: #define,#error,#i nclude,#if,#else,#elif,#endif,#ifdef,#ifndef,#undef,#line,#pragma等.非常明显,所有预处理命令均以符号#开头,下面分别加以介绍. 1

宏定义学习

(1)宏名一般用大写(2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改.例如:数组大小常用宏定义(3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查.(4)宏定义末尾不加分号:(5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头.(6)可以用#undef命令终止宏定义的作用域(7)宏定义不可以嵌套(8)字符串" "中永远不包含宏(9)宏定义不分配内存,变量定义分配内存.(10)宏定义不存在类型问题,它的参数也

宏定义学习(1)

#define 标识符 字符串 输入半径,求周长.面积.球体积,使用不带参数的宏定义 #include <stdio.h> #define PI 3.1415926 int main() { double l,s,r,v; printf("input radius:"); scanf("%lf",&r); l=2.0*PI*r; s=PI*r*r; v=4.0/3*PI*r*r*r; printf("l=%10.4f\ns=%10.4l

我对CONTAINING_RECORD宏的详细解释

宏CONTAINING_RECORD的用处其实还是相当大的, 而且很是方便, 它的主要作用是: 根据结构体中的某成员的指针来推算出该结构体的指针! 下面从一个简单的例子开始说起: 我们定义一个结构体, 同时类型化: typedef struct{ int a; int b; int c; }ss; 这是一个很简单的结构体, 没什么特殊的, 稍微分析下该结构体: 结构体的大小(字节):4+4+4=12字节 成员a的偏移:0 成员b的偏移:4 成员c的偏移:8 我们用ss来定义一个变量: ss s

初级篇第十一期:学习使用常量定义和宏定义

学习建议:自己动手,丰衣足食 学习周期:1周 学习目的:熟练使用常量定义和宏定义 学习答疑:欢迎来技术群里提问并做分享 学习工具:Xcode开发环境 学习内容:熟悉项目开发中常用的两个定义 我们一般定义常量数字和字符串的时候一般会考虑用常量来定义   static CGFloat const kDefaultColorLayerOpacity = 0.4f; 一般定义宏的时候,都是用来定义方法,用宏的时候一定要多注意使用哦,会关系到括号的问题   #define SWH_RGBA(r, g, b

CONTAINING_RECORD 宏

CONTAINING_RECORD 这样的一个宏,我看了它的定义,如下:#define CONTAINING_RECORD(address, type, field) ((type *)( (PCHAR)(address) - (ULONG_PTR)(&((type*)0)->field)))class A{      char c;      int a;      short b;}int a = 100;int *pInt = &a;比如,我调用了 CONTAINING_REC

完成端口(Completion Port)详解

目录: 1. 完成端口的优点 2. 完成端口程序的运行演示 3. 完成端口的相关概念 4. 完成端口的基本流程 5. 完成端口的使用详解 6. 实际应用中应该要注意的地方 一. 完成端口的优点 1. 我想只要是写过或者想要写C/S模式网络服务器端的朋友,都应该或多或少的听过完成端口的大名吧,完成端口会充分利用Windows内核来进行I/O的调度,是用于C/S通信模式中性能最好的网络通信模型,没有之一:甚至连和它性能接近的通信模型都没有. 2. 完成端口和其他网络通信方式最大的区别在哪里呢? (1

套接字I/O模型-完成端口IOCP

“完成端口”模型是迄今为止最为复杂的一种I/O模型.然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统.因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型.要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的

object hook实现禁止创建文件

原理不说了,大伙都懂得.. 要解决的问题: 1. 怎么在windbg中看到_OBJECT_TYPE和_OBJECT_TYPE_INITIALIZER结构的内容. 2. 如何得到pOldParseProcedure的地址 3. 如何改写((POBJECT_TYPE)*IoDeviceObjectType)->TypeInfo.ParseProcedure=pNewProcedure 对于第一个问题: nt!_OBJECT_HEADER +0x000 PointerCount     : Int4B