Linux Tutorial #15 debug-objects

문연수·2021년 5월 31일
0

Linux Tutorial

목록 보기
16/25
post-thumbnail

이번 장에서 설명할 내용은 debugobjects 이다. Documentation/core-api/debug-object.rst 에 자세하게 기술되어 있는데 이들의 내용을 간단하게 요약하여 설명토록 하겠다.

공식 문서에 따르면 debugobjects 를 아래와 같이 기술한다.

debugobjects 는 커널 객체(object)의 수명과 연산의 유효성을 추적하는 기반 시설이다.
debugobjects 는 다음과 같은 오류 패턴을 확인하는데 유용하다:

  • 초기화되지 않은 객체의 활성화
  • 활성화 객체의 초기화
  • 해제된, 파괴된 객체의 사용

이번 장에서는 어떤 구조로 debugobjects 가 커널 객체의 유효성을 판단하는지 분석하는데 그 의의가 있다. 코드가 그렇게 길지 않으므로 천천히 하나 하나 뜯어보겠다. 해당 글에서 나오는 모든 코드는 include/linux/debugobjects.h, lib/debugobjects.c 에서 확인할 수 있다.

1. 핵심 자료 구조

debugobjects 의 축이 되는 핵심 자료구조에 대해 먼저 소개하고, 이를 통해 어떤 방식으로 데이터의 유효성을 검사하는지 알아보겠다.

struct debug_obj

위 구조체는 추적되는 객체의 정보를 표현한다.

struct debug_obj {
	struct hlist_node	node;
	enum debug_obj_state	state;
	unsigned int		astate;
	void			*object;
	const struct debug_obj_descr *descr;
};

node : 추적 리스트에 객체를 연결하기 위한 hlist node
state : 추적되는 객체의 상태
astate : 현재 활성화 상태
object : 실제 객체를 가르키는 포인터
descr : 디버그 정보 구조체를 가르키는 포인터.

struct debug_obj_descr

위 구조체는 객체의 디버그 정보를 명세한다.

struct debug_obj_descr {
	const char		*name;
	void *(*debug_hint)(void *addr);
	bool (*is_static_object)(void *addr);
	bool (*fixup_init)(void *addr, enum debug_obj_state state);
	bool (*fixup_activate)(void *addr, enum debug_obj_state state);
	bool (*fixup_destroy)(void *addr, enum debug_obj_state state);
	bool (*fixup_free)(void *addr, enum debug_obj_state state);
	bool (*fixup_assert_init)(void *addr, enum debug_obj_state state);
};

name : 객체 자료형의 이름
debug_hint : 객체를 나타내는 것을 허용하며, 커널 심볼과 연관된 주소를 반환하는 함수.
is_static_object : obj 가 정적이라면 참을, 그렇지 않다면 거짓을 반환하는 함수.
fiexup_init : 초기화 검사에 실패했을 때 호출되는 수리 함수. 모든 수리 함수는 수리에 성공했을 때 반드시 참을, 그렇지 않을 때 거짓을 반환해야 한다.
fixup_activate : 활성화 검사에 실패했을 때 호출되는 수리 함수.
fixup_destroy : 파괴 검사에 실패했을 때 호출되는 수리 함수.
fixup_free : 해제 검사에 실패했을 때 호출되는 수리 함수.
fixup_assert_init : assert_init 확인에 실패했을 때 호출되는 함수.

struct debug_bucket

추적되는 객체들은 hash 테이블에 저장되며, chaining hash 방식으로 각각의 객체가 연결된다.

struct debug_bucket {
	struct hlist_head	list;
	raw_spinlock_t		lock;
};

최종적으로 위와 같은 구조를 가지게 된다.

obj_hash 그리고 obj_static_pool

#define ODEBUG_HASH_BITS	14
#define ODEBUG_HASH_SIZE	(1 << ODEBUG_HASH_BITS)

#define ODEBUG_POOL_SIZE	1024

static struct debug_bucket	obj_hash[ODEBUG_HASH_SIZE];

