Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

98258
1323
114600


[리눅스커널] 메모리관리/디버깅: 가상주소를 물리주소로 변환 확인 실습 14. Memory Management

이번 소절에서는 가상주소를 물리주소로 변환하는 과정을 실습으로 알아봅시다.

3장 3.6 절에 소개한 rpi_debugfs.c 소스를 활용해 실습을 진행합니다. 먼저 rpi_debugfs.c 소스를 입력하고 커널 빌드를 하시기 바랍니다. 

패치 코드 입력 방법 알아보기

먼저 입력할 패치 코드부터 소개합니다.
[drivers/soc/bcm/rpi_debugfs.c]  
01 static int rpi_kernel_debug_stat_set(void *data, u64 val)
02 {
03     int ret;
04
05    raspbian_debug_state = (uint32_t)val;
06    
07    if ( raspbian_debug_state == 1402 ) {
08 unsigned long phys_address, page_frame_num;
09 struct page *page_ptr;
10
11 void * func_ptr = schedule;
12 unsigned long vir_address = (unsigned long)func_ptr;
13
14 phys_address = __pa(vir_address);
15 page_frame_num = virt_to_pfn(vir_address);
16
17 page_ptr = virt_to_page(vir_address);
18
19 trace_printk("[+]sym: %pS vir_address: 0x%lx phys_address: 0x%lx pfn: 0x%lx page_ptr:%p\n", 
20 func_ptr, vir_address, phys_address, page_frame_num, page_ptr);
21   }

원래 위에서 본 패치 코드를 입력하기 전 rpi_kernel_debug_stat_set() 함수 구현부는 다음과 같았습니다. 패치 코드의 07~21번째 줄 코드를 다음 오리지널 코드 04번째 줄 부분에 입력하면 됩니다.
01 static int rpi_kernel_debug_stat_set(void *data, u64 val)
02 {
03    raspbian_debug_state = (uint32_t)val;
04
05    printk("[rpi] [%s][L:%d], raspbian_debug_state[%lu],value[%lu]===\n", 
__func__, __LINE__, (long unsigned int)raspbian_debug_state, (long unsigned int)val);
06
07    return 0;
08 }

패치 코드 입력 방법을 알아봤으니 이제 패치 코드 내용을 소개합니다.

11번째 줄 코드를 보겠습니다.
11 void * func_ptr = schedule;

schedule() 함수 주소를 func_ptr 포인터에 저장합니다. 라즈비안 커널은 가상주소 기반으로 구동하므로 schedule() 함수 주소는 가상 주소입니다.

다음 12번째 줄 코드입니다.
12 unsigned long vir_address = (unsigned long)func_ptr;
 
func_ptr 포인터를 unsigned long 타입으로 캐스팅해 vir_address 지역 변수에 저장합니다. schedule() 함수 주소를 vir_address 지역 변수로 저장하려는 목적입니다.

다음 14번째 줄입니다.
14 phys_address = __pa(vir_address);

커널에서 제공하는 __pa() 함수를 사용해 가상 주소를 담고 있는 vir_address를 물리주소로 변환합니다. 결과 phys_address 변수는 물리 주소를 저장하게 됩니다.

이번에는 15번째 줄 코드를 보겠습니다.
15 page_frame_num = virt_to_pfn(vir_address);

virt_to_pfn() 함수를 사용해 가상 주소인 vir_address에 대한 페이지 프레임 번호를 page_frame_num지역 변수에 저장합니다.

    가상 주소에 대한 페이지 프레임 번호를 계산하는 코드입니다.

마지막 19~20 번째 줄 로그는 이 정보를 ftrace로 출력하는 동작입니다.
19 trace_printk("[+]sym: %pS vir_address: 0x%lx phys_address: 0x%lx pfn: 0x%lx page_ptr:%p\n", 
20 func_ptr, vir_address, phys_address, page_frame_num, page_ptr);

trace_printk() 함수로 출력하는 세부 정보는 다음과 같습니다.
sym: %pS = func_ptr // func_ptr 주소에 대한 심볼 정보 
vir_address: 0x%lx = vir_address // vir_address 주소 
phys_address: 0x%lx = phys_address // phys_address 주소 
pfn: 0x%lx = page_frame_num // 페이지 프레임 번호
page_ptr:%p = page_ptr  // 페이지 디스크립터 주소

출력 정보는 주석문 내용을 참고하세요.
실습 코드를 입력한 후 컴파일 후 라즈비안에 설치를 합니다. 다음 라즈베리파이를 재부팅시킵니다.

ftrace를 설정하고 로그를 추출해보기

이어서 ftrace 로그를 설정하는 방법을 소개합니다.
#!/bin/bash

echo 0 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo "tracing_off" 

echo 0 > /sys/kernel/debug/tracing/events/enable
sleep 1
echo "events disabled"

echo  secondary_start_kernel  > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter init"

echo function > /sys/kernel/debug/tracing/current_tracer
sleep 1
echo "function tracer enabled"

echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
sleep 1
echo "event enabled"
echo  rpi_kernel_debug_stat_set > /sys/kernel/debug/tracing/set_ftrace_filter

sleep 1
echo "set_ftrace_filter enabled"

echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
echo 1 > /sys/kernel/debug/tracing/options/sym-offset
echo "function stack trace enabled"

echo 1 > /sys/kernel/debug/tracing/tracing_on
echo "tracing_on"

위 명령어를 입력한 후 vir_mem_test.sh 이름으로 저장합니다. 

ftrace 설정 셸 스크립트는 이전 소절에서 다룬 내용과 다르지 않습니다. 대신 다음 명령어가 추가됐습니다.
echo rpi_kernel_debug_stat_set > /sys/kernel/debug/tracing/set_ftrace_filter

rpi_kernel_debug_stat_set() 함수 콜스택을 ftrace로 출력하기 위한 명령어입니다.

vir_mem_test.sh 셸 스크립트를 실행해 ftrace를 설정합니다.
root@raspberrypi:/home/pi# ./vir_mem_test.sh

이어서 다음 명령어를 입력합니다.
root@raspberrypi:/home/pi# echo 1402 > /sys/kernel/debug/rpi_debug/val

위 명령어를 입력하면 raspbian_debug_state 전역 변수가 1402로 바뀝니다.
결과 rpi_kernel_debug_stat_set() 함수 08~20번째 줄 코드를 실행합니다. 
01 static int rpi_kernel_debug_stat_set(void *data, u64 val)
02 {
03     int ret;
04
05    raspbian_debug_state = (uint32_t)val;
06    
07    if ( raspbian_debug_state == 1402 ) {
08 unsigned long phys_address, page_frame_num;
09 struct page *page_ptr;
...
19 trace_printk("[+]sym: %pS vir_address: 0x%lx phys_address: 0x%lx pfn: 0x%lx page_ptr:%p\n", 
20 func_ptr, vir_address, phys_address, page_frame_num, page_ptr);
21   }

이어서 get_ftrace.sh 셸 스크립트를 실행해 ftrace 로그를 받습니다. 
root@raspberrypi:/home/pi# ./get_ftrace.sh

get_ftrace.sh 셸 스크립트는 이전 챕터에서 소개했으니 코드 내용은 생략합니다.

여기까지 실습 과정을 정리해볼까요?
1. rpi_kernel_debug_stat_set() 함수에 실습 코드를 작성한다.
2. 커널을 빌드하고 라즈비안에 커널 이미지를 설치한다.
3. ftrace를 설정하고 다음 명령어를 실행해 실습 코드를 실행한다.
root@raspberrypi:/home/pi# echo 1402 > /sys/kernel/debug/rpi_debug/val
4. get_ftrace.sh 셸 스크립트를 실행해 ftrace를 추출한다.

ftrace 로그 분석하기

분석할 ftrace 로그를 소개합니다.
01 bash-863 [000] .... 742.800103: rpi_kernel_debug_stat_set+0x14/0x428 <-simple_attr_write+0xbc/0x104
02 bash-863 [000] .... 742.800155: <stack trace>
03 => rpi_kernel_debug_stat_set+0x18/0x428
04 => simple_attr_write+0xbc/0x104
05 => full_proxy_write+0x64/0x80
06 => __vfs_write+0x4c/0x170
07 => vfs_write+0xb4/0x1c0
08 => ksys_write+0x5c/0xbc
09 => sys_write+0x18/0x1c
10 => ret_fast_syscall+0x0/0x28
11 => 0x7ed602d4
12 bash-863 [000] .... 742.800168: rpi_kernel_debug_stat_set+0xa0/0x428: [+]sym: schedule+0x0/0xa8 vir_address: 0x807a0a8c phys_address: 0x7a0a8c pfn: 0x7a0 page_ptr:ba3b0280
 
01~11 번째 줄 로그는 특별히 유용한 정보를 담지 않습니다. 단지 rpi_kernel_debug_stat_set() 함수가 실행했다는 정보를 알려줍니다. 

우리가 분석할 메모리 디버깅 정보는 모두 12번째 줄에 모여 있습니다.
12 bash-863 [000] .... 742.800168: rpi_kernel_debug_stat_set+0xa0/0x428: [+]sym: schedule+0x0/0xa8 vir_address: 0x807a0a8c phys_address: 0x7a0a8c pfn: 0x7a0 page_ptr:ba3b0280

12번째 줄에 담긴 메시지는 다음과 같습니다.
[+]sym: schedule+0x0/0xa8 // 0x807a0a8c 주소 심볼 정보
vir_address: 0x807a0a8c     // schedule() 함수 주소(가상주소)
phys_address: 0x7a0a8c     // 0x807a0a8c 가상 주소에 대한 물리주소
pfn: 0x7a0           // 페이지 프레임 번호
page_ptr:ba3b0280           // 페이지 디스크립터 주소

세부 설명은 주석문 내용을 참고하세요.

이번 소절에서는 가상주소를 물리주소로 변환하는 원리를 파악하는 것이 목적이니 각각 메시지의 의미를 알아보겠습니다. 먼저 가상 주소에서 물리주소로 어떻게 변환하는지 알아볼까요?

물리주소는 다음 계산식으로 변환됩니다.
0x7a0a8c(물리주소) = 0x807a0a8c(가상주소) - 0x8000_0000  


이어서 페이지 프레임 번호를 계산해볼까요? 계산식은 다음과 같습니다.
물리주소 >> 12
0x7a0a8c >> 12
11110100000101010001100 >> 12
---------------------------------------------
11110100000 
0x7a0

0x7a0a8c 물리주소에 대한 페이지 프레임 번호는 0x7a0입니다. 14.2 절에서 배운 바와 같이 물리 주소를 왼쪽으로 12만큼 비트 시프트한 결과가 페이지 프레임 번호입니다.

이번 시간에 소개한 패치 코드를 활용하면 커널에서 가상주소로 존재하는 커널 함수나 전역 변수의 물리 주소와 페이지 프레임 번호를 ftrace 로그로 알아낼 수 있습니다. 가상 주소를 물리 주소로 변환하는 과정을 더 빨리 배울 수 있습니다.


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

Reference(커널 메모리 소개) 
가상 주소를 물리 주소로 어떻게 변환할까?   
메모리 존(Zone)에 대해서   
커널 메모리 할당은 어떻게 할까   
슬랩 메모리 할당자와 kmalloc 슬랩 캐시 분석   
커널 메모리 디버깅




핑백

덧글

댓글 입력 영역