Jockey - A User-space Library for Record-replay Debugging

리눅스 상에서 Record & Replay 를 이용한 디버깅 방법을 제안한 논문을 번역한 것이다.

ABSTRACT

jockey 는 리눅스 프로그램을 디버깅하기 위한 record & replay 실행 툴이다.
그것은 시간 의존적인 영향이 있는 시스템 콜과 CPU 명령어들의 호출을 record 하고, 나중에 결정적으로 replay 한다.
그것은 긴 실행 프로그램을 진단하기 위해서 프로세스 체크포인트를 지원한다.
jockey 는 타겟 프로세스의 부분으로서 실행되는 공유 오브젝트 파일로서 실행된다.
이 디자인이 안전성과 사용 용이함의 jockey 의 목적을 달성하는 키인 동안, 그것은 또한 도전한다.
이 논문은 실용적인 이슈에 대해서 의논한다. 환경을 뛰어넘을 필요가 있는 낮은 오버헤드 시스템콜 가로챔, jockey 와 타겟 프로세스 사이의 리소스 사용을 분리하는 기술, 그리고 jockey 의 동작을 작게 콘트롤을 위한 인터페이스를 포함한다.

INTRODUCTION

jockey 는 리눅스에서 동작하는 record & replay 툴이다. 그것은 보통 프로그램의 실행을 기록하고 나중에 결정적으로 replay 한다. jockey 는 복잡한 방식으로 운영체제 또는 다른 컴퓨터들과 커뮤니케이션하는 대화식 또는 분산된 프로그램을 디버깅하는데 도움을 주기위해 설계되었다. 우리는 http://www.freshmeat.net 에서 jockey 를 공개하고 있다.
jockey 는 원래 FAB(Federated Array of Bricks)를 디버깅하는데 돕기 위함으로 개발되어 졌다. FAB 는 상용 서버들의 클러스터에 구축된 고가용성 디스크 정렬(array)이다. 그것은 복잡한 피어-투-피어 스타일의 응답과 말소-코딩 프로토콜을 사용함으로서 iSCSI 클라이언트들이 논리적 볼륨에 접근하는 것을 제공한다.
예를 들면 gdb 같은 전통적인 디버거들은 single-node, 순차적 프로그램을 디버깅하기 위한 포괄적인 지원을 제공한다.
그것들은 그러나 FAB 와 같은 대화식이나 분산된 프로그램에는 유용하지 않다. 우리는 3 가지 문제점을 확인하고 어떻게 jockey 를 완화시킬 것인지 논의한다.
첫째로, 프로그램의 실행은 본래적으로 비결정적이다. 프로세스의 동작은 분기할 것이고, OS, 사용자, 다른 프로세스 와의 대화에 의존한다.
jockey 는 이러한 프로그램을 디버깅하는 것을 도와준다. 모든 비결정적인 선택 프로세스 생성을 record 하고 개발자가 원할 때 여러번 replay 한다. 그래서 비결정적인 프로그램을 위한 디버깅은 순차적, 반복적인 프로그램에 줄어든다.
둘째, 이런 프로그램들은 긴 주기동안 자주 실행된다. 그들은 많은 리소스를 필요로 하거나(과학적인 계산), 서버 프로그램(FAB 와 분산된 해쉬 테이블) 또는 사용자 대화가 상당히 필요하기 때문에.
간단한 버그의 재생산은 개발자의 끈기로 자주 테스트할 수 있다. jockey 는 실행 도중에 프로세스의 상태를 투명하게 체크포인팅함으로 인한 문제를 경감시킨다. 개발자는 어떤 체크포인트로 부터 replay 를 시작할 수 있고, 문제를 진단하기 위해 실행의 히스토리를 통해 쉽게 “시간 여행” 을 할 수 있다. 체크포인팅은 또한 저장 공간 오버헤드를 증가시킨다. 체크포인트보다 오래된 log record 가 버려지게 됨으로서.
셋째, FAB 와 같은 분산된 시스템 실행은 여러 대의 컴퓨터들 상에서 프로세스가 시작되기를 요구된다. 이것은 귀찮고, 프로그램 개발을 위해 회전(turn-around) 시간이 증가된다. jockey 는 각 프로세스에 독립적으로 record & replay 함으로서 이 문제를 경감시킨다. 전체 시스템의 실행을 record 한 후에, 개발자는 전통적인 디버거에서 각 프로세스를 replay 할 수 있다.
이것은 또한 제한이 있다. jockey 는 동시에 전체 시스템의 실행을 조사하길 원할 때는 덜 유용할 수 있다. 우리는 섹션 5.2 에서 우리의 경험에 대해 논의할 것이다.
이 섹션의 나머지는 jockey 의 설계, 그것의 장점, 과제에 대해서 간략히 알아본다.