static struct debug_obj		obj_static_pool[ODEBUG_POOL_SIZE] __initdata;

static HLIST_HEAD(obj_pool);
static HLIST_HEAD(obj_to_free);

앞서 본 hash 테이블과 debug_obj 는 정적인 크기를 가지는 배열로 선언한다. 이후 obj_poolobj_to_free 리스트를 통해 이들을 연결하여 위에서 본 그림과 같은 자료구조를 만들어 낸다.

자료구조만 봐서는 이들이 어떻게 사용될지 감이 안 잡힐 수 있다. 맨 위에서 말했듯이 debugobjects 는 커널 객체의 수명과 연산의 유효성을 검사하는 기반 시설이다. 어떻게 이들이 커널의 자료 구조를 보호할 수 있는지를 환기하고 2장을 읽기 바란다.

2. 핵심 함수들

이제 위에서 설명한 자료구조들을 초기화하고 활용하는 함수들을 소개하겠다. 실행되는 순서대로 어떻게 돌아가는지 잘 살펴보길 바란다.

debug_object_early_init 함수

void __init debug_objects_early_init(void)
{
	int i;

	for (i = 0; i < ODEBUG_HASH_SIZE; i++)
		raw_spin_lock_init(&obj_hash[i].lock);

	for (i = 0; i < ODEBUG_POOL_SIZE; i++)
		hlist_add_head(&obj_static_pool[i].node, &obj_pool);
}

debug_object_early_init 함수는 1번에서 설명했던 hash tableobject pool 을 초기화하는 함수이다.

debug_object_init 함수

debug_object_init 객체가 초기화될 때 그 유효성을 검사하는 함수이다. 커널 객체를 초기화할때 인자로 전달한 주소의 객체를 초기화해도 되는지, 혹은 안되는지를 알린다. 함수는 앞에 붙어 있는 언더바로 눈치챘을 수 있겠지만 래핑 함수(Wrapping Function)이다. 실제 함수 원형은 이후에 설명하겠다.

static void
__debug_object_init(void *addr, const struct debug_obj_descr *descr, int onstack)
{
	enum debug_obj_state state;
	bool check_stack = false;
    
	// debug_bucket 가르키는 구조체 포인터
	struct debug_bucket *db;
	// debug_obj 를 가르키는 구조체 포인터
	struct debug_obj *obj;
	unsigned long flags;

	fill_pool();

	// 주소를 해싱하여 bucket 을 가져온다.
	db = get_bucket((unsigned long) addr);

	// 락을 건다.
	raw_spin_lock_irqsave(&db->lock, flags);

	// bucket 에서 addr 에 해당하는 obj 를 찾는다.
	obj = lookup_object(addr, db);
	if (!obj) {
		// 못 찾았다면 새롭게 할당하여 db 에 addr 을 연결
		obj = alloc_object(addr, db, descr);
		if (!obj) {
			// 할당에 실패했다면, `debugobjects` 를 비활성화 시키고
			debug_objects_enabled = 0;
			// 락을 풀고
			raw_spin_unlock_irqrestore(&db->lock, flags);
			// Out-of-memory 에러를 띄우고 반환한다.
			debug_objects_oom();
			return;
		}
        
		// 스택 검사를 참으로 설정
		check_stack = true;
	}

	// 상태 검사
	switch (obj->state) {
	// NONE, INIT, INACTIVATE 라면 정상적으로 INIT
	case ODEBUG_STATE_NONE:
	case ODEBUG_STATE_INIT:
	case ODEBUG_STATE_INACTIVE:
		obj->state = ODEBUG_STATE_INIT;
		break;

	// 활성화된 객체의 초기화는 맨 위에서 말한 두 번째 조건에 위배된다.
	case ODEBUG_STATE_ACTIVE:
		state = obj->state;
		// 스핀 락을 풀고
		raw_spin_unlock_irqrestore(&db->lock, flags);
		// 경고 메세지를 띄우고
		debug_print_object(obj, "init");
		// init 에 대한 수리 함수를 호출한다.
		debug_object_fixup(descr->fixup_init, addr, state);
		return;

	// 파괴된 객체를 초기화(사용) 하는 것은 세 번째 조건에 위배된다.
	case ODEBUG_STATE_DESTROYED:
		// 스핀 락을 풀고
		raw_spin_unlock_irqrestore(&db->lock, flags);
		// 경고 메세지를 출력한다.
		debug_print_object(obj, "init");
		return;
	default:
		break;
	}

	// 스핀 락을 풀고
	raw_spin_unlock_irqrestore(&db->lock, flags);
	// 앞선 check_stack 이 참이라면
	if (check_stack)
		// debug_obj 의 addr 이 커널 스택에 있는지 점검
		debug_object_is_on_stack(addr, onstack);
}

