소개 | 드디어 이 시리즈의 마지막 기사에 이르렀습니다! 이번에는 원격 디버깅, 공유 라이브러리 지원, 식 평가, 멀티스레딩 지원 등 디버깅의 고급 개념에 대한 높은 수준의 개요를 제공하겠습니다. 이러한 아이디어는 구현하기가 더 복잡하므로 수행 방법을 자세히 설명하지는 않겠습니다. 하지만 질문이 있는 경우 이러한 개념에 대한 질문에 기꺼이 답변해 드리겠습니다. |
원격 디버깅은 임베디드 시스템이나 다양한 환경 디버깅에 매우 유용합니다. 또한 높은 수준의 디버거 작업과 운영 체제 및 하드웨어와의 상호 작용 사이에 미세한 경계를 설정합니다. 실제로 GDB 및 LLDB와 같은 디버거는 로컬 프로그램을 디버깅할 때에도 원격 디버거로 실행될 수 있습니다. 일반적인 구조는 다음과 같습니다:
디버그 아카이브
디버거는 명령줄을 통해 상호 작용하는 구성 요소입니다. IDE를 사용하는 경우 그 위에 머신 인터페이스를 통해 디버거와 통신하는 또 다른 레이어가 있을 수도 있습니다. 대상 머신(아마도 네이티브 머신과 동일)에는 이론적으로 주소에 중단점 설정과 같은 모든 하위 수준 디버깅 작업을 수행하는 매우 작은 운영 체제 디버깅 라이브러리 주변의 래퍼인 디버그 스텁이 있습니다. 요즘 디버그 스텁이 점점 더 커지고 있기 때문에 "이론적으로"라고 말합니다. 예를 들어 내 컴퓨터의 LLDB 디버그 스텁 크기는 7.6MB입니다. 디버그 스텁은 일부 운영 체제별 기능(이 경우 ptrace)을 사용하여 원격 프로토콜을 통해 디버깅된 프로세스 및 디버거와 통신합니다.
가장 일반적인 원격 디버깅 프로토콜은 GDB 원격 프로토콜입니다. 이는 디버거와 디버그 스텁 간에 명령과 정보를 전달하는 데 사용되는 텍스트 기반 패킷 형식입니다. 자세한 내용은 다루지 않겠습니다. 여기에서 더 자세한 내용을 읽어보실 수 있습니다. LLDB를 시작하고 log 활성화 gdb-remote packets 명령을 실행하면 원격 프로토콜을 통해 전송된 모든 패킷에 대한 추적을 얻을 수 있습니다. GDB에서는 set remotelogfile을 사용하여 동일한 작업을 수행할 수 있습니다.
간단한 예로 다음은 중단점을 설정하는 패킷입니다.
으아아아$는 패킷의 시작을 표시합니다. Z0은 메모리 중단점을 삽입하는 명령입니다. 400570과 1은 매개변수로 전자는 중단점을 설정하는 주소이고 후자는 특정 대상에 대한 중단점 유형 지정자입니다. 마지막으로 #43은 데이터가 손상되지 않았는지 확인하는 체크섬입니다.
GDB 원격 프로토콜은 사용자 정의 패킷으로 확장하기가 매우 쉽고, 이는 플랫폼이나 언어별 기능을 구현하는 데 유용합니다.
공유 라이브러리 및 동적 로딩 지원디버거는 중단점을 설정하고 소스 코드 수준 정보 및 기호 등을 얻을 수 있도록 디버깅 중인 프로그램에 의해 로드되는 공유 라이브러리를 알아야 합니다. 동적으로 연결된 라이브러리를 찾는 것 외에도 디버거는 런타임 시 dlopen을 통해 로드되는 라이브러리도 추적해야 합니다. 이러한 목적을 달성하기 위해 동적 링커는 교차 구조를 유지합니다. 이 구조는 공유 라이브러리 설명자의 연결된 목록과 연결된 목록이 업데이트될 때마다 호출되는 함수에 대한 포인터를 유지 관리합니다. 이 구조는 ELF 파일의 .dynamic 섹션에 저장되며 프로그램 실행 전에 초기화됩니다.
간단한 추적 알고리즘:
저는 이러한 개념에 대한 간단한 예를 작성했으며 여기에서 찾을 수 있습니다. 누구든지 관심이 있다면 앞으로 더 자세히 글을 쓸 수 있습니다.
표현식 계산표현식 평가는 사용자가 프로그램을 디버깅하는 동안 원래 소스 언어의 표현식을 평가할 수 있는 프로그램의 기능입니다. 예를 들어, LLDB 또는 GDB에서는 print foo()를 실행하여 foo 함수를 호출하고 결과를 인쇄할 수 있습니다.
표현의 복잡성에 따라 여러 가지 계산 방법이 있습니다. 표현식이 단순한 식별자인 경우 디버거는 이 시리즈의 마지막 부분에서 했던 것처럼 디버그 정보를 보고 변수를 찾아 값을 인쇄할 수 있습니다. 표현식이 다소 복잡한 경우 코드를 중간 표현식(IR)으로 컴파일하고 해석하여 결과를 얻는 것이 가능할 수 있습니다. 예를 들어 일부 표현식의 경우 LLDB는 Clang을 사용하여 표현식을 LLVM IR로 컴파일하고 해석합니다. 표현식이 더 복잡하거나 특정 함수를 호출해야 하는 경우 코드를 대상으로 JIT화하고 디버기의 주소 공간에서 실행해야 할 수도 있습니다. 여기에는 mmap을 호출하여 일부 실행 가능 메모리를 할당한 다음 컴파일된 코드를 해당 블록에 복사하고 실행하는 작업이 포함됩니다. LLDB는 LLVM의 JIT 기능을 사용하여 구현됩니다.
JIT 컴파일에 대해 더 자세히 알아보고 싶다면 해당 주제에 대한 Eli Bendersky의 기사를 적극 추천합니다.
多线程调试支持本系列展示的调试器仅支持单线程应用程序,但是为了调试大多数真实程序,多线程支持是非常需要的。支持这一点的最简单的方法是跟踪线程的创建,并解析 procfs 以获取所需的信息。
Linux 线程库称为 pthreads。当调用 pthread_create 时,库会使用 clone 系统调用来创建一个新的线程,我们可以用 ptrace 跟踪这个系统调用(假设你的内核早于 2.5.46)。为此,你需要在连接到调试器之后设置一些 ptrace 选项:
ptrace(PTRACE_SETOPTIONS, m_pid, nullptr, PTRACE_O_TRACECLONE);
现在当 clone 被调用时,该进程将收到我们的老朋友 SIGTRAP 信号。对于本系列中的调试器,你可以将一个例子添加到 handle_sigtrap 来处理新线程的创建:
case (SIGTRAP | (PTRACE_EVENT_CLONE << 8)): //get the new thread ID unsigned long event_message = 0; ptrace(PTRACE_GETEVENTMSG, pid, nullptr, message); //handle creation //...
一旦收到了,你可以看看 /proc//task/ 并查看内存映射之类来获得所需的所有信息。
GDB 使用 libthread_db,它提供了一堆帮助函数,这样你就不需要自己解析和处理。设置这个库很奇怪,我不会在这展示它如何工作,但如果你想使用它,你可以去阅读这个教程。
多线程支持中最复杂的部分是调试器中线程状态的建模,特别是如果你希望支持不间断模式或当你计算中涉及不止一个 CPU 的某种异构调试。
最后!呼!这个系列花了很长时间才写完,但是我在这个过程中学到了很多东西,我希望它是有帮助的。如果你有关于调试或本系列中的任何问题,请在 Twitter @TartanLlama或评论区联系我。如果你有想看到的其他任何调试主题,让我知道我或许会再发其他的文章。
위 내용은 Linux 디버거에 대한 고급 주제에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!