jockey 는 두 가지의 실용적인 목적에 의해 설계되었다. 첫번째는 사용하기 쉬움이다. jockey 는 반드시 쉽고, 생산에 안전해야 한다. 그것은 타겟 프로그램, 운영체제, 디버거의 변경없이 동작되어야 한다. 두번째는 보편성이다.
jockey 는 일반 리눅스 프로그램을 콘트롤 할 수 있다. 그러나 특별한 프로그래밍 언어나 API, MPI 또는 CORBA 의 경우는 아니다.
우리는 유저 영역 라이브러리를 타겟 프로세스의 부분으로서 실행시킴으로서 jockey 를 수행함으로서 첫번째 목적을 개선했다.
커널 베이스 주제와 대조를 이루어, jockey 는 관리정책이나 패치된 커널없이 누구나 사용할 수 있다. 개발자는 변경없이 친숙한 디버거를 사용함으로서 계속할 수 있다. 게다가 이 설계는 타겟 프로그램 콘트롤을 허용하거나 쉽게 jockey 를 확장을 허용한다. 이것은 섹션 4 에서 논의할 것이다. 우리의 두번째 목적은 공정한 하위 레벨(시스템 콜과 CPU 명령어)에서 record 와 replay 이벤트를 개선하는 것이다.

우리는 jockey 없이 원래의 실행에 동일한 jockey 아래에서 프로그램 실행을 만들려고 노력하지 않는다.
jockey 는 내부적으로 mmap 과 파일 엑세스 실행을 필요로 한다. mmap 주소와 파일 디스크립터 원래의 실행과 record 하는 것의 실행은 다를 수도 있다. 이것은 보통 추가적인 문제의 원인이 되지 않는다. jockey 에 의해서 target 되어진 프로그램이 보통 맨 먼저 비결정적이기 때문에.
또한 성능은 두번째 목표이다. jockey 는 오직 테스팅과 디버깅에 사용되어진다.
그것이 타겟 프로그램의 실행을 질적으로 바꾸지 않는 한, jockey 속도 저하는 받아들일 수 있다.
실제 문제로서, 우리는 섹션 5 에서 jockey 의 오버헤드는 I/O 집중 프로그램에 최대 30% 이다. 좀 더 자주 제로에 닫힌다. 우리의 허용치 제한안에 있다.

jockey 의 요점은 libjockey.so 이며, x86 공유 오브젝트 파일이다. 그림 1 은 리눅스 랜덤 숫자 디바이스인, /dev/random 으로 부터 읽는 단순한 프로그램을 보여준다. 그림 2 는 가장 일반적인 jockey 의 사용을 보여준다. 프로그램 실행을 record 또는 replay 하는 것은 소스 코드 또는 실행 파일을 변경하지 않고 할 수 있다.
간단히 시작할 때 libjockey.so 를 로딩하는 것은 jockey 가 프로세스를 콘트롤하는 원인이 된다. 이 예제는 jockey 가 getc 를 통해 만들어진 read 시스템 콜을 호출하는 것을 가로챈다. 그것은 record 하는 동안 read 값을 기록한다. replay 할 때, 그것은 랜덤 디바이스로 부터 실제적으로 읽지 않고 로그로부터 값을 읽는다. jockey 는 또한 그림 3 과 같이 몇가지 다른 방법으로 실행한다.