핵심 라인에 주석을 달아 그 내용을 설명하였다. 앞선 세 가지 조건에 위배되는 상태에 대해서는 에러를 출력하는 것에 주목하라.

실제 함수 원형은 아래와 같다:

void debug_object_init(void *addr, const struct debug_obj_descr *descr)
{
	if (!debug_objects_enabled)
		return;

	__debug_object_init(addr, descr, 0);
}

debug_objects 가 사용 불가일 땐 즉시 반환하고 그렇지 않다면 초기화에 대한 디버깅을 수행하는 __debug_object_init 함수를 호출한다.

debug_object_free 함수

debug_object_free 함수는 커널 객체의 해제의 유효성을 검사하는 함수이다. 코드를 보며 그 내용을 살피겠다:

void debug_object_free(void *addr, const struct debug_obj_descr *descr)
{
	enum debug_obj_state state;
	struct debug_bucket *db;
	struct debug_obj *obj;
	unsigned long flags;

	// debugobjects 가 비활성화 상태라면 즉시 반환한다.
	if (!debug_objects_enabled)
		return;

	db = get_bucket((unsigned long) addr);

	raw_spin_lock_irqsave(&db->lock, flags);

	obj = lookup_object(addr, db);
	if (!obj)
		goto out_unlock;

	// 상태를 검사한다.
	switch (obj->state) {
	// 활성화 상태라면?
	case ODEBUG_STATE_ACTIVE:
		state = obj->state;
		raw_spin_unlock_irqrestore(&db->lock, flags);
		// 경고를 출력하고
		debug_print_object(obj, "free");
		// 수리 함수를 호출한다.
		debug_object_fixup(descr->fixup_free, addr, state);
		return;
	// 그 외에는
	default:
		// chaning list 에서 obj 를 빼내고
		hlist_del(&obj->node);
		// 스핀 락을 푼 다음
		raw_spin_unlock_irqrestore(&db->lock, flags);
		// obj 를 해제한다.
		free_object(obj);
		return;
	}
out_unlock:
	raw_spin_unlock_irqrestore(&db->lock, flags);
}

debug_object_activate 함수

debug_object_activate 함수는 커널 객체의 활성화를 디버깅하는 함수이다. 자세한 설명은 역시 주석으로 달아 두겠다.

