E Debugging Tools 번역

E. Debugging Tools

Pintos의 디버깅을 위해 많은 툴이 마음대로 사용할 수 있습니다. 이 부록에서는 이들 중 몇 가지를 소개합니다.

E.1 printf()

printf()의 값을 과소평가하지 마십시오. pintos에서 printf()가 구현되는 방법은 락이 걸렸든 열렸든 커널 스레드에 있든 인터럽트 핸들러에 있든 거의 모든 커널에서 호출할 수 있다.

printf는 단순히 데이터를 검사하는 것 이상으로 유용합니다. 또한 유용한 오류 메시지 없이 커널이 충돌하거나 패닉이 발생한 경우에도 문제가 발생한 시기와 위치를 파악하는 데 도움이 될 수 있습니다. 이 전략은 실패했다고 의심되는 코드 조각 전체에 다른 문자열(예: “<1>”, “<2>, …)을 인쇄하기 위한 호출을 뿌리는 것입니다. 만약 여러분이 <1>이 인쇄된 것을 볼 수 없다면, 그 이전에 어떤 나쁜 일이 일어났고, 만약 여러분이 <1>을 볼 수 있지만 <2>를 볼 수 없다면, 그 두 점 사이에 나쁜 일이 일어났을 것입니다. 학습한 내용에 따라 의심되는 새로운 작은 코드 영역에 더 많은 printf() 호출을 삽입할 수 있습니다. 결국 문제를 하나의 문장으로 좁힐 수 있다. 관련 기법은 섹션 E.6 트리플 폴트를 참조하십시오.

E.2 ASSERT

ASSERT들은 문제를 조기에 발견할 수 있기 때문에 유용하다. 그렇지 않으면 알아차리기 전에 말이죠. 이상적으로는 각 함수는 인수의 유효성을 확인하는 일련의 ASSERT에서 시작해야 한다. (함수의 로컬 변수에 대한 이니셜라이저는 인수가 확인되기 전에 평가되므로 인수가 이니셜라이저에서 유효하다고 가정하지 않도록 주의해야 한다.) 여러분은 또한 여러분이 일이 잘못될 가능성이 있다고 생각되는 장소들에서 기능의 몸 전체에 ASSERT들을 뿌릴 수 있습니다. 특히 루프 불변량을 점검하는 데 유용합니다.

핀토스는 에 정의된 ASSERT 매크로를 제공하여 ASSERT을 확인합니다.

Macro: ASSERT (expression) 식 값을 테스트합니다. 0(거짓)으로 평가하면 커널 패닉이 발생합니다. 패닉 메시지에는 실패한 식, 파일 및 회선 번호, 역추적이 포함되어 문제를 찾는 데 도움이 됩니다. 자세한 내용은 섹션 E.4 역추적을 참조하십시오.

E.3 Function and Parameter Attributes

<debug.h> 에 이러한 매크로가 정의되어 있습니다. 컴파일러에게 함수 또는 함수 매개변수의 특수 속성을 알려준다. 그들의 확장은 GCC에 따라 다르다.

  • Macro: UNUSED

    함수 매개 변수에 추가하여 컴파일러에 매개 변수가 함수 내에서 사용되지 않을 수 있음을 알립니다. 그렇지 않으면 나타나는 경고를 억제합니다.

  • Macro: NO_RETURN

    컴파일러에게 함수가 돌아오지 않음을 알리기 위해 함수 프로토타입에 추가된다. 컴파일러가 경고와 코드 생성을 미세 조정할 수 있게 한다.

  • Macro: NO_INLINE

    컴파일러가 함수를 인라인으로 내보내지 않도록 하기 위해 함수 프로토타입에 추가된다. 간혹 역추적 품질을 개선하는 데 유용합니다(아래 참조).

  • Macro: PRINTF_FORMAT (format, first)

    함수 프로토타입에 추가하여 컴파일러에 함수가 printf(-like 형식 문자열)를 인수 번호 형식(1부터 시작)으로 사용하고 해당 값 인수가 먼저 번호가 매겨진 인수에서 시작됨을 알려준다. 이렇게 하면 잘못된 인수 유형을 전달했는지 컴파일러가 알려줄 수 있습니다.

