안녕하세요.
이번 글은 ftrace를 이용해서 whoami 유틸리티 프로그램을 간단하게 추적해 보는 것입니다. 이미 ftrace는 이전 내용을 보셨다면 어렵지 않게 따라 할 수 있을 것입니다.
유저 공간에서 fork() 시스템 콜 함수를 호출하면 유저 프로세스가 실행된다고 이미 알고 있을 것입니다. 그런데 유저 프로세스를 생성하는 목적은 크게 2가지로 분류할 수 있습니다.
- fork() 시스템 콜 함수로 호출해 같은 작업을 프로그램을 여러 프로세스가 나눠서 실행
- exec() 시스템 콜 함수로 새로운 프로그램을 생성해서 실행
보통 첫 번째 방법을 대부분 방식을 이용하지만 이번에는 두 번째 방법인 이미 만들어 놓은 프로그램 파일을 실행할 때를 알아보겠습니다.
1. ftrace 로그 설정
기본적은 설정은 동일하고 이벤트 설정과 콜 스택을 지정하는 명령어 부분만 확인해 보겠습니다. ftrace_log 디렉터리를 하나 만들고 그 안에서 작업을 했습니다.
1) ftrace 이벤트 설정 부분
ftrace 이벤트에서 sched_ 로 시작하면 보통 프로세스 스케줄링 동작을 출력하는 것을 예상할 수 있습니다. 그중에 프로세스 생성, 실행, 종료, 자원해제 한 동작을 추적하는 이벤트를 지원합니다. 이 이벤트를 활성화합니다.
sched_process_exit : 프로세스 생성
sched_process_fork : 프로세스 실행
sched_process_free : 프로세스 종료
sched_process_exec : 프로세스 자원(메모리, 태스크 디스크립터) 해제
2) 함수의 콜 스택을 위한 필터 설정을 지정하는 명령 이벤트 설정 부분
이름을 포함하는 함수들의 호출 정보 (진입 시점과 반환 시점)를 기록합니다. 즉, 콜 스택을 보기 위해서 필터를 지정합니다.
sys_clone: 새로운 프로세스 (또는 스레드)를 생성하는 시스템 콜입니다.
do_exit: 현재 프로세스를 종료시키는 커널 함수입니다.
search_binary_handler.part.*: 실행 가능한 바이너리 파일을 찾고 해당 형식에 맞는 로더를 호출하는 함수입니다. exec() 계열 시스템 콜 처리 과정에서 중요한 역할을 합니다.
책에 있는 search_binary_handler 하면 로그 파일에 함수가 나타나지 않습니다. 아래 명령어로 ftrace가 추적할 수 있는 함수 목록을 확인하여 search_binary_handler와 유사한 이름이 있는지 찾아봅니다. 그러면 search_binary_handler.part.x 가 있다는 것을 확인할 수 있습니다.
cat /sys/kernel/debug/tracing/available_filter_functions | grep search_binary
copy_process.part.*: 새로운 프로세스를 생성하는 핵심 함수인 copy_process 함수의 특정 부분입니다. 새로운 프로세스를 생성하는 복잡한 과정을 수행합니다.
이 과정을 효율적으로 관리하고 코드를 모듈화 하기 위해 개발자들은 이 함수를 여러 개의 작은 부분으로 나눌 수 있습니다. .part 뒤의 숫자는 이러한 세부 단계를 나타내는 일종의 인덱스라고 볼 수 있습니다.
책에서 copy_process.part.5로 하면 에러가 발생합니다. 사용하는 라즈베리파이 커널버전에 따라 다를 수 있으니 *로 하신 후 확인 log파일에서 part. 뒤를 확인하시면 됩니다.
echo sys_clone do_exit search_binary_handler search_binary_handler.part.* copy_process.part.* > /sys/kernel/debug/tracing/set_ftrace_filter
2. ftrace 로그 분석
위에서 실행한 ftrace의 로그를 파일에는 whoami 명령어를 입력했을 때 프로세스 실행 정보가 있습니다.
rpi_kernel_src/ftrace_log# vim ftrace_log.c
26~31번 줄은 이전 글에서 본 것처럼 _do_fork 함수를 호출해 프로세스를 생성하는 동작입니다.
33번 줄은 sched_process_fork 이벤트로 프로세스가 생성한 세부 정보를 알 수 있습니다. pid가 832인 bash 프로세스가 pid가 1354인 프로세스를 생성한다는 의미입니다. 생성하는 프로세서의 이름이 부모 프로세스의 이름과 같지만 프로세스 생성이 끝난 시점에는 자신만의 이름을 갖습니다. 복제되는 단계에서 부모이름과 리소스를 복제합니다.
34~39번 줄은 콜스택으로 39번 줄에서 호출방향으로 생각하면 됩니다. 38번 줄의 execve( ) 함수는 execve 시스템 콜의 핸들러 함수 이므로 유저 공간에서 execve POSIX 시스템 콜 발생했다는 사실을 알 수 있습니다. 이는 fork( ) 시스템 콜 이후에 execve( ) 시스템 콜을 유저 공간에서 실행했다는 사실을 알 수 있습니다.
41번 줄은 sched_process_exec 이벤트를 통해 프로세스의 실행 정보를 출력한 것입니다. 이것은 실행을 시작한 pid가 1354인 whoami 프로세스가 /usr/bin/whoami 인 것으로 알 수 있습니다.
sys_execve() 함수가 호출된 후 다음 함수 흐름으로 exec_binprm() 함수가 호출됩니다.
rpi_kernel_src/ftrace_log# vim ../linux/fs/exec.c
1700번 줄에서 whoami 프로세스가 실행을 시작하는 동작을 ftrace의 sched_process_exec 이벤트로 출력합니다. /usr/bin/whoami 자체는 프로세스가 아닙니다. 커널이 이 파일을 메모리에 적제 해서 실행할 때가 프로세스입니다.
다시 ftrace의 42~47번 줄을 보면 종료되는 로그입니다. root라는 결과를 출력하고 프로세스가 바로 종료한 것입니다. 이것은 유저 공간에서 exit() 함수를 호출하면 실행되는 코드의 흐림입니다.
마지막으로 46번 줄에 실행주소 오프셋이 __wake_up_parent+0x0 인 부분이 있습니다. 이것은 이전 내용에 이야기한 것처럼 한번 더 확인 이 필요합니다.
ARM 아키텍처에서는 파이프라인을 적용하기 때문에 실제로 실행된 코드 주소에서 +0x04만큼 떨어진 주소를 프로그램 카운터로 저장합니다.
실제 어셈블리 코드를 보면 sys_exit_group()+0xc가 됩니다. 그렇기 때문에 sys_exit_group()의 마지막 코드에서 do_group_exit() 함수를 호출합니다.
책에서는 cat 명령어도 분석하지만, 내용이 비슷하기 때문에 넘어가도록 하겠습니다. 그리고 책 내용이 pi 3로 제작되었기 때문에 조금 확인해 가며 봐야 할 부분들이 있는 것 같습니다.
감사합니다.
<참고 자료>
1. [도서] 디버깅을 통해 배우는 리눅스 커널의 구조와 원리 p269 ~ 277, wikibook