int debug_object_activate(void *addr, const struct debug_obj_descr *descr)
{
	enum debug_obj_state state;
	struct debug_bucket *db;
	struct debug_obj *obj;
	unsigned long flags;
	int ret;
	struct debug_obj o = { .object = addr,
			       .state = ODEBUG_STATE_NOTAVAILABLE,
			       .descr = descr };

	if (!debug_objects_enabled)
		return 0;

	db = get_bucket((unsigned long) addr);

	raw_spin_lock_irqsave(&db->lock, flags);

	obj = lookup_object(addr, db);
	if (obj) {
		bool print_object = false;

		// 상태 확인
		switch (obj->state) {
		// INIT 혹은 INACTIVE 라면
		case ODEBUG_STATE_INIT:
		case ODEBUG_STATE_INACTIVE:
			// 상태를 변경하고
			obj->state = ODEBUG_STATE_ACTIVE;
			// 반환 값으로 사용 될 ret 을 0 으로 초기화한다.
			ret = 0;
			break;
		// 상태가 ACTIVE 라면
		case ODEBUG_STATE_ACTIVE:
			state = obj->state;
			raw_spin_unlock_irqrestore(&db->lock, flags);
			// 에러 메세지를 출력하고
			debug_print_object(obj, "activate");
			// 수리 함수를 호출하고 그 반환값으로 ret 을 초기화한다.
			ret = debug_object_fixup(descr->fixup_activate, addr, state);
			return ret ? 0 : -EINVAL;
		// 파괴된 상태라면?
		case ODEBUG_STATE_DESTROYED:
			// print_object 를 참으로 변경하고
			print_object = true;
			// 반환값으로 사용될 ret 을 -EINVAL 로 초기화.
			ret = -EINVAL;
			break;
		// 그 외에는
		default:
			// ret 을 0 으로 초기화한다.
			ret = 0;
			break;
		}
		raw_spin_unlock_irqrestore(&db->lock, flags);
		if (print_object)
			debug_print_object(obj, "activate");
		return ret;
	}

	// 스핀 락을 풀고
	raw_spin_unlock_irqrestore(&db->lock, flags);

	/*
	 * We are here when a static object is activated. We
	 * let the type specific code confirm whether this is
	 * true or not. if true, we just make sure that the
	 * static object is tracked in the object tracker. If
	 * not, this must be a bug, so we try to fix it up.
	 */
	// 등록된 정적 함수가 있다면 addr 을 전달하여 호출
	if (descr->is_static_object && descr->is_static_object(addr)) {
		/* track this static object */
		// addr 을 초기화하고
		debug_object_init(addr, descr);
		// 상태를 활성화시킨다.
		debug_object_activate(addr, descr);
	} else {
		// 에러 메세지를 출력하고
		debug_print_object(&o, "activate");
		// 수리 함수를 호출한다.
		ret = debug_object_fixup(descr->fixup_activate, addr,
					ODEBUG_STATE_NOTAVAILABLE);
		return ret ? 0 : -EINVAL;
	}
	return 0;
}

debug_object_deactivate 함수

위 함수는 커널 객체를 비활성화 할 때의 유효성을 검사한다. 자세한 내용 설명은 주석으로 남긴다.

void debug_object_deactivate(void *addr, const struct debug_obj_descr *descr)
{
	struct debug_bucket *db;
	struct debug_obj *obj;
	unsigned long flags;
	bool print_object = false;

	if (!debug_objects_enabled)
		return;

	db = get_bucket((unsigned long) addr);

	raw_spin_lock_irqsave(&db->lock, flags);

	obj = lookup_object(addr, db);
	if (obj) {
		// 상태 확인
		switch (obj->state) {
		// INIT, INACTIVE, ACTIVE 상태라면
		case ODEBUG_STATE_INIT:
		case ODEBUG_STATE_INACTIVE:
		case ODEBUG_STATE_ACTIVE:
			// 현재 활성 상태를 확인
			if (!obj->astate)
				// 비활성화라면 객체의 상태를 비활성화로
				obj->state = ODEBUG_STATE_INACTIVE;
			else
				// 객체 출력을 활성화
				print_object = true;
			break;

		// 객체가 파괴되었다면
		case ODEBUG_STATE_DESTROYED:
			// 객체 출력을 활성화
			print_object = true;
			break;
		default:
			break;
		}
	}

	// 스핀 락을 풀고
	raw_spin_unlock_irqrestore(&db->lock, flags);
	// 오브젝트가 존재하지 않는다면
	if (!obj) {
		// 사용 불가능 객체를 만들어서
		struct debug_obj o = { .object = addr,
				       .state = ODEBUG_STATE_NOTAVAILABLE,
				       .descr = descr };
		// 경고 메세지를 출력한다.
		debug_print_object(&o, "deactivate");
	} else if (print_object) {
		// 경고 메세지를 출력한다.
		debug_print_object(obj, "deactivate");
	}
}