E.4 Backtraces

커널 패닉이 발생하면 “역추적(backtrace)” 즉, 패닉이 발생했을 때 실행 중이던 함수 내부의 주소 목록으로 프로그램이 어떻게 위치하게 되었는지에 대한 요약을 인쇄합니다. 또한 <debug.h>에 프로토타입으로 된 debug_backtrace() 호출을 삽입할 수 있습니다., 코드에서 임의의 시점에서 역추적을 인쇄합니다.backtrace_all() 또한 <debug_.h> 에 선언되었습니다, 모든 나사산의 역추적을 인쇄합니다.

역추적된 주소는 원시 16진수 숫자로 나열되며 해석하기가 어렵습니다. 우리는 이것들을 함수 이름과 소스 파일 라인 번호로 변환하기 위해 역추적이라는 도구를 제공한다. 첫 번째 인수로 kernel.o의 이름을 지정하고 나머지 인수로 역추적(0x 접두사 포함)을 구성하는 16진수 숫자를 지정합니다. 각 주소에 해당하는 함수 이름과 소스 파일 라인 번호를 출력합니다.

변환된 형태의 역추적이 흐트러지거나 (예를 들어 함수 A가 함수 B 위에 나열되지만 B는 A를 호출하지 않음) 이치에 맞지 않으면 역추적이 스택에서 추출되므로 커널 스레드의 스택을 손상시키는 것이 좋습니다. 또는 역추적에 전달된 커널이 역추적을 생성한 커널과 동일하지 않을 수 있습니다.

때로는 역추적이 아무런 손상 없이 혼란스러울 수 있습니다. 컴파일러 최적화는 놀라운 동작을 일으킬 수 있다. 함수가 다른 함수를 최종 동작(테일 호출)으로 호출하면 호출 함수가 역추적에 전혀 나타나지 않을 수 있습니다. 마찬가지로, 함수 A가 다시 돌아오지 않는 다른 함수 B를 호출할 때, 컴파일러는 관련 없는 함수 C가 A 대신 역추적에 나타나도록 최적화할 수 있다. 함수 C는 단순히 A 바로 다음에 메모리에 있는 함수이다. 스레드 프로젝트에서 테스트 실패에 대한 역추적에서 일반적으로 볼 수 있습니다. 자세한 내용은 pass() Fails를 참조하십시오.

여기 예가 있어요. Pintos가 파일 시스템 프로젝트를 위한 실제 Pintos 제출에서 가져온 다음과 같은 콜 스택을 출력했다고 가정해 보십시오.

1
2
Call stack: 0xc0106eff 0xc01102fb 0xc010dc22 0xc010cf67 0xc0102319
0xc010325a 0x804812c 0x8048a96 0x8048ac8.

그런 다음 아래와 같은 역추적 유틸리티를 호출하여 역추적 정보를 잘라내어 명령줄에 붙여넣습니다. 현재 디렉터리에 kernel.o가 있다고 가정합니다. 물론 여기서 여백이 넘치더라도 단일 셸 명령줄에 다음 항목을 모두 입력할 수 있습니다.

1
2
backtrace kernel.o 0xc0106eff 0xc01102fb 0xc010dc22 0xc010cf67 
0xc0102319 0xc010325a 0x804812c 0x8048a96 0x8048ac8

그러면 역추적 출력은 다음과 같은 형태로 나타납니다.

1
2
3
4
5
6
7
8
9
0xc0106eff: debug_panic (lib/debug.c:86)
0xc01102fb: file_seek (filesys/file.c:405)
0xc010dc22: seek (userprog/syscall.c:744)
0xc010cf67: syscall_handler (userprog/syscall.c:444)
0xc0102319: intr_handler (threads/interrupt.c:334)
0xc010325a: intr_entry (threads/intr-stubs.S:38)
0x0804812c: (unknown)
0x08048a96: (unknown)
0x08048ac8: (unknown)

(당신이 컴파일한 소스 코드와 사용한 컴파일러가 다를 수 있기 때문에 당신의 커널 바이너리에서 위의 명령을 실행하면 당신은 동일한 주소를 볼 수 없을 것이다.)

