리눅스 커널 코드를 읽다 보면 C 코드에는 존재하지 않는 함수를 호출하는 경우가 있습니다. 희한하게도 해당 함수를 아무리 검색해도 찾을 수 없는데 다른 함수에서 아무 문제 없이 호출합니다.
자 그럼 한 가지 예를 들게요. 아래 코드를 보면 end_page_writeback 함수에서 PageReclaim와 ClearPageReclaim 함수를 호출합니다. 이 함수는 페이지 write back 동작을 멈추는 역할을 하는 것으로 보이네요.
[mm/filemap.c]
void end_page_writeback(struct page *page)
{
if (PageReclaim(page)) {
ClearPageReclaim(page);
rotate_reclaimable_page(page);
}
PageReclaim 함수는 해당 페이지가 Reclaim 속성인지를 확인하고 ClearPageReclaim는 페이지 내 Reclaim 속성을 해제하는 함수로 보입니다. 그런데 함수 이름으로 그 함수가 어떤 일을 하는지 예상하는 것도 좋지만 실제 함수 구현부를 열어 봐야 되겠죠. PageReclaim와 ClearPageReclaim 함수 코드를 분석해야 합니다.
아, 그런데 문제가 생겼습니다. 커널 코드 어디에도 PageReclaim와 ClearPageReclaim 함수를 찾아볼 수가 없습니다. 이거 난감하네요. 여기서 분석을 멈춰야 할까요? 이럴 때는 전처리 코드를 보는 게 좋습니다. 왜냐면 전처리 파일은 해더 파일과 인라인 함수와 매크로를 모두 푼 정보를 담고 있기 때문이죠. 자 이제 전처리 파일을 찾아서 PageHWPoison 함수가 어디에 있는지 확인해볼까요?
end_page_writeback 함수는 [mm/filemap.c] 파일에 구현돼 있으니 [mm/.tmp_filemap.i] 전처리 파일을 열어봐야겠군요. 참고로 전처리 파일은 tmp란 접두사가 붙는다는 점도 기억 하세요.
[mm/.tmp_filemap.i] 전처리 파일에서 end_page_writeback 함수을 찾아보니 구현부가 다음과 같습니다. 뭔가 기대하고 전처리 코드를 열었는데 안타깝게도 C 코드와 완전히 같네요.
1 void end_page_writeback(struct page *page)
2 {
3 if (PageReclaim(page)) {
4 ClearPageReclaim(page);
5 rotate_reclaimable_page(page);
6 }
3번과 4번째 줄 코드를 봐도 PageReclaim와 ClearPageReclaim 함수를 그대로 호출합니다. 이거 참 난감하네요. 이럴 때 당황하지 말고 전처리 파일에서 PageReclaim, ClearPageReclaim 함수를 검색해볼까요? 만약 PageReclaim과 ClearPageReclaim 함수가 만약 인라인 타입 함수면 전처리 파일에서 확인할 수 있거든요. 왜냐면 전처리 파일은 인라인 함수 구현부, 해더 파일, 구조체 선언부를 모두 담고 있거든요.
그런데 [mm/.tmp_filemap.i] 파일에서 다음과 같이 PageReclaim, ClearPageReclaim 함수 구현부가 보입니다.
1 static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) __attribute__((always_inline)) int PageReclaim(struct page *page) { return test_bit(PG_reclaim, &({ ((void)(sizeof(( long)(0 && PageTail(page))))); compound_head(page);})->flags); }
2 static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) __attribute__((always_inline)) void SetPageReclaim(struct page *page) { _set_bit(PG_reclaim,&({ ((void)(sizeof(( long)(1 && PageTail(page))))); compound_head(page);})->flags); } static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) __attribute__((always_inline))
3 void ClearPageReclaim(struct page *page) { _clear_bit(PG_reclaim,&({ ((void)(sizeof(( long)(1 && PageTail(page))))); compound_head(page);})->flags); }
4 # 313 "/home001/austindh.kim/src/raspberry_kernel/linux/include/linux/page-flags.h"
자 그럼 C 코드에서는 함수 구현부가 없는데 전처리 코드에는 보입니다. 참 신기하죠. C 코드에 없는 함수가 전처리 파일에는 있다니 말이죠. 그럼 이런 추정을 해 볼 수 있지 않을까요? 어느 코드인지는 모르겠지만 컴파일될 때 전처리 과정에서 이 함수를 생성하는 거죠.
그럼 어느 코드가 전처리 과정에서 이 함수를 생성하는지 알아볼까요? 이 코드를 찾기 위해서 우선 전처리 코드에서 PageReclaim, SetPageReclaim ClearPageReclaim 함수 전후를 천천히 읽어볼 필요가 있습니다. 그런데 4번째 줄 해더 파일이 보이죠? 이 해더 파일 313라인 코드를 좀 볼까요.
# 313 "/home001/austindh.kim/src/raspberry_kernel/linux/include/linux/page-flags.h"
[linux/include/linux/page-flags.h] 해더 파일을 열어보면 다음 코드가 있습니다.
PAGEFLAG(Reclaim, reclaim, PF_NO_TAIL)
TESTCLEARFLAG(Reclaim, reclaim, PF_NO_TAIL)
위 코드가 전처리 과정에서 PageReclaim, ClearPageReclaim 함수를 생성하는 매크로입니다. 달리 설명해 드리면 이 매크로가 PageReclaim, ClearPageReclaim 함수 구현부라고 말할 수도 있습니다. 평소 C 코드로 리눅스 커널 코드를 분석하는 분들은 매우 낯설게 느낄 수 있는데요.
그러면 어떻게 위 매크로가 컴파일 도중 전처리 과정에서 PageReclaim, ClearPageReclaim 함수로 치환되는지 확인해볼까요?
우선 이제 PAGEFLAG 매크로가 어떻게 구현됐는지 확인해야겠죠. [linux/include/linux/page-flags.h] 해더 파일에서 PAGEFLAG란 매크로를 검색하니 다음 코드가 보이네요. PAGEFLAG 매크로는 uname, lname, policy란 입력을 받아서, TESTPAGEFLAG, SETPAGEFLAG, CLEARPAGEFLAG 매크로로 치환됩니다.
#define PAGEFLAG(uname, lname, policy) \
TESTPAGEFLAG(uname, lname, policy) \
SETPAGEFLAG(uname, lname, policy) \
CLEARPAGEFLAG(uname, lname, policy)
매크로가 치환된다는 의미는 어떤 C 코드에 PAGEFLAG(uname, lname, policy); 매크로를 쓰면, 컴파일러는 PAGEFLAG 매크로 대신 다음 코드를 붙힌다는 것이죠.
TESTPAGEFLAG(uname, lname, policy) \
SETPAGEFLAG(uname, lname, policy) \
CLEARPAGEFLAG(uname, lname, policy)
그럼 각각 매크로 구현부 살펴볼게요.
우선 먼저, TESTPAGEFLAG 매크로를 우선 볼게요. TESTPAGEFLAG 매크로는 다른 매크로로 치횐되지는 않네요. 이제 매크로 분석의 종착역에 온 듯합니다.
1 #define TESTPAGEFLAG(uname, lname, policy) \
2 static __always_inline int Page##uname(struct page *page) \
3 { return test_bit(PG_##lname, &policy(page, 0)->flags); }
이제 코드를 차근차근 살펴볼까요?
TESTPAGEFLAG 매크로는 uname, lname, policy 세 개 입력 인자를 받습니다. 이중 첫 번째 인자인 uname 입력을 받아서 "Page##uname"란 인라인 형태의 함수를 선언합니다. 희한한 점이 입력 인자와 "Page##uname" 함수에 ## 기호가 붙어 있네요. 참고로 함수의 파라미터는 (struct page *)page로 페이지 디스크립터 이군요.
이 매크로 uname인자로 Reclaim이 전달됩니다. [linux/include/linux/page-flags.h] 해더 파일을 잠깐 되돌아가면 “PAGEFLAG(Reclaim, reclaim, PF_NO_TAIL)” 로 호출하고 있죠. 이 규칙에 따라 Page##uname은 PageReclaim 함수로 치환됩니다.
다음은 3번째 줄 코드인데요. 두 번째 파라미터인 lname을 입력으로 받아 "PG_##lname"란 규칙으로 코드를 생성합니다.
3 test_bit(PG_##lname, &policy(page, 0)->flags);
이번에 lname 인자로 reclaim이 전달되니 test_bit 함수 인자는 다음과 같이 치환됩니다.
test_bit(PG_##lname, &policy(page, 0)->flags);
test_bit(PG_reclaim, &policy(page, 0)->flags);
이제 다음 해더 파일에서 PAGEFLAG 매크로에 전달된 인자가 TESTPAGEFLAG 까지 어떻게 전달하는지 점검해야겠죠?
[linux/include/linux/page-flags.h]
PAGEFLAG(Reclaim, reclaim, PF_NO_TAIL)
TESTCLEARFLAG(Reclaim, reclaim, PF_NO_TAIL)
PAGEFLAG 매크로에 전달되는 인자들은 각각 다음과 같습니다.
uname: Reclaim
lname: reclaim
policy: PF_NO_TAIL
그럼 이해를 돕기 위해 TESTPAGEFLAG 부터 각각 매크로 입력 코드를 위 파라미터로 바꿔서 표현하면 다음과 같습니다. 아래 코드에서 화살표로 표시된 부분을 눈여겨 보세요.
#define TESTPAGEFLAG(uname, lname, policy) \
-> #define TESTPAGEFLAG(Reclaim, reclaim, PF_NO_TAIL) \
static __always_inline int Page##uname(struct page *page) \
{ return test_bit(PG_##lname, &policy(page, 0)->flags); }
-> static __always_inline int PageReclaim(struct page *page) \
{ return test_bit(PG_reclaim, &policy(page, 0)->flags); }
SETPAGEFLAG와 CLEARPAGEFLAG 매크로도 비슷한 원리로 전처리 과정에서 SetPageReclaim, ClearPageReclaim 함수를 생성합니다. 파라미터가 치환되는 과정은 다음 화살표로 지정된 코드의 볼드체를 확인해 주세요.
#define SETPAGEFLAG(uname, lname, policy) \
static __always_inline void SetPage##uname(struct page *page) \
{ set_bit(PG_##lname, &policy(page, 1)->flags); }
->static __always_inline void SetPageReclaim(struct page *page) \
{ set_bit(PG_reclaim, &policy(page, 1)->flags); }
#define CLEARPAGEFLAG(uname, lname, policy) \
static __always_inline void ClearPage##uname(struct page *page) \
{ clear_bit(PG_##lname, &policy(page, 1)->flags); }
-> static __always_inline void ClearPageReclaim struct page *page) \
{ clear_bit(PG_reclaim, &policy(page, 1)->flags); }
이렇게 C 코드를 전처리하는 과정에서 매크로를 인라인 함수 형태로 치환하니 C 코드에서 PageReclaim, ClearPageReclaim 함수를 볼 수 없었던 겁니다.
그런데 ##uname을 입력으로 PageReclaim, ClearPageReclaim 코드를 생성하는 과정이 좀 낯설 수도 있는데요. ##을 이용하면, Define으로 선언된 매크로에 파라미터로 Argument를 전달할 수 있습니다. 함수에 파라미터를 전달하듯이 말이죠. 그런데 이 기법은 리눅스 커널뿐만 아니라 원래 C언어가 제공하는 기능입니다. 그래서 C언어를 쓰는 다른 RTOS(Real Time OS)에서도 이 기법을 자주 활용하고 있죠.
그럼 여기까지 분석한 내용을 참고해서 매크로를 한번 만들어볼까요? 매크로로 인라인 타입 함수를 생성하는 과정을 살펴봤으니 이번에는 전역 변수를 생성하는 매크로입니다.
CRASH_STATUS_OUT란 매크로를 다음과 같이 선언하고 다른 코드에서 CRASH_STATUS_OUT 매크로에 입력으로 now, prev를 argument로 전달하는 것이죠.
#define CRASH_STATUS_OUT (CURRENT, PREVIOUS) \
int raspberry_crash_##CURRENT; \
int raspberry_crash_##PREVIOUS; \
CRASH_STATUS_OUT(now, prev)
위 코드를 컴파일하면 전처리 과정에서 다음과 같은 전역 변수를 생성합니다. 이 이유는 now, prev란 Argument를 CRASH_STATUS_OUT 매크로가 받아서 치환하기 때문이죠.
int raspberry_crash_now;
int raspberry_crash_prev;
그럼 다음과 같이 코드를 수정한 후 컴파일을 해볼게요. 혹시 오타로 컴파일 에러를 만나면 에러 메시지를 차근차근 읽으면서 수정하시길 바래요.
diff --git a/mm/filemap.c b/mm/filemap.c
index edfb90e..da9d40a 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -47,6 +47,12 @@
#include <asm/mman.h>
+#define CRASH_STATUS_OUT(CURRENT, PREVIOUS) \
+int raspberry_crash_##CURRENT; \
+int raspberry_crash_##PREVIOUS;
+
+CRASH_STATUS_OUT(now, prev)
+
*
* Shared mappings implemented 30.11.1994. It's not fully working yet,
* though.
커널 빌드가 끝나면 생성되는 [mm/.tmp_filemap.i] 전처리 파일을 열어 보면 다음과 같이 int raspberry_crash_now; int raspberry_crash_prev 전역 변수가 생성된 것을 확인할 수 있습니다.
int raspberry_crash_now; int raspberry_crash_prev;
44293 # 119 "/home001/austindh.kim/src/raspberry_kernel/linux/mm/filemap.c"
44294 static int page_cache_tree_insert(struct address_space *mapping,
이번에도 C 코드에서는 raspberry_crash_now, raspberry_crash_prev란 전역 변수를 볼 수 없습니다. 그럼 이 변수들은 언제 생성된 걸까요? CRASH_STATUS_OUT 란 매크로가 컴파일 될 때 전처리 과정에서 전역 변수를 생성하는 것이죠.
이렇게 코드 리뷰를 하다 함수 검색이 안 될 때 전처리 파일을 열어서 확인해 보세요. 이와 같은 기법으로 전처리 과정에서 함수를 생성하는 코드를 만나면 위에서 분석한 과정을 떠올리면서 차근차근 매크로를 따라가 보세요. 그럼 생각보다 빨리 매크로를 분석할 수 있습니다.
'Core BSP 분석 > 리눅스 커널 핵심 분석' 카테고리의 다른 글
[리눅스] printk 아규먼트 포멧 (0) | 2023.05.06 |
---|---|
[C언어] 포인터 (p + 1) 연산 (0) | 2023.05.06 |
[Linux] 컴파일러(Complier) 소개 (0) | 2023.05.06 |
GCC - C언어 매크로(Macro) -(1) (0) | 2023.05.06 |
LKML - [PATCH v2] clk: fix reentrancy of clk_enable() on UP systems (0) | 2023.05.06 |