ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] container_of
    Linux/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`를 얻어내는게 맞다. 잘 이해가 안간다면, 이 글의 맨 처음에 소개한 예제를 살펴보자.

Designed by Tistory.