역추적의 첫 줄은 커널 패닉을 구현하는 함수인 debug_panic()을 가리킨다. 역추적이 커널 패닉에서 비롯되기 때문에 디버그_패닉()은 역추적에 나타나는 첫 번째 함수이다.

두 번째 행은 file_seek()를 assertion failure의 결과로 패닉한 함수로 표시합니다. 이 예제에 사용된 소스 코드 트리에서 filesys/file.c의 줄 405는 assertion입니다.

1
ASSERT (file_ofs >= 0);

(이 줄은 어설션 실패 메시지에도 인용되었습니다.) 따라서 file_seek()는 음의 파일 오프셋 인수를 통과했기 때문에 패닉에 빠졌다.

세 번째 행은 file_seek()라고 하는 seek()를 나타내며, 아마도 오프셋 인수의 유효성을 검사하지 않을 것이다. 이 제출에서 seek()는 seek 시스템 호출을 구현합니다.

네 번째 줄은 시스템 호출 핸들러인 syscall_handler()가 seek()를 호출했음을 나타냅니다.

다섯 번째 및 여섯 번째 줄은 인터럽트 핸들러 입력 경로입니다.

나머지 행은 PHYS_BASE 아래의 주소에 대한 것입니다. 즉, 커널이 아닌 사용자 프로그램의 주소를 참조합니다. 커널이 패닉 상태에 빠졌을 때 어떤 사용자 프로그램이 실행되고 있었는지 알고 있다면 다음과 같이 사용자 프로그램에서 역추적을 다시 실행할 수 있습니다(물론 한 줄에 명령 입력).

1
2
3
backtrace tests/filesys/extended/grow-too-big 0xc0106eff 0xc01102fb
0xc010dc22 0xc010cf67 0xc0102319 0xc010325a 0x804812c 0x8048a96
0x8048ac8

결과는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
0xc0106eff: (unknown)
0xc01102fb: (unknown)
0xc010dc22: (unknown)
0xc010cf67: (unknown)
0xc0102319: (unknown)
0xc010325a: (unknown)
0x0804812c: test_main (...xtended/grow-too-big.c:20)
0x08048a96: main (tests/main.c:10)
0x08048ac8: _start (lib/user/entry.c:9)

명령행에 커널 및 사용자 프로그램 이름을 모두 지정할 수도 있습니다.

1
2
3
backtrace kernel.o tests/filesys/extended/grow-too-big 0xc0106eff
0xc01102fb 0xc010dc22 0xc010cf67 0xc0102319 0xc010325a 0x804812c
0x8048a96 0x8048ac8

결과는 결합된 역추적입니다.

1
2
3
4
5
6
7
8
9
10
11
In kernel.o:
0xc0106eff: debug_panic (lib/debug.c:86)
0xc01102fb: file_seek (filesys/file.c:405)
0xc010dc22: seek (userprog/syscall.c:744)
0xc010cf67: syscall_handler (userprog/syscall.c:444)
0xc0102319: intr_handler (threads/interrupt.c:334)
0xc010325a: intr_entry (threads/intr-stubs.S:38)
In tests/filesys/extended/grow-too-big:
0x0804812c: test_main (...xtended/grow-too-big.c:20)
0x08048a96: main (tests/main.c:10)
0x08048ac8: _start (lib/user/entry.c:9)

여기까지 읽어보신 분들을 위한 추가 팁이 있습니다. 역추적은 호출 스택을 제거할 수 있을 만큼 충분히 영리합니다. 헤더와 “.” 트레일러를 명령행에서 포함시키면 됩니다. 이렇게 하면 잘라내거나 붙여 넣는 데 문제가 생기는 것을 줄일 수고를 덜 수고를 덜 수 있습니다. 따라서 다음 명령은 처음 사용한 것과 동일한 출력을 인쇄합니다.

1
2
backtrace kernel.o Call stack: 0xc0106eff 0xc01102fb 0xc010dc22
0xc010cf67 0xc0102319 0xc010325a 0x804812c 0x8048a96 0x8048ac8.
Posted 2021-03-03