타겟 프로세스와 jockey 를 같이 배치시키는 우리의 결정은 도전과 한계가 있다. 첫째, jockey 는 심각한 버그 또는 악의적인 타겟 프로그램에 의해 제대로 동작하지 않을 수 있다. 만일 그렇다면, 예를 들면 타겟 프로그램은 jockey 에 의해 내부적으로 사용된 메모리 영역이 파괴될 수 있다.
우리는 타겟 프로그램과 jockey 사이의 가능한 많은 리소스의 사용을 분리함으로 인한 이 문제에 대해서 얘기한다.
결과적으로 jockey 는 일반 메모리 버그, free 메모리 블럭에 접근하고, off-by-one 정렬에 접근하는 것이 포함된 것을 record, replay 할 수 있다. 리소스 분리는 섹션 3.2 에서 논의한다.
두번째 도전은 시스템 콜에 의해 직접적으로 초기화 되지 않은 이벤트들을 record 하고 replay 하는 것이다. 우리는 우리의 솔루션을 섹션 3.4 와 3.6 에서 2 개의 이벤트 타입, 시그널과 memory-mapped file I/O 를 기술한다.
그러나, 근본적으로 캡쳐하기 불가능한 이벤트가 있다. 예를 들면, 커널 기반의 pthread 와 함께 발생한 메모리 엑세스 race 는 replay 할 수 없다. 쓰레드 컨텍스트 스위치는 jockey 의 콘트롤 밖이기 때문이다.
이런 이유 때문에, jockey 는 커널 멀티 쓰레딩을 지원하지 않는다. 비슷하게 그것은 어떤 프로그램 또는 공유 메모리나 공유 파일을 통한 다른 프로세스(디바이스)와 대화하는 API 는 지원하지 않는다. 예를 들면, memory-mapped network I/O 를 위한 uDAPL.
jockey 는 유저영역 쓰레드(예를 들면, Capriccio - FAB 는 비슷한 패키지에 들어있음)를 지원하지 않는다.

RELATED WORK

record & replay 실행은 효율적인 디버깅 방법으로서 주장해왔다. 이 섹션은 jockey 와 관련한 record & replay 디버깅 연구 이전에 다시 조사한다.

bugnet 은 가장 앞선 결정적인 record & replay 툴 중에 하나다. 그것은 주기적으로 시스템의 체크포인트를 가지며, 프로세스에 의한 I/O 작업들을 가로챈다. bugnet 은 그러나 오직 특별한 API 로 작성된 프로그램을 지원한다. 이와 다르게 jockey 는 일반 리눅스 프로그램을 지원한다. Flashback 은 가장 최근의 일이다. 그것은 jockey 와 비슷한 세트(시스템콜을 record & replay 하고, fork 기반의 체크포인팅)를 제공한다. 그러나 flashback 은 커널 패치로서 제공되어 진다. 그래서 그것은 jockey 보다 덜 쉽고, 덜 안전하다.
약간 다른 연구로서, 어떤 시스템은 각각의 메모리 접근을 record, replay 한다. 그것은 bugnet, flashback 또는 jockey 와 같은 이벤트 기반 연구보다 몇가지 장점을 가지고 있다. 첫번째, 역(리버스) 실행이 가능하다. 글자 그대로 CPU 명령어를 한 스탭씩 뒤쪽으로 할 수 있다. 두번째, 그것은 시스템 콜의 의미나 OS 와 다른 대화하는 것을 깊게 알 필요가 없기 때문에 좀더 일반적이다.
그러나 그것은 특별한 컴파일러와 큰 로깅 오버헤드를 가진다.
더욱이 복잡한 최적화, 이들 시스템들은 CPU 집중적인 프로그램에서 초당 수 메가 바이트의 양에 로그를 생성한다.
jockey 는 이와 대조적으로 프로그램에 초당 오직 수백 바이트를 생성한다. 섹션 5.1 에서 볼 것이다.

Revirt 는 가상 머신으로 낮은 레벨의 인터럽트와 디바이스 활동을 record & replay 한다.
그것은 네트워크 침입을 탐지하고 커널 버그를 진단하는 데, 유용하다는 것을 증명했다. 몇몇 다른 논문들 역시 가상 머신을 사용함으로서 분산된 시스템을 에뮬레이팅하는 것을 얘기했다.
이들 시스템이 강력할 때, 그것 역시 사용하는 데, 귀찮다. 예를 들면, 각 가상 머신에 모든 파일 시스템 트리를 생성할 필요가 있다.
오직 유저 영역 프로그램을 디버깅 하는데 관심이 있을 때 그것은 과잉이다. jockey 는 이들 시스템들보다 단순하고, 쉽게 설계되어졌다.

