-
[리눅스 커널] container_ofLinux/kernel 2023. 8. 5. 18:52
글의 참고
- https://en.wikipedia.org/wiki/Offsetof
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
- `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.
- 대개 UEFI 에서 말하는 platform은 hardwares를 의미한다. 근데 구체적인 특정 하드웨어를 의미하기 보다는 Chipset 및 SoC를 의미하는 경우가 많다.
글의 내용
: `container_of` 왜 쓸까? `container_of` 매크로 함수는 구조체 변수의 멤버 변수를 통해 구조체 시작 주소를 알아냈다는데 있다. 예를 들어, 아래 예제에서 `struct my_struct` 구조체에 `name`과 `list` 구조체 변수가 있다. `container_of`는 구조체 멤버 변수 `name` 이나 `list`를 통해서 이 멤버 변수들을 포괄하는 구조체 시작 주소를 반환해준다.
struct my_struct { const char *name; struct list_node list; }; void itr_list(struct list_node *current){ while (current != NULL) { struct my_struct *element = container_of(current, struct my_struct, list); printf("%s\n", element->name); } } - 참고 : https://en.wikipedia.org/wiki/Offsetof
: 그래서 위의 예를 보면, `struct list_node *`를 통해서 `struct my_struct *`를 찾아내고 있다.
/include/linux/stddef.h #ifndef _LINUX_STDDEF_H #define _LINUX_STDDEF_H #ifndef _SIZE_T #define _SIZE_T typedef unsigned int size_t; #endif #undef NULL #define NULL ((void *)0) #undef offsetof #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) #endif ################################################################################## /include/linux/container_of.h /* SPDX-License-Identifier: GPL-2.0 */ #ifndef _LINUX_CONTAINER_OF_H #define _LINUX_CONTAINER_OF_H #include <linux/build_bug.h> #include <linux/stddef.h> ... ... /** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * * WARNING: any const qualifier of @ptr is lost. */ #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ ((type *)(__mptr - offsetof(type, member))); }) ... ...
: offsetof의 경우에서 주의할 부분은 0과 size_t이다. 0은 주소의 베이스를 의미한다. 아래 코드를 보면 이해할 수 있을 듯 하다.
#include <stdio.h> struct foo { short a; char b[10]; int c; }; int main() { printf("%d\n", (int)&(((struct foo *)0)->a)); // 0 printf("%d\n", (int)&(((struct foo *)0)->b)); // 2 printf("%d\n", (int)&(((struct foo *)0)->c)); // 12 printf("%d\n", (int)&(((struct foo *)5)->a)); // 5 printf("%d\n", (int)&(((struct foo *)5)->b)); // 7 printf("%d\n", (int)&(((struct foo *)5)->c)); // 17 printf("%d\n", (int)&(((struct foo *)10)->a)); // 10 printf("%d\n", (int)&(((struct foo *)10)->b)); // 12 printf("%d\n", (int)&(((struct foo *)10)->c)); // 22 return 0; }
: 그리고 size_t는 컴퓨터의 사양에 따라 주의가 필요하다. 만약 malloc()을 통해 할당받은 메모리 주소가 0xbadf1202 일 때, size_t가 char, short, int, unsigned long long 타입에 따라 어떻게 바뀌는지 보자.
#include <stdio.h> struct foo { short a; char b[10]; int c; }; int main() { printf("0x%x\n", (char)&(((struct foo *)0xbadf1202)->a)); // 0x2 printf("0x%x\n", (short)&(((struct foo *)0xbadf1202)->b)); // 0x1204 printf("0x%x\n", (int)&(((struct foo *)0xbadf1202)->c)); // 0xbadf120e printf("0x%x\n", (unsigned long long)&(((struct foo *)0xbadf1202)->c)); // 0xbadf120e return 0; }
: 할당받은 메모리 보다 타입이 작을 경우, 주소가 짤려 나간다. 그니깐 타입 자체를 가장 큰 타입으로 받으면 문제는 없다. 일단 `size_t`가 어떻게 정의되는지도 확인해봐야 하지만, 64비트 지원 컴퓨터라면, `size_t`를 (unsigned long long)으로 바꿔야 할 듯 하다.
: 그리고 위 코드에서 `유저 레벨에서 작성하면 페이지 폴트 같은 메모리 오류 나는거 아닌가?` 할 수도 있는데, 실제 오류가 나는건 값을 쓸 때 발생하지, 읽기만 하면 에러는 발생하지 않는다. 물론, 커널 영역을 읽으려고 하면 오류가 나긴한다. 그런데, 페이징이 활성화되어 되면, 가상 주소를 사용하기 때문에 오류가 발생하지 않을 것으로 보인다.
#include <stdio.h> struct foo { short a; char b[10]; int c; }; int main() { *(&(((struct foo *)0)->a) = 2; // Segmentation Fault if((int)&(((struct foo *)0)->a == 2) { printf("%d\n", (int)&(((struct foo *)0)->a)); // No Error } return 0; }
: 내가 Custom OS를 만들 때, 최종 작성한 container_of() 매크로는 아래와 같다. 위에서는 void*를 썻지만, 사실 주소의 계산에서 가장 작은 단위는 byte이기 때문에 u8(unsigned char)를 사용해서 바꿔줬다. 그리고 이름모를 __mptr도 base라는 이름으로 변경했다.
#ifndef offsetof #define offsetof(type, member) (int)&(((type *)0)->member) #endif #define container_of(ptr, type, member) ({ \ u8 *base = (u8 *)(ptr); \ ((type *)(base - offsetof(type, member))); })
: 그리고 container_of()의 재미있는 점은 괄호에도 있다. 괄호가 2개 사용되는 것이 보인다. 먼저 {} 괄호로 지역적으로 변수를 사용하겠다는 것을 나타내고 ()을 통해서 값을 반환할 수 있는 구조를 만든다. 원래라면 {} 만 쓸 경우, 두 번 째줄에 `((type *)(base - offsetof(type, member))); })` 의 값은 반환되지 못하고 컴파일시 에러를 뿜는다. 아래 코드에서 `()`이 없는 매크로는 에러를 발생시킨다. 그런데 아래의 매크로 함수는 우리가 예상하는 대로 정확하게 동작을 한다.
#include <stdio.h> #define test(x) { \ int i = 2; \ x - i; \ } #define test(x) ({ \ int i = 2; \ x - i; \ })
- 주의 점
: container_of의 첫 번째 인자는 입력 주소인데, 대개는 포인터가 들어간다. container_of()의 첫 번째인자는 자신이 반환받고 싶은 구조체 변수의 멤버 변수 주소를 입력값으로 넣어야 한다는 것이다. 아래의 뻘짓 코드를 보자.
struct node { struct node left; struct node right; } struct foo { int a; int b; struct node *nd; } int main(void) { struct foo tmp; struct foo *dst; tmp.a = 1; tmp.b = 2; tmp.nd = malloc(sizeof(struct node)); dst = container_of(tmp.nd, struct foo, nd); if(dst ==tmp) // True else // False return 0; }
: 위의 코드는 자연스럽게 넘어갈 수 있는 실수다. 위에서 container_of 매크로에 들어가는 tmp 구조체 변수의 nd만 넣으면 이상한 놈이 반환된다. 왜냐면, `nd`가 포인터 변수이기 때문이다. 위의 결과는 `container_of`가 `nd`가 가리키고 있는 주소를 기준으로 계산해서 이상한 값을 가지고 오게 된다. 실제로 위해서 원했던 결과는 `tmp.nd`를 통해서 tmp를 얻어내는 것인데도 말이다. 만약, 원하는 결과를 얻고 싶다면, container_of()의 첫 번째 값으로 &(tmp.nd)를 넣어야 한다.
: 그런데, 일반적으로 이렇게 사용하지 않는다. 저렇게 사용해서도 안된다. 즉, `nd`가 가리키고 있는 값을 통해서 `tmp`를 얻어내는게 맞다. 잘 이해가 안간다면, 이 글의 맨 처음에 소개한 예제를 살펴보자.
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] devicetree overlay (0) 2023.08.07 [리눅스 커널] PM - Thermal framework : Overview & devicetree (0) 2023.08.07 [리눅스 커널] debug - dynamic debug (0) 2023.08.05 [리눅스 커널] Data structure - Linked list (0) 2023.08.05 [리눅스 커널] pinctrl - raspberry pi 3 overview (0) 2023.08.04