커널 모드 프로그래밍을 하다 보면 CONTAINING_RECORD 매크로를 자주 보게 됩니다. 이 매크로는 자기 자신을 포함하고 있는 상위 구조체의 주소를 얻기 위해 사용합니다.
간단한 예제를 살펴보겠습니다.
이렇게 HEADER라는 구조체가 있습니다. Hello, World, A, B, C는 HEADER 구조체의 속성이고, 마지막 Body는 가변 크기의 멤버를 담고 있습니다.
방금 전 Body는 이런 모양의 구조체를 다시 포함하고 있습니다.
그럼 실제로 사용해보겠습니다.
여기서 ExAllocatePool로 header에 메모리를 할당 할 때 HEADER 크기만 하지 않고 HELLO_BODY의 크기도 더해줬습니다. 그래서 HEADER, HELLO_BODY는 따로 떨어져 있지 않고 바로 이어져있습니다.
HEADER의 Body가 PVOID라 주소를 담고 있을것 같지만 HELLO_BODY가 이 공간을 차지하고 있기 때문에 HELLO_BODY의 첫번째 멤버의 값이 표시될 것입니다.
hellobody에는 header의 Body의 주소를 넣어주었기 때문에, header의 Hello, World, A, B, C는 액세스 할 수 없습니다.
SetValue 함수 안에서는 header의 존재를 알 수 없기 때문에 이럴때 CONTAINING_RECORD 매크로를 사용합니다. hellobody가 HEADER 구조체의 Body에 연결되어 있다고 한다면 CONTAINING_RECORD(BodyPointer, HEADER, Body)로 하면 현재 HELLO_BODY를 포함하고 있는 HEADER 구조체의 주소를 알아 올 수 있습니다.
HEADER 구조체의 주소를 알아왔기 때문에 HEADER의 멤버 A에도 접근이 가능합니다.
위의 예보다 더욱 많이 사용되는 곳이 연결 리스트(Linked list)를 순회하면서 참조할 때입니다. 간단한 사용예를 들면 아래와 같습니다.
CONTAINING_RECORD 매크로는 ntdef.h에 정의되어 있습니다.
매크로가 캐스팅을 많이 해서 복잡해 보이지만 실제로는 매우 간단합니다.
예제와 연결해서 설명해보면 address에는 hellobody(BodyPointer), type에는 HEADER, field에는 Body가 들어갑니다. 먼저 hellobody의 주소에서 Body의 위치를 빼주는 것입니다.
(ULONG_PTR)(&((type *)0)->field)))에서 0을 HEADER * 형으로 캐스팅 하였기 때문에 HEADER 구조체상에서 Body의 위치가 나오게 됩니다.
이렇게 해서 Body의 위치만큼 앞으로 간다면 HEADER 구조체의 선두, 즉 HEADER 구조체의 주소를 알 수 있게 되는 것입니다.
간단한 예제를 살펴보겠습니다.
typedef struct _HEADER {
ULONG Hello;
ULONG World;
ULONG A;
ULONG B;
ULONG C;
PVOID Body;
} HEADER, *PHEADER;이렇게 HEADER라는 구조체가 있습니다. Hello, World, A, B, C는 HEADER 구조체의 속성이고, 마지막 Body는 가변 크기의 멤버를 담고 있습니다.
typedef struct _HELLO_BODY {
ULONG Hello;
} HELLO_BODY, *PHELLO_BODY;방금 전 Body는 이런 모양의 구조체를 다시 포함하고 있습니다.
그럼 실제로 사용해보겠습니다.
VOID
SetHeader(
PVOID BodyPointer)
{
CONTAINING_RECORD(BodyPointer, HEADER, Body)->A = 0xAAAAAAA;
}
VOID
ContainingRecord()
{
PHEADER header = ExAllocatePool(NonPagedPool, sizeof(HEADER) + sizeof(HELLO_BODY));
PHELLO_BODY hellobody;
hellobody = (PHELLO_BODY)&header->Body;
SetHeader(hellobody)
ExFreePool(header);
}여기서 ExAllocatePool로 header에 메모리를 할당 할 때 HEADER 크기만 하지 않고 HELLO_BODY의 크기도 더해줬습니다. 그래서 HEADER, HELLO_BODY는 따로 떨어져 있지 않고 바로 이어져있습니다.
HEADER의 Body가 PVOID라 주소를 담고 있을것 같지만 HELLO_BODY가 이 공간을 차지하고 있기 때문에 HELLO_BODY의 첫번째 멤버의 값이 표시될 것입니다.
hellobody에는 header의 Body의 주소를 넣어주었기 때문에, header의 Hello, World, A, B, C는 액세스 할 수 없습니다.
SetValue 함수 안에서는 header의 존재를 알 수 없기 때문에 이럴때 CONTAINING_RECORD 매크로를 사용합니다. hellobody가 HEADER 구조체의 Body에 연결되어 있다고 한다면 CONTAINING_RECORD(BodyPointer, HEADER, Body)로 하면 현재 HELLO_BODY를 포함하고 있는 HEADER 구조체의 주소를 알아 올 수 있습니다.
HEADER 구조체의 주소를 알아왔기 때문에 HEADER의 멤버 A에도 접근이 가능합니다.
위의 예보다 더욱 많이 사용되는 곳이 연결 리스트(Linked list)를 순회하면서 참조할 때입니다. 간단한 사용예를 들면 아래와 같습니다.
for (links = queue.Flink; links != &queue; links = links->Flink)
{
tempdata = CONTAINING_RECORD(links, DATA, DataLinks);
DbgPrint("data, Hello : %d World : %d\n", tempdata->Hello, tempdata->World);
}CONTAINING_RECORD 매크로는 ntdef.h에 정의되어 있습니다.
#define CONTAINING_RECORD(address, type, field) \
((type *)((PCHAR)(address) - \
(ULONG_PTR)(&((type *)0)->field)))매크로가 캐스팅을 많이 해서 복잡해 보이지만 실제로는 매우 간단합니다.
예제와 연결해서 설명해보면 address에는 hellobody(BodyPointer), type에는 HEADER, field에는 Body가 들어갑니다. 먼저 hellobody의 주소에서 Body의 위치를 빼주는 것입니다.
(ULONG_PTR)(&((type *)0)->field)))에서 0을 HEADER * 형으로 캐스팅 하였기 때문에 HEADER 구조체상에서 Body의 위치가 나오게 됩니다.
이렇게 해서 Body의 위치만큼 앞으로 간다면 HEADER 구조체의 선두, 즉 HEADER 구조체의 주소를 알 수 있게 되는 것입니다.


댓글을 달아 주세요
http://monac.egloos.com/1193485
리눅스 커널에서도 같은 기법이 쓰입니다. 아마, BSD도 똑같을 겁니다. 범용적인 자료구조를 구현할 때 유용합니다. 굳이 템플릿이 없어도라는 생각도 하곤 합니다. ㅎㅎ
List_Entry의 offset을 빼서 개체 상위로 가던 시절. orz