Linux调试器中如何实现断点
这篇文章将为大家详细讲解有关Linux调试器中如何实现断点,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。
随着后面文章的发布,这些链接会逐渐生效。
DWARF
Elves 和 dwarves 这篇文章,描述了 DWARF 调试信息是如何工作的,以及如何用它来将机器码映射到高层源码中。回想一下,DWARF 包含了函数的地址范围和一个允许你在抽象层之间转换代码位置的行表。我们将使用这些功能来实现我们的断点。
函数入口如果你考虑重载、成员函数等等,那么在函数名上设置断点可能有点复杂,但是我们将遍历所有的编译单元,并搜索与我们正在寻找的名称匹配的函数。DWARF 信息如下所示:
DW_TAG_compile_unitDW_AT_producerclangversion3.9.1(tags/RELEASE_391/final)DW_AT_languageDW_LANG_C_plus_plusDW_AT_name/super/secret/path/MiniDbg/examples/variable.cppDW_AT_stmt_list0x00000000DW_AT_comp_dir/super/secret/path/MiniDbg/buildDW_AT_low_pc0x00400670DW_AT_high_pc0x0040069cLOCAL_SYMBOLS:DW_TAG_subprogramDW_AT_low_pc0x00400670DW_AT_high_pc0x0040069cDW_AT_namefoo......DW_TAG_subprogramDW_AT_low_pc0x00400700DW_AT_high_pc0x004007a0DW_AT_namebar...
我们想要匹配 DW_AT_name 并使用 DW_AT_low_pc(函数的起始地址)来设置我们的断点。
voiddebugger::set_breakpoint_at_function(conststd::string&name){for(constauto&cu:m_dwarf.compilation_units()){for(constauto&die:cu.root()){if(die.has(dwarf::DW_AT::name)&&at_name(die)==name){autolow_pc=at_low_pc(die);autoentry=get_line_entry_from_pc(low_pc);++entry;//skipprologueset_breakpoint_at_address(entry->address);}}}}
这代码看起来有点奇怪的唯一一点是 ++entry。 问题是函数的 DW_AT_low_pc 不指向该函数的用户代码的起始地址,它指向 prologue 的开始。编译器通常会输出一个函数的 prologue 和 epilogue,它们用于执行保存和恢复堆栈、操作堆栈指针等。这对我们来说不是很有用,所以我们将入口行加一来获取用户代码的第一行而不是 prologue。DWARF 行表实际上具有一些功能,用于将入口标记为函数 prologue 之后的第一行,但并不是所有编译器都输出它,因此我采用了原始的方法。
源码行要在高层源码行上设置一个断点,我们要将这个行号转换成 DWARF 中的一个地址。我们将遍历编译单元,寻找一个名称与给定文件匹配的编译单元,然后查找与给定行对应的入口。
DWARF 看上去有点像这样:
.debug_line:linenumberinfoforasinglecuSourcelines(fromCU-DIEat.debug_infooffset0x0000000b):NSnewstatement,BBnewbasicblock,ETendoftextsequencePEprologueend,EBepiloguebeginIS=valISAnumber,DI=valdiscriminatorvalue[lno,col]NSBBETPEEBIS=DI=uri:"filepath"0x004004a7[1,0]NSuri:"/super/secret/path/a.hpp"0x004004ab[2,0]NS0x004004b2[3,0]NS0x004004b9[4,0]NS0x004004c1[5,0]NS0x004004c3[1,0]NSuri:"/super/secret/path/b.hpp"0x004004c7[2,0]NS0x004004ce[3,0]NS0x004004d5[4,0]NS0x004004dd[5,0]NS0x004004df[4,0]NSuri:"/super/secret/path/ab.cpp"0x004004e3[5,0]NS0x004004e8[6,0]NS0x004004ed[7,0]NS0x004004f4[7,0]NSET
所以如果我们想要在 ab.cpp 的第五行设置一个断点,我们将查找与行 (0x004004e3) 相关的入口并设置一个断点。
voiddebugger::set_breakpoint_at_source_line(conststd::string&file,unsignedline){for(constauto&cu:m_dwarf.compilation_units()){if(is_suffix(file,at_name(cu.root()))){constauto<=cu.get_line_table();for(constauto&entry:lt){if(entry.is_stmt&&entry.line==line){set_breakpoint_at_address(entry.address);return;}}}}}
我这里做了 is_suffix hack,这样你可以输入 c.cpp 代表 a/b/c.cpp 。当然你实际上应该使用大小写敏感路径处理库或者其它东西,但是我比较懒。entry.is_stmt 是检查行表入口是否被标记为一个语句的开头,这是由编译器根据它认为是断点的最佳目标的地址设置的。
符号查找当我们在对象文件层时,符号是王者。函数用符号命名,全局变量用符号命名,你得到一个符号,我们得到一个符号,每个人都得到一个符号。 在给定的对象文件中,一些符号可能引用其他对象文件或共享库,链接器将从符号引用创建一个可执行程序。
可以在正确命名的符号表中查找符号,它存储在二进制文件的 ELF 部分中。幸运的是,libelfin 有一个不错的接口来做这件事,所以我们不需要自己处理所有的 ELF 的事情。为了让你知道我们在处理什么,下面是一个二进制文件的 .symtab 部分的转储,它由 readelf 生成:
Num:ValueSizeTypeBindVisNdxName0:00000000000000000NOTYPELOCALDEFAULTUND1:00000000004002380SECTIONLOCALDEFAULT12:00000000004002540SECTIONLOCALDEFAULT23:00000000004002780SECTIONLOCALDEFAULT34:00000000004002c80SECTIONLOCALDEFAULT45:00000000004004300SECTIONLOCALDEFAULT56:00000000004004e40SECTIONLOCALDEFAULT67:00000000004005080SECTIONLOCALDEFAULT78:00000000004005280SECTIONLOCALDEFAULT89:00000000004005580SECTIONLOCALDEFAULT910:00000000004005700SECTIONLOCALDEFAULT1011:00000000004007140SECTIONLOCALDEFAULT1112:00000000004007200SECTIONLOCALDEFAULT1213:00000000004007240SECTIONLOCALDEFAULT1314:00000000004007500SECTIONLOCALDEFAULT1415:0000000000600e180SECTIONLOCALDEFAULT1516:0000000000600e200SECTIONLOCALDEFAULT1617:0000000000600e280SECTIONLOCALDEFAULT1718:0000000000600e300SECTIONLOCALDEFAULT1819:0000000000600ff00SECTIONLOCALDEFAULT1920:00000000006010000SECTIONLOCALDEFAULT2021:00000000006010180SECTIONLOCALDEFAULT2122:00000000006010280SECTIONLOCALDEFAULT2223:00000000000000000SECTIONLOCALDEFAULT2324:00000000000000000SECTIONLOCALDEFAULT2425:00000000000000000SECTIONLOCALDEFAULT2526:00000000000000000SECTIONLOCALDEFAULT2627:00000000000000000SECTIONLOCALDEFAULT2728:00000000000000000SECTIONLOCALDEFAULT2829:00000000000000000SECTIONLOCALDEFAULT2930:00000000000000000SECTIONLOCALDEFAULT3031:00000000000000000FILELOCALDEFAULTABSinit.c32:00000000000000000FILELOCALDEFAULTABScrtstuff.c33:0000000000600e280OBJECTLOCALDEFAULT17__JCR_LIST__34:00000000004005a00FUNCLOCALDEFAULT10deregister_tm_clones35:00000000004005e00FUNCLOCALDEFAULT10register_tm_clones36:00000000004006200FUNCLOCALDEFAULT10__do_global_dtors_aux37:00000000006010281OBJECTLOCALDEFAULT22completed.691738:0000000000600e200OBJECTLOCALDEFAULT16__do_global_dtors_aux_fin39:00000000004006400FUNCLOCALDEFAULT10frame_dummy40:0000000000600e180OBJECTLOCALDEFAULT15__frame_dummy_init_array_41:00000000000000000FILELOCALDEFAULTABS/super/secret/path/MiniDbg/42:00000000000000000FILELOCALDEFAULTABScrtstuff.c43:00000000004008180OBJECTLOCALDEFAULT14__FRAME_END__44:0000000000600e280OBJECTLOCALDEFAULT17__JCR_END__45:00000000000000000FILELOCALDEFAULTABS46:00000000004007240NOTYPELOCALDEFAULT13__GNU_EH_FRAME_HDR47:00000000006010000OBJECTLOCALDEFAULT20_GLOBAL_OFFSET_TABLE_48:00000000006010280OBJECTLOCALDEFAULT21__TMC_END__49:00000000006010200OBJECTLOCALDEFAULT21__dso_handle50:0000000000600e200NOTYPELOCALDEFAULT15__init_array_end51:0000000000600e180NOTYPELOCALDEFAULT15__init_array_start52:0000000000600e300OBJECTLOCALDEFAULT18_DYNAMIC53:00000000006010180NOTYPEWEAKDEFAULT21data_start54:00000000004007102FUNCGLOBALDEFAULT10__libc_csu_fini55:000000000040057043FUNCGLOBALDEFAULT10_start56:00000000000000000NOTYPEWEAKDEFAULTUND__gmon_start__57:00000000004007140FUNCGLOBALDEFAULT11_fini58:00000000000000000FUNCGLOBALDEFAULTUND__libc_start_main@@GLIBC_59:00000000004007204OBJECTGLOBALDEFAULT12_IO_stdin_used60:00000000006010180NOTYPEGLOBALDEFAULT21__data_start61:00000000004006a0101FUNCGLOBALDEFAULT10__libc_csu_init62:00000000006010280NOTYPEGLOBALDEFAULT22__bss_start63:00000000006010300NOTYPEGLOBALDEFAULT22_end64:00000000006010280NOTYPEGLOBALDEFAULT21_edata65:000000000040067044FUNCGLOBALDEFAULT10main66:00000000004005580FUNCGLOBALDEFAULT9_init
你可以在对象文件中看到用于设置环境的很多符号,最后还可以看到 main 符号。
我们对符号的类型、名称和值(地址)感兴趣。我们有一个该类型的 symbol_type 枚举,并使用一个 std::string 作为名称,std::uintptr_t 作为地址:
enumclasssymbol_type{notype,//Notype(e.g.,absolutesymbol)object,//Dataobjectfunc,//Functionentrypointsection,//Symbolisassociatedwithasectionfile,//Sourcefileassociatedwiththe};//objectfilestd::stringto_string(symbol_typest){switch(st){casesymbol_type::notype:return"notype";casesymbol_type::object:return"object";casesymbol_type::func:return"func";casesymbol_type::section:return"section";casesymbol_type::file:return"file";}}structsymbol{symbol_typetype;std::stringname;std::uintptr_taddr;};
我们需要将从 libelfin 获得的符号类型映射到我们的枚举,因为我们不希望依赖关系破环这个接口。幸运的是,我为所有的东西选了同样的名字,所以这样很简单:
symbol_typeto_symbol_type(elf::sttsym){switch(sym){caseelf::stt::notype:returnsymbol_type::notype;caseelf::stt::object:returnsymbol_type::object;caseelf::stt::func:returnsymbol_type::func;caseelf::stt::section:returnsymbol_type::section;caseelf::stt::file:returnsymbol_type::file;default:returnsymbol_type::notype;}};
最后我们要查找符号。为了说明的目的,我循环查找符号表的 ELF 部分,然后收集我在其中找到的任意符号到 std::vector 中。更智能的实现可以建立从名称到符号的映射,这样你只需要查看一次数据就行了。
std::vectordebugger::lookup_symbol(conststd::string&name){std::vectorsyms;for(auto&sec:m_elf.sections()){if(sec.get_hdr().type!=elf::sht::symtab&&sec.get_hdr().type!=elf::sht::dynsym)continue;for(autosym: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});}}}returnsyms;}添加命令
一如往常,我们需要添加一些更多的命令来向用户暴露功能。对于断点,我使用 GDB 风格的接口,其中断点类型是通过你传递的参数推断的,而不用要求显式切换:
elseif(is_prefix(command,"break")){if(args[1][0]=='0'&&args[1][1]=='x'){std::stringaddr{args[1],2};set_breakpoint_at_address(std::stol(addr,0,16));}elseif(args[1].find(':')!=std::string::npos){autofile_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]);}}
对于符号,我们将查找符号并打印出我们发现的任何匹配项:
elseif(is_prefix(command,"symbol")){autosyms=lookup_symbol(args[1]);for(auto&&s:syms){std::cout''"0x"测试一下
在一个简单的二进制文件上启动调试器,并设置源代码级别的断点。在一些 foo 函数上设置一个断点,看到我的调试器停在它上面是我这个项目最有价值的时刻之一。
符号查找可以通过在程序中添加一些函数或全局变量并查找它们的名称来进行测试。请注意,如果你正在编译 C++ 代码,你还需要考虑名称重整。
关于“Linux调试器中如何实现断点”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。