결정적인 record & replay 는 병렬과 분산 환경에서 대부분 효과적이다. 실제로 가장 빠른 툴은 특히 그런 환경을 타겟으로 했다.
그 때 이래로, 이론적인 개선은 공유 메모리 병렬 프로그램과 메세지 전달 프로그램 둘다를 위해 제안했다. jockey 는 분산 시스템의 결정적인 replay 를 아직 지원하지 않는다. 그것은 오직 시스템 독립적 안에서 프로세스들을 replay 할 수 있다.
우리는 섹션 5.2 에서 논의할 것이고, 우리는 이 제한이 심각한 장애가 아닌 것을 알았다.

IMPLEMENTATION OF JOCKEY

우리는 리눅스 C++ 에서 실행했다.
libjockey.so 를 로딩된 직 후에 타겟 프로그램이 실행을 시작하기 전에 동적 링커(ld.so)는 libjockey.so 를 로딩된 후에 곧 jockey 의 초기화 루틴을 실행한다. 초기화 루틴은 다음 태스크를 실행한다.

  1. 타이밍 또는 컨텍스트 영향과 함께 libc 의 각 시스템 콜을 위해 jockey 는 첫번째 몇 명령어들을 다시 쓰고 그것의 호출을 가로챈다. jockey 는 현재 80 시스템 콜, time, recvfrom 그리고 select 를 가로챈다. jockey 는 record 하는 도중에 이 호출들에 의해 값을 생성하여 기록한다. 그리고 replay 동안에 로그로 부터 값을 읽는다.
  2. jockey 는 비결정적인 영향과 함께 CPU 명령어를 위해 같은 것을 한다. 그것은 현재 오직 rdtsc, CPU 의 타임스탬프 카운터를 읽기위해 x86 명령어를 패치한다. 그것은 예를들면, libc 에 있는 가짜 랜덤 숫자 생성기로서 사용된다.
  3. jockey 는 단지 타겟 프로그램 콘트롤이 돌아가기전에 프로세스 상태를 체크포인트 한다. replay 모드에서는 jockey 는 체크포인트를 load 한다. 체크포인트는 record, replay 둘다 하는 동안 같은 환경 변수 셋과 커맨드 라인 파라미터를 보는 것은 타겟이 허용될 필요가 있다. 우리는 섹션 3.3 에서 좀 더 자세히 알아볼 것이다.
  4. jockey 는 타겟 프로그램의 제어를 넘긴다. 여기서 jockey 는 오직 타겟이 시스템콜 또는 비결정적 CPU 명령을 실행할 때 활성화 된다.

다른 섹션은 첫번째 두번째 단계에 대해서 좀 더 자세히 알아본다. 섹션 3.2 는 타겟 프로그램으로 부터 필요없는 간섭를 피하기 위해 분리하기위한 jockey 의 노력에 대해 논의한다.
섹션 3.3 은 jockey 의 체크포인트 기능에 대해서 묘사하고, 도전과 같이 우리는 극복했다.

