소개 | 메모리 주소에 중단점을 설정하는 것은 좋지만 가장 사용자 친화적인 도구를 제공하지는 않습니다. 우리는 코드와 동일한 추상화 수준에서 디버깅할 수 있도록 소스 코드 줄과 함수 항목 주소에 중단점을 설정할 수 있기를 원합니다. |
이 문서에서는 디버거에 소스 수준 중단점을 추가합니다. 우리가 이미 지원하는 모든 기능을 사용하면 처음에 생각했던 것보다 훨씬 쉽습니다. 또한 코드나 데이터를 찾고 연결 개념을 이해하는 데 유용한 기호의 유형과 주소를 가져오는 명령을 추가할 것입니다.
시리즈 색인이 링크는 후속 기사가 게시됨에 따라 점차적으로 적용됩니다.
Elves and dwarves 이 문서에서는 DWARF 디버깅 정보가 작동하는 방식과 이를 사용하여 기계 코드를 상위 수준 소스 코드에 매핑하는 방법을 설명합니다. DWARF에는 함수의 주소 범위와 추상화 계층 간의 코드 위치를 변환할 수 있는 라인 테이블이 포함되어 있다는 점을 기억하세요. 우리는 중단점을 구현하기 위해 이 함수들을 사용할 것입니다.
기능 입구함수 이름에 중단점을 설정하는 것은 오버로딩, 멤버 함수 등을 생각하면 조금 복잡할 수 있지만, 우리는 모든 컴파일 단위를 살펴보고 우리가 찾고 있는 이름과 일치하는 함수를 검색할 것입니다. DWARF 정보는 다음과 같습니다:
으아악DW_AT_name을 일치시키고 DW_AT_low_pc(함수의 시작 주소)를 사용하여 중단점을 설정하려고 합니다.
으아악이 코드에서 조금 이상해 보이는 유일한 점은 ++ 항목입니다. 문제는 함수의 DW_AT_low_pc가 함수에 대한 사용자 코드의 시작 주소를 가리키지 않고 프롤로그의 시작을 가리킨다는 것입니다. 컴파일러는 일반적으로 스택 저장 및 복원, 스택 포인터 조작 등을 수행하는 데 사용되는 함수의 프롤로그와 에필로그를 출력합니다. 이것은 우리에게 그다지 유용하지 않으므로 프롤로그 대신 사용자 코드의 첫 번째 줄을 얻기 위해 항목 줄을 하나씩 증가시킵니다. DWARF 라인 테이블에는 실제로 항목을 함수 프롤로그 뒤의 첫 번째 라인으로 표시하는 기능이 있지만 모든 컴파일러가 이를 출력하는 것은 아니기 때문에 원래 접근 방식을 사용했습니다.
소스 코드 라인상위 레벨 소스 라인에 중단점을 설정하려면 DWARF에서 라인 번호를 주소로 변환해야 합니다. 우리는 컴파일 단위를 반복하여 이름이 주어진 파일과 일치하는 것을 찾은 다음 주어진 줄에 해당하는 항목을 찾습니다.
DWARF는 다음과 같습니다:
으아악그래서 ab.cpp의 5번째 줄에 중단점을 설정하려면 해당 줄(0x004004e3)과 관련된 항목을 찾아서 중단점을 설정하면 됩니다.
으아악여기서 is_suffix 해킹을 수행하여 a/b/c.cpp에 c.cpp를 입력할 수 있습니다. 물론 실제로는 대소문자를 구분하는 경로 처리 라이브러리 등을 사용해야 하지만 게으릅니다. Entry.is_stmt는 행 테이블 항목이 명령문의 시작 부분으로 표시되었는지 확인합니다. 이 명령문은 중단점에 가장 적합하다고 생각되는 주소를 기반으로 컴파일러에 의해 설정됩니다.
기호 조회객체 파일 수준에서는 기호가 왕입니다. 함수는 기호로 이름이 지정되고, 전역 변수는 기호로 이름이 지정됩니다. 여러분도 기호를 얻고, 우리도 기호를 얻으며, 모두가 기호를 얻습니다. 주어진 개체 파일에서 일부 기호는 다른 개체 파일이나 공유 라이브러리를 참조할 수 있으며 링커는 기호 참조에서 실행 가능한 프로그램을 만듭니다.
기호는 바이너리 파일의 ELF 섹션에 저장되어 있는 적절한 이름의 기호 테이블에서 조회할 수 있습니다. 다행스럽게도 libelfin에는 이를 수행할 수 있는 좋은 인터페이스가 있으므로 모든 ELF 작업을 직접 처리할 필요가 없습니다. 우리가 다루고 있는 내용에 대한 아이디어를 제공하기 위해 readelf에 의해 생성된 바이너리의 .symtab 부분 덤프가 있습니다.
으아악오브젝트 파일에서 환경 설정에 사용되는 수많은 심볼을 볼 수 있으며, 마지막으로 메인 심볼을 볼 수 있습니다.
심볼의 종류, 이름, 값(주소)에 관심이 있습니다. 해당 유형의 Symbol_type 열거형이 있고 std::string을 이름으로 사용하고 std::uintptr_t를 주소로 사용합니다.
으아악우리는 libelfin에서 얻은 기호 유형을 열거형에 매핑해야 합니다. 종속성으로 인해 이 인터페이스가 중단되는 것을 원하지 않기 때문입니다. 운 좋게도 모든 이름을 같은 이름으로 골랐기 때문에 쉬웠습니다.
으아악마지막으로 기호를 찾아야 합니다. 설명을 위해 기호 테이블의 ELF 부분을 반복하고 거기에서 찾은 모든 기호를 std::벡터로 수집합니다. 더 스마트하게 구현하면 이름에서 기호로의 매핑을 설정할 수 있으므로 데이터를 한 번만 보면 됩니다.
std::vector debugger::lookup_symbol(const std::string& name) { std::vector syms; for (auto &sec : m_elf.sections()) { if (sec.get_hdr().type != elf::sht::symtab && sec.get_hdr().type != elf::sht::dynsym) continue; for (auto sym : sec.as_symtab()) { if (sym.get_name() == name) { auto &d = sym.get_data(); syms.push_back(symbol{to_symbol_type(d.type()), sym.get_name(), d.value}); } } } return syms; }
一如往常,我们需要添加一些更多的命令来向用户暴露功能。对于断点,我使用 GDB 风格的接口,其中断点类型是通过你传递的参数推断的,而不用要求显式切换:
else if(is_prefix(command, "break")) { if (args[1][0] == '0' && args[1][1] == 'x') { std::string addr {args[1], 2}; set_breakpoint_at_address(std::stol(addr, 0, 16)); } else if (args[1].find(':') != std::string::npos) { auto file_and_line = split(args[1], ':'); set_breakpoint_at_source_line(file_and_line[0], std::stoi(file_and_line[1])); } else { set_breakpoint_at_function(args[1]); } }
对于符号,我们将查找符号并打印出我们发现的任何匹配项:
else if(is_prefix(command, "symbol")) { auto syms = lookup_symbol(args[1]); for (auto&& s : syms) { std::cout << s.name << ' ' << to_string(s.type) << " 0x" << std::hex << s.addr << std::endl; } }
在一个简单的二进制文件上启动调试器,并设置源代码级别的断点。在一些 foo 函数上设置一个断点,看到我的调试器停在它上面是我这个项目最有价值的时刻之一。
符号查找可以通过在程序中添加一些函数或全局变量并查找它们的名称来进行测试。请注意,如果你正在编译 C++ 代码,你还需要考虑名称重整。
本文就这些了。下一次我将展示如何向调试器添加堆栈展开支持。
你可以在这里找到这篇文章的代码。
위 내용은 Linux 디버거의 소스 코드 수준 중단점 기술에 대한 심층 탐구!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!