[리눅스][커널] container_of 매크로란?
이번에는 container_of란 매크로를 소개합니다.
커널 코드에서 current 매크로 못지않게 많이 활용하는 매크로이니 잘 알아야겠죠. 그럼 다음 샘플 코드를 함께 보면서 container_of란 매크로을 어떻게 활용하는지 살펴볼까요?
다음 wq_barrier_func 함수를 예를 들까요? container_of를 써서 struct wq_barrier *barr 로컬 변수에 어떤 값을 대입하고 있죠.
소스 위치: [elixir.bootlin.com/linux/v4.14.70/source/kernel/workqueue.c] static struct workqueue_struct *dev_to_wq(struct device *dev) { struct wq_device *wq_dev = container_of(dev, struct wq_device, dev); return wq_dev->wq; }
위 코드를 읽기 전에 우선 container_of란 매크로를 쉽게 표현해볼까요?
container_of(입력주소, 구조체, 해당 구조체 멤버)
따라서 container_of(dev, struct wq_device, dev)는 코드는 다음과 같이 해석할 수 있습니다.
dev: 입력주소 struct wq_device: 구조체 dev: struct wq_device에 위치한 멤버
container_of의 첫 번째와 세 번째 파라미터는 모두 dev인데, 이 파라미터는 다르게 해석해야 합니다. 세 번째 파라미터는 dev_to_wq함수에서 전달되는 (struct device *) 타입의 dev가 아니고 struct wq_device란 구조체에 위치한 멤버 이름입니다.
struct wq_device 구조체의 정의는 다음과 같으니 참고하세요.
struct wq_device { struct workqueue_struct *wq; struct device dev; };
이렇게 container_of란 매크로에 익숙하지 않은 분들에겐 좀 헷갈리는 코드인데요.
다음과 같이 수정해야 조금 더 가독성이 높지 않을까요?
Before와 After 코드를 눈으로 따라가서 봅시다. 물론 이렇게 수정한 코드를 빌드 해서 동작시켜도 똑같이 동작합니다.
[After] static struct workqueue_struct *dev_to_wq(struct device *dev_ptr) { struct wq_device *wq_dev = container_of(dev_ptr, struct wq_device, dev); return wq_dev->wq; } [Before] static struct workqueue_struct *dev_to_wq(struct device *dev) { struct wq_device *wq_dev = container_of(dev, struct wq_device, dev); return wq_dev->wq; }
여기까지 container_of란 매크로를 쉽게 설명을 했는데요. 이 매크로의 구현부를 확인해볼까요? container_of 란 매크로의 구현부는 다음과 같습니다.
1 #define container_of(ptr, type, member) ({ \ 2 const typeof( ((type *)0)->member ) *__mptr = (ptr); 3 (type *)( (char *)__mptr - offsetof(type,member) );})
위에서 ptr은 입력 주소, type은 구조체 그리고 member는 type이란 구조체에 정의된 멤버라고 했죠? 그럼 이 매크로가 위에서 살펴본 샘플 코드에서 어떻게 치환되는지 살펴볼까요?
여기서 세 번째 줄 코드에 offsetof란 매크로가 보입니다. 이 매크로 형식은 다음과 같습니다.
offsetof(구조체,멤버)
container_of 매크로를 이해하기 위해서는 offsetof란 코드를 알아야 합니다. offsetof 코드는 구조체에서 멤버가 위치한 오프셋을 알아내는 매크로입니다. 그런데 struct wq_device란 멤버에서 (struct device *) 타입의 dev란 멤버는 0x8만큼 오프셋에 떨어져 있습니다.
struct wq_device * wq_dev = ({ const struct wq_dev *__mptr = dev; (struct wq_dev *)( (char *)__mptr - offsetof(struct wq_dev,work) );
여기까지 분석한 내용을 반영하면 다음과 같이 표시할 수 있다는 거죠. __mptr은 입력 주소인데 이 주소에서 오프셋을 빼는 동작입니다.
wq_dev = ({ const struct device *__mptr = dev; (struct wq_device *)( (char *)__mptr - 0x8); })
이번에는 다른 코드에서 container_of란 매크로를 살펴볼까요?
static void rcu_free_pool(struct rcu_head *rcu) { struct worker_pool *pool = container_of(rcu, struct worker_pool, rcu); ida_destroy(&pool->worker_ida);
container_of 매크로에 전달된 인자를 해석해 봅시다.
첫 번째 파라미터(rcu): 입력 주소
두 번째 파라미터(struct worker_pool): 구조체
세 번째 파라미터(rcu): struct worker_pool 구조체에 위치한 멤버
이번에도 좀 헷갈릴 수 있는 코드이군요.
[kernel/workqueue.c] 파일에 가보면 strut worker_pool 구조체를 볼 수 있는데요.
해당 구조체를 확인하니 역시 rcu란 멤버가 보이죠?
struct worker_pool { spinlock_t lock; /* the pool lock */ int cpu; /* I: the associated cpu */ int node; /* I: the associated node ID */ //... struct rcu_head rcu; } ____cacheline_aligned_in_smp;
그럼 이제 container_of 매크로는 어셈블리 명령어로 어떻게 처리하는지 점검해볼까요? rcu_free_pool 함수 구현부를 어셈블리 코드로 보니 다음과 같군요.
static void rcu_free_pool(struct rcu_head *rcu) { 80134fc8: e1a0c00d mov ip, sp 80134fcc: e92dd818 push {r3, r4, fp, ip, lr, pc} 80134fd0: e24cb004 sub fp, ip, #4 80134fd4: e52de004 push {lr} ; (str lr, [sp, #-4]!) 80134fd8: ebff6558 bl 8010e540 <__gnu_mcount_nc> 80134fdc: e1a04000 mov r4, r0 //<<--[1] struct worker_pool *pool = container_of(rcu, struct worker_pool, rcu); ida_destroy(&pool->worker_ida); 80134fe0: e240004c sub r0, r0, #76 ; 0x4c //<<--[2] 80134fe4: eb0c57f5 bl 8044afc0 <ida_destroy>
이제 그럼 어셈블리 코드에서 container_of를 어떻게 처리하는지 같이 살펴볼까요?
[1]: rcu_free_pool에 전달된 파라미터는 (struct rcu_head*) 타입의 rcu입니다.
ARM 프로세스의 호출 규약에 따라 이 파라미터는 r0 레지스터에 실려 옵니다.
r0를 r4에 이동시키는 명령어입니다. 이 명령어가 실행되면 rcu란 파라미터의 주소는 r4에 위치합니다.
조금 더 깊이 있게 ARM 어셈블리 명령어를 분석하다 보면 어느 정도 패턴이 보이거든요.
이런 패턴의 ARM 어셈블리 명령어를 보면 "아 r0을 r4에 백업시켜 놓고 r0에 어떤 값을 저장하려는 거구나" 라는 생각이 떠오르지 않나요?
어떤 함수를 호출할 때는 파라미터는 r0에 저장한 후 호출하거든요.
[2]: r0에 0x4c을 빼는 연산입니다. 그럼 이 동작은 뭘 의미할까요?
r0 = r0 - 0x4c = (struct rcu_head *rcu 주소) - (struct worker_pool.rcu 멤버 오프셋)
이렇게 어셈블리 명령어를 분석하니 한 가지 교훈을 얻을 수 있습니다. container_of 매크로를 어떤 코드에서 만나면 두 번째 인자인 구조체에서 세 번째 인자인 해당 구조체 멤버의 오프셋이 얼마인지 계산해야겠죠. 이를 위해 해당 구조체의 선언부를 코드를 열어서 확인하면서 머릿속으로 각 멤버 오프셋이 몇 바이트인지 체크해야 합니다. 그런데 어셈블리 코드를 보면 struct worker_pool 구조체의 rcu 멤버의 오프셋을 이미 알고 있는 듯하네요. GCC 컴파일러가 struct worker_pool 구조체의 rcu 멤버의 오프셋을 계산해서 어셈블리 코드를 생성한 것이지요.
가끔은 C 코드로 어떤 매크로를 읽을 때 보다 어셈블리 명령어가 훨씬 이해 속도가 빠를 때도 있습니다. 그러니 C 코드와 어셈블리 코드를 함께 보는 습관을 갖는 게 좋습니다.
(Personal Blog)
http://rousalome.egloos.com/
댓글 달기