ftrace(function+tracer)는 커널 내부에서 어떤 일이 일어나는 지 확인하거나, 디버깅 혹은 성능 분석에 많이 사용되는 기능이다. 아래의 그림은 function_graph tracer을 이용하여 call chain을 확인한 것이다.
많은 다른 포스팅에서 ftrace를 소개하고 사용법도 잘 설명하기 때문에 해당 포스트에서는 구체적인 사용법보다는 원리에 초점을 맞춘다.
Simple usage
ftrace은 package manager을 통해 설치하는 유틸리티가 아니고, 커널에 내장되어 있는 기능이다. 사용자가 ftrace를 다루기 위해서는 debugfs을 이용해야 한다.
대부분의 배포판들은 /sys/kernel/debug 경로에 debugfs를 마운트한다. 아래의 커맨드와 같이 debugfs가 어디에 마운트되어 있는지 확인할 수도 있다. 만약 마운트되어 있지 않다면 직접 마운트 하면 된다.
/sys/kernel/tracing 경로로 이동하고(root 권한이 필요하다), 해당 폴더에 있는 파일들을 조회하면 아래와 같은 결과를 얻을 수 있다.
만약 영어에 능숙하다면, README 파일을 통해 사용법을 익힐 수 있다. 다양한 파일들이 존재하는데, ftrace는 사실 function tracer의 기능 뿐만 아니라 tracing infrastructure에 가까운 성격을 가진다. 즉, 해당 인프라 위에 다양한 종류의 tracer들이 존재하고 각각의 tracer를 컨트롤하기 위해 저렇게 많은 파일들이 존재한다.
또한 해당 경로에 존재하는 파일들은 일반 파일이 아니다(삭제도 할 수 없다!). 각 파일들은 개별적으로 등록된 핸들러가 존재하고, 그 핸들러에서는 read 또는 write 시스템 콜을 통해 전달된 버퍼의 값을 사용하여 어떤 행동을 취할지 결정한다.
예를 들어 아래와 같은 커맨드를 실행하면, tracing_on에 등록된 write handler가 실행하게 된다. 입력 값이 0이면 traing을 off하고 아니라면 on하는 기능을 수행한다.
앞서 말했듯이, ftrace 인프라 위에 다양한 tracer들이 존재한다. 사용 가능한 tracer을 보기 위해서는 available_tracers 파일을 읽으면 된다.
이 중에서 해당 포스트에서 다룰 tracer는 function, function_graph tracer이다. 간단하게 해당 tracer을 이용하는 방법으로는, 아래와 같은 커맨드를 순서대로 입력하면 된다.
위에서는 function_graph tracer을 선택하였고, tracing되는 결과를 얻기 위해서는 trace 또는 trace_pipe 파일을 읽으면 된다.
이 밖에 pid, function, stack depth에 대한 다양한 필터들을 설정할 수 있다. 자세한 내용은 해당 포스트에서는 다루진 않는다.
이와 같이 유용한 기능을 제공하는 function tracer은 어떻게 구현되는지에 대해서 이제 알아보도록 하자.
Implementation
gcc는 다양한 옵션들이 있는데, 그 중에서 프로파일을 위한 옵션이 존재하는데 그 중 하나의 옵션이 -pg 옵션이다. manual 페이지에서는 다음과 같이 설명되어 있다.
설명에 따르면, -pg 옵션을 사용하면 profile을 위한 추가적은 코드를 생성한다고 한다. 구체적으로 어떤 코드가 어디에 생성되는 지 확인하기 위해 간단한 프로그램을 -pg 옵션을 넣고 컴파일 한다.
위와 같은 코드를 하나는 그냥 컴파일하고 하나는 -pg 옵션을 주고 컴파일을 진행하고 objdump를 통해 두 오브젝트 파일을 비교한 결과는 아래와 같다.
11번 라인과 21번 라인을 보면 pg 옵션을 넣고 빌드한 오브젝트 파일에 별도의 함수 호출이 있는 것을 확인할 수 있다. 해당 함수 호출은 relocation 섹션을 살펴보면 알 수 있듯이 mcount 함수에 대한 호출이다.
이때, 22번과 23번 라인은 함수 인자들을 스택을 저장하는 과정인데, mcount 함수는 이 과정 전에 호출된다. 따라서 mcount 함수는 일반적인 함수 호출 규약과는 다르게, parameter을 save/restore하는 로직이 포함되어야 한다.
커널에 ftrace를 도입한 패치("ftrace: add basic support for gcc profiler instrumentation")에 구현된 mcount를 살펴보면, parameter을 save/restore하는 구현부를 볼 수 있다.
Line 11~18는 parameter을 저장하고, Line 25~32는 parameter을 복원하는 부분이다. Line 20~21은 caller와 callee의 instruction pointer 위치를 parameter로 세팅하는 부분이다. 이해를 돕기 위해 23번 라인을 실행하기 직전의 stack을 그림으로 표현하였다.
따라서, ftrace_trace_function은 caller와 callee의 주소를 이용하여 다양한 정보들을 보여줄 수 있으며 대표적으로 symbol 정보들을 보여줄 수도 있다. 이 원리를 이용하면 간단하게 나만의 function tracer을 구현할 수 있다.
Simple tracer
위에서 소개한 원리를 이용한 small tracer을 소개하도록 하겠다. 아래와 같이 프로젝트를 clone하면 된다.
$ git clone https://github.com/YWHyuk/small_tracer.git
해당 프로젝트는 mcount.S와 심볼 정보들을 파싱하기 위한 symbol.py 스크립트로 구성되어 있다. symbol.py 스크립트는 addr2line 유틸리티를 이용하여 instruction pointer에 대응하는 함수의 심볼을 가지고 온다.
프로젝트에 포함되어 있는 예제 코드 main.c를 아래와 같이 빌드를 하고 실행하면 a.out 이름의 실행 가능한 파일이 생성된다.
$ gcc -pg mcount.S main.c
해당 프로그램을 실행하기 앞서 main.c를 살펴보자. 해당 샘플 코드는 main 함수에서
foo() 함수를 호출하는 bar() 함수를 호출한다.
재귀 함수 recursive() 함수를 호출한다
이제 해당 파일을 실행하면, 아래와 같이 기본적인 callee, caller 관계가 출력되는 것을 관찰할 수 있다.
이러한 tracer에 관심이 있다면, Namhyung님의 uftrace를 살펴보면 좋을 것 같다(uftrace는 small tracer와는 비교가 안되게 놀라운 기능들을 포함하고 있다!). 실제로 user application을 tracing하는 것은 해당 포스트에서 다루지 못한 다른 이슈들이 존재한다(library...). 해당 링크는 uftrace에 대한 기술적인 내용들을 설명하고 있으니 참고하면 도움이 될 것 같다.
コメント