예를 들어, 그림 4 는 time 시스템 콜을 어떻게 record 하고 replay 하는지 보여준다. (a) libc.so 에서 time 의 첫번째 몇 CPU 명령어를 보여준다. jockey 가 시작할 때, 그것은 처음 5 바이트에 jmp 명령어를 쓴다.
만일 5 바이트가 다른 CPU 명령어의 중간에 있으면 time 의 경우, jockey 는 다른 명령어 바운더리(필요한 만큼 nop 를 메모리에 채운다)에 덮어쓴다. © 에서 jockey 는 필요하다면 오래된 구현을 실행 시킬 수 있게 하기 위하여, 새로이 할당된 메모리 영역의 함수의 본래 처음 5 바이트(time 은 6 바이트)를 또한 copy 한다.
(d) 는 time 의 새로운 구현에 대한 엔트리 포인트의 슈드 코드를 보여준다. 우리는 섹션 3.2 에서 논의함으로서, jockey 는 흩어진 스택상의 시스템 콜을 가로채고 타락한 타겟 메모리를 피하기 위해서 이코드는 동적으로 생성된다.
결국, (e) 는 newtime 을 보여준다. jockey 의 시간의 구현을 보여준다. recording 하는 동안, newtime 호출은 원래 구현© 과 리턴된 값을 기록한다. replay 동안, 그것은 실제적으로 시스템 콜의 실행없이 로그로 부터 값을 간단히 지원한다.
libjockey.so 는 같은 이름의 시스템콜의 새로운 구현이 꼭 제공되지 않는 이유가 한가지 크게 있다. -사실상, LD_PRELOAD 는 목적으로 자주 사용된다.
이유는 libc 또는 동적 링커 안에 만들어진 시스템 콜을 잃어버리게(놓치게) 될 것이다. 예를 들면, get.c 에 의한 read 호출.
이 내부의 호출들은 정적 링커에 의해서 미리 결정되고, 그들은 LD_PRELOAD 를 재정의하여 사용함으로서 단지 오버라이딩 할 수없다.
태스크(2)를 위해서, jockey 는 타겟 프로세스에서 발견되는 모든 성가신 CPU 명령어들을 다시 쓴다.
이것은 2 가지 방법이 있는데, slow 모드와 cached 모드이다. slow 모드에서는 jockey 는 처음 /proc/N/maps (N 은 타겟 프로세스 ID) 특별한 파일을 읽는다. 이것은 타겟 프로세스의 가상 메모리 매핑을 보여준다.
그것은 각 매핑된 공유 오브젝트 파일의 헤더를 읽는다. 텍스트 섹션의 위치를 발견하고, 각 텍스트 섹션을 스캔한다.
jockey 는 섹션에서 비결정적인 CPU 명령어를 찾는다. 그리고 그것을 패치 한다.
공유된 오브젝트 파일이 프로세스의 주소 공간에 새로이 로드되어 질 때, 그것은 또한 mmap 시스템 콜의 호출을 가로채고 같은 일을 한다. jockey 는 step(1) 와 (2) 동안, CPU 명령을 분석할 필요가 있다.
x86 의 복잡한 명령어 인코딩을 가지는 사소한 태스크는 아니다. 그것은 오픈소스 x86 디스어셈블러 라이브러리, 보기드문 경우를 위한, libdisasm 을 참고 하고 보통의 명령어들을 위한 혼성의 테이블기반의 파서를 사용된다.
opcode 와 오퍼랜드를 그들의 명령 길이에 맵하는 몇몇의 테이블은 빨리 우리에게 모든 명령 발생의 80% 이상을 분석하게 했다.
더욱더 이 기술을 사용하는 것은 그러나 1.5GHz Pentium-M 프로세서 상에서 약 350 밀리초 일반적인 리눅스 프로그램의 모든 CPU 명령어를 분석 하는 것이 어떤 사용자들에게는 무척 느려질 수 있다.
스타트업 지연시간을 훨씬 줄이기 위해서, jockey 는 또한 캐쉬 모드 명령어 패치를 사용한다. 여기 slow 모드가 끝난 후에 jockey 는 각 공유된 오브젝트 파일 ~/.jockey-sig 에 비결정적인 명령어의 위치를 쓴다.
다음 시간에 프로그램이 시작할 때, 프로세스의 가상 메모리를 스캔하지 않고 ~/.jockey-sig 파일을 읽는다. 오브젝트 파일의 타임스탬프가 아닌한 수정된다.
jockey 의 명령어 패치 방법은 ATOM 또는 valgrind 를 사용한 full 프로그램 바이너리 변환보다 간단하고 빠르다.
그것은 비결정적인 CPU 명령어들과 시스템 콜의 시작에 오직 몇 바이트 패치가 필요하다. 타겟 프로그램의 정지(휴식)은 본래적으로 수행한다.
실로 우리는 섹션 5 에서 보여줄 것이고, jockey 의 성능 오버헤드는 CPU 집중적인 프로그램에서 무시해도 좋다.



  • computer/job/rtcclab/rnr_paper8.txt
  • Last modified: 4 weeks ago
  • by likewind