Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

11206
629
98793


[Linux][Kernel] container_of 매크로 Appendix1 매크로분석

이번에는 container_of란 매크로를 배워볼게요. 커널 코드에서 current 매크로 못지않게 많이 활용하는 매크로이니 잘 알아야겠죠. 그럼 다음 샘플 코드를 함께 보면서 container_of란 매크로을 어떻게 활용하는지 살펴볼까요?

다음 wq_barrier_func 함수를 예를 들까요? container_of를 써서 struct wq_barrier *barr 로컬 변수에 어떤 값을 대입하고 있죠.
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란 매크로에 익숙하지 않은 분들에겐 좀 헷갈리는 코드인데요.

다음과 같이 수정해야 조금 더 가독성이 높지 않을까요? 물론 이렇게 수정한 코드를 빌드 해서 동작시켜도 똑같이 동작합니다.
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;
}

여기까지 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);

첫 번째 파라미터(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 코드와 어셈블리 코드를 함께 보는 습관을 갖는 게 좋습니다.

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"



Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 


    핑백

    덧글

    • 살벌한 눈의여왕 2018/07/10 12:18 # 답글

      gcc 컴파일러가 rcu의 구조체의 오프셋을 계산한다는 것은 결국 offsetof 매크로를 이용한 것이겠지요?
      그런데 offsetof의 정의를 보면 ((size_t) &((TYPE *)0)->MEMBER) 이렇게 되어있던데 저 화살표 마크(->)는 컴파일러수준에서 해석되는 symbol인가요?
      그러니까 kernel code를 컴파일 할 때, 자신의 symbol table에서 struct worker_pool이 있을테니, 거기서 rcu의 offset을 계산해 놓는건가요?
    • Guillermo 2018/08/09 17:17 # 답글

      정신 없이 개발에 몰입하다가 이제야 답신을 드리는군요.


      맞습니다.
      GCC 컴파일러가 심볼 테이블에 있는 구조체 오프셋 값을 체크한 후 어셈블리 코드를 이미 생성하는 겁니다.

      다음 코드를 보면 0을 (TYPE *)인 포인터 타입으로 캐스팅하고 다시 & 기호로 주소에 접근합니다.
      ((size_t) &((TYPE *)0)->MEMBER)

      (TYPE *) 은 메모리 공간에 있는 덤프값을 가르키고 &는 메모리 주소를 가르킵니다.
      쉽게 설명하면 (TYPE *) + & 실행을 두번 하면 셈셈이가 되고 이 과정에서 MEMBER 오프셋 주소를 읽는 겁니다.

      // 제 블로그에 있는 글을 읽고 언제든 질문 남겨주시면 빠른 시일 내에 답신 드리겠습니다.
    • 살벌한 눈의여왕 2018/09/10 15:43 # 답글

      답변 감사합니다.

      다만 struct rcu_head를 포함하는 구조체의 주소를 알기 위해서는,

      struct rcu_head가 struct worker_pool에 포함된다는 사실을 반드시 미리 알아야 하는 제약이 있군요.

      초보 개발자에겐 큰 제약입니다ㅎㅎㅎ 감사합니다.
    댓글 입력 영역