3. debugobjects 의 활용

리눅스 커널에서 debug objects 을 활용ㅇ하고 있는 소스 파일은 생각보다 많지 않다. 여기에서는 kernel/workqueue.c 에서 debugobjects 를 어떻게 활용하는지 살펴 보도록 하겠다.

// 디버그 힌트 콜백 함수
static void *work_debug_hint(void *addr)
{
	return ((struct work_struct *) addr)->func;
}

// 정적 객체인지 판단하는 함수
static bool work_is_static_object(void *addr)
{
	struct work_struct *work = addr;

	return test_bit(WORK_STRUCT_STATIC_BIT, work_data_bits(work));
}

// 초기화 디버그 실패 시 호출되는 수리 함수
static bool work_fixup_init(void *addr, enum debug_obj_state state)
{
	struct work_struct *work = addr;

	switch (state) {
	case ODEBUG_STATE_ACTIVE:
		cancel_work_sync(work);
		debug_object_init(work, &work_debug_descr);
		return true;
	default:
		return false;
	}
}

// 해제 디버그 실패 시 호출되는 수리 함수
static bool work_fixup_free(void *addr, enum debug_obj_state state)
{
	struct work_struct *work = addr;

	switch (state) {
	case ODEBUG_STATE_ACTIVE:
		cancel_work_sync(work);
		debug_object_free(work, &work_debug_descr);
		return true;
	default:
		return false;
	}
}

// 위에서 설명한 내용을 등록하는 `struct debug_obj_descr` 구조체
static const struct debug_obj_descr work_debug_descr = {
	.name		= "work_struct",		// debug_object 에 명칭 부여
	.debug_hint	= work_debug_hint,		// 디버그 힌트 콜백 함수
	.is_static_object = work_is_static_object,	// 정적 객체인지 판단하는 함수
	.fixup_init	= work_fixup_init,		// 디버그 실패 시 호출되는 수리 함수
	.fixup_free	= work_fixup_free,		// 디버그 실패 시 호출되는 수리 함수
};

위 내용은 struct debug_obj_descr 구조체를 초기화하기 위해 선언한 함수 각종 함수이다. 위 내용을 바탕으로 어떻게 코드를 디버깅하는지 살펴 보자.

static inline void debug_work_activate(struct work_struct *work)
{
	debug_object_activate(work, &work_debug_descr);
}

static inline void debug_work_deactivate(struct work_struct *work)
{
	debug_object_deactivate(work, &work_debug_descr);
}

void __init_work(struct work_struct *work, int onstack)
{
	if (onstack)
		debug_object_init_on_stack(work, &work_debug_descr);
	else
		debug_object_init(work, &work_debug_descr);
}
EXPORT_SYMBOL_GPL(__init_work);

void destroy_work_on_stack(struct work_struct *work)
{
	debug_object_free(work, &work_debug_descr);
}
EXPORT_SYMBOL_GPL(destroy_work_on_stack);

void destroy_delayed_work_on_stack(struct delayed_work *work)
{
	destroy_timer_on_stack(&work->timer);
	debug_object_free(&work->work, &work_debug_descr);
}
EXPORT_SYMBOL_GPL(destroy_delayed_work_on_stack);

kernel/workqueue.cstruct work_struct *work 구조체를 debug_object 로 등록한다. 해당 객체에 대해 debug_work_activate() 함수와 debug_work_deactivate() 함수를 호출하며 struct work_struct *work 가 올바르게 동작하는지 점검한다.

출처

[책] 리눅스 커널 소스 해설: 기초 입문 (정재준 저)
[사이트] https://www.kernel.org/doc/Documentation/core-api/debug-objects.rst
[사진] https://liujunming.top/2018/03/12/linux-kernel%E4%B8%ADHList%E5%92%8CHashtable/

profile
2000.11.30

0개의 댓글