变量是鬼鬼祟祟的。有时,它们会很快乐地呆在寄存器中,可是一回头就会跑到仓库中。为了优化,编译器或许会彻底将它们从窗口中抛出。不管变量在内存中的怎么移动,咱们都需求一些办法在调试器中盯梢和操作它们。这篇文章将会教你怎么处理调试器中的变量,并运用 libelfin 演示一个简略的完结。
系列文章索引
预备环境
断点
寄存器和内存
ELF 和 DWARF
源码和信号
源码级逐渐履行
源码级断点
仓库打开
处理变量
高档论题
在开端之前,请保证你运用的 libelfin 版本是我分支上的 fbreg。这包含了一些 hack 来支撑获取当时仓库帧的基址并评价方位列表,这些都不是由原生的 libelfin 供给的。你或许需求给 GCC 传递 -gdwarf-2 参数使其生成兼容的 DWARF 信息。可是在完结之前,我将具体阐明 DWARF 5 最新规范中的方位编码办法。假如你想要了解更多信息,那么你能够从这儿获取该规范。
DWARF 方位
某一给定时间的内存中变量的方位运用 DW_AT_location 特点编码在 DWARF 信息中。方位描绘能够是单个方位描绘、复合方位描绘或方位列表。
简略方位描绘:描绘了目标的一个接连的部分(通常是一切部分)的方位。简略方位描绘能够描绘可寻址存储器或寄存器中的方位,或短少方位(具有或不具有已知值)。比方,DW_OP_fbreg -32: 一个整个存储的变量 – 从仓库帧基址开端的32个字节。
复合方位描绘:依据片段描绘目标,每个目标能够包含在寄存器的一部分中或存储在与其他片段无关的存储器方位中。比方, DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2:前四个字节坐落寄存器 3 中,后两个字节坐落寄存器 10 中的一个变量。
方位列表:描绘了具有有限生计期或在生计期内更改方位的目标。比方:
[ 0]DW_OP_reg0
[ 1]DW_OP_reg3
[ 2]DW_OP_reg2
依据程序计数器的当时值,方位在寄存器之间移动的变量。
依据方位描绘的品种,DW_AT_locaTIon 以三种不同的办法进行编码。exprloc 编码简略和复合的方位描绘。它们由一个字节长度组成,后跟一个 DWARF 表达式或方位描绘。loclist 和 loclistptr 的编码方位列表,它们在 .debug_loclists 部分中供给索引或偏移量,该部分描绘了实践的方位列表。
DWARF 表达式
运用 DWARF 表达式核算变量的实践方位。这包含操作仓库值的一系列操作。有许多 DWARF 操作可用,所以我不会具体解说它们。相反,我会从每一个表达式中给出一些比如,给你一个可用的东西。别的,不要惧怕这些;libelfin将为咱们处理一切这些复杂性。
字面编码
DW_OP_lit0、DW_OP_lit1……DW_OP_lit31
将字面量压入仓库
DW_OP_addr
将地址操作数压入仓库
DW_OP_constu
将无符号值压入仓库
寄存器值
DW_OP_fbreg
压入在仓库帧基址找到的值,偏移给定值
DW_OP_breg0、DW_OP_breg1…… DW_OP_breg31
将给定寄存器的内容加上给定的偏移量压入仓库
仓库操作
DW_OP_dup
仿制仓库顶部的值
DW_OP_deref
将仓库顶部视为内存地址,并将其替换为该地址的内容
算术和逻辑运算
DW_OP_and
弹出仓库顶部的两个值,并压回它们的逻辑 AND
DW_OP_plus
与 DW_OP_and 相同,可是会添加值
操控流操作
DW_OP_le、DW_OP_eq、DW_OP_gt 等
弹出前两个值,比较它们,而且假如条件为真,则压入 1,否则为 0
DW_OP_bra
条件分支:假如仓库的顶部不是 0,则经过 offset 在表达式中向后或向后越过
输入转化
DW_OP_convert
将仓库顶部的值转换为不同的类型,它由给定偏移量的 DWARF 信息条目描绘
特别操作
DW_OP_nop
什么都不做!
DWARF 类型
DWARF 类型的表明需求满足强壮来为调试器用户供给有用的变量表明。用户常常期望能够在应用程序等级进行调试,而不是在机器等级进行调试,而且他们需求了解他们的变量正在做什么。
DWARF 类型与大多数其他调试信息一同编码在 DIE 中。它们能够具有指示其称号、编码、巨细、字节等的特点。很多的类型标签可用于表明指针、数组、结构体、typedef 以及 C 或 C++ 程序中能够看到的任何其他内容。
以这个简略的结构体为例:
struct test{int i;float j;int k[42];test* next;};
这个结构体的父 DIE 是这样的:
< 1><0x0000002a> DW_TAG_structure_typeDW_AT_name “test”DW_AT_byte_size 0x000000b8DW_AT_decl_file 0x00000001 test.cppDW_AT_decl_line 0x00000001
上面说的是咱们有一个叫做 test 的结构体,巨细为 0xb8,在 test.cpp 的第 1 行声明。接下来有许多描绘成员的子 DIE。
< 2><0x00000032> DW_TAG_memberDW_AT_name “i”DW_AT_type <0x00000063>DW_AT_decl_file 0x00000001 test.cppDW_AT_decl_line 0x00000002DW_AT_data_member_locaTIon 0< 2><0x0000003e> DW_TAG_memberDW_AT_name “j”DW_AT_type <0x0000006a>DW_AT_decl_file 0x00000001 test.cppDW_AT_decl_line 0x00000003DW_AT_data_member_locaTIon 4< 2><0x0000004a> DW_TAG_memberDW_AT_name “k”DW_AT_type <0x00000071>DW_AT_decl_file 0x00000001 test.cppDW_AT_decl_line 0x00000004DW_AT_data_member_locaTIon 8< 2><0x00000056> DW_TAG_memberDW_AT_name “next”DW_AT_type <0x00000084>DW_AT_decl_file 0x00000001 test.cppDW_AT_decl_line 0x00000005DW_AT_data_member_location 176(as signed = -80)
每个成员都有一个称号、一个类型(它是一个 DIE 偏移量)、一个声明文件和行,以及一个指向其成员地点的结构体的字节偏移。其类型指向如下。
< 1><0x00000063> DW_TAG_base_typeDW_AT_name “int”DW_AT_encoding DW_ATE_signedDW_AT_byte_size 0x00000004< 1><0x0000006a> DW_TAG_base_typeDW_AT_name “float”DW_AT_encoding DW_ATE_floatDW_AT_byte_size 0x00000004< 1><0x00000071> DW_TAG_array_typeDW_AT_type <0x00000063>< 2><0x00000076> DW_TAG_subrange_typeDW_AT_type <0x0000007d>DW_AT_count 0x0000002a< 1><0x0000007d> DW_TAG_base_typeDW_AT_name “sizetype”DW_AT_byte_size 0x00000008DW_AT_encoding DW_ATE_unsigned< 1><0x00000084> DW_TAG_pointer_typeDW_AT_type <0x0000002a>
如你所见,我笔记本电脑上的 int 是一个 4 字节的有符号整数类型,float是一个 4 字节的浮点数。整数数组类型经过指向 int 类型作为其元素类型,sizetype(能够认为是 size_t)作为索引类型,它具有 2a 个元素。 test * 类型是 DW_TAG_pointer_type,它引证 test DIE。
完结简略的变量读取器
如上所述,libelfin 将为咱们处理大部分复杂性。可是,它并没有完结用于表明可变方位的一切办法,而且在咱们的代码中处理这些将变得非常复杂。因而,我现在挑选只支撑 exprloc。请依据需求添加对更多类型表达式的支撑。假如你真的有勇气,请提交补丁到 libelfin 中来协助完结必要的支撑!
处理变量主要是将不同部分定位在存储器或寄存器中,读取或写入与之前相同。为了简略起见,我只会告知你怎么完结读取。
首要咱们需求告知 libelfin 怎么从咱们的进程中读取寄存器。咱们创立一个承继自 expr_context 的类并运用 ptrace 来处理一切内容:
class ptrace_expr_context : public dwarf::expr_context {public:ptrace_expr_context (pid_t pid) : m_pid{pid} {}dwarf::taddr reg (unsigned regnum) override {return get_register_value_from_dwarf_register(m_pid, regnum);}dwarf::taddr pc() override {struct user_regs_struct regs;ptrace(PTRACE_GETREGS, m_pid, nullptr, ®s);return regs.rip;}dwarf::taddr deref_size (dwarf::taddr address, unsigned size) override {//TODO take into account sizereturn ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr);}private:pid_t m_pid;};
读取将由咱们 debugger 类中的 read_variables 函数处理:
void debugger::read_variables() {using namespace dwarf;auto func = get_function_from_pc(get_pc());//…}
咱们上面做的榜首件事是找到咱们现在进入的函数,然后咱们需求循环拜访该函数中的条目来寻觅变量:
for (const auto& die : func) {if (die.tag == DW_TAG::variable) {//…}}
咱们经过查找 DIE 中的 DW_AT_location 条目获取方位信息:
auto loc_val = die[DW_AT::location];
接着咱们保证它是一个 exprloc,并恳求 libelfin 来评价咱们的表达式:
if (loc_val.get_type() == value::type::exprloc) {ptrace_expr_context context {m_pid};auto result = loc_val.as_exprloc().evaluate(&context);
现在咱们现已评价了表达式,咱们需求读取变量的内容。它能够在内存或寄存器中,因而咱们将处理这两种状况:
switch (result.location_type) {case expr_result::type::address:{auto value = read_memory(result.value);std::cout << at_name(die) << " (0x" << std::hex << result.value << ") = "<< value << std::endl;break;}case expr_result::type::reg:{auto value = get_register_value_from_dwarf_register(m_pid, result.value);std::cout << at_name(die) << " (reg " << result.value << ") = "<< value << std::endl;break;}default:throw std::runtime_error{"Unhandled variable location"};}
你能够看到,我依据变量的类型,打印输出了值而没有解说。期望经过这个代码,你能够看到怎么支撑编写变量,或许用给定的姓名查找变量。
最终咱们能够将它添加到咱们的指令解析器中:
else if(is_prefix(command, “variables”)) {read_variables();}
测验一下
编写一些具有一些变量的小功用,不必优化并带有调试信息编译它,然后检查是否能够读取变量的值。测验写入存储变量的内存地址,并检查程序改动的行为。
现已有九篇文章了,还剩最终一篇!下一次我会评论一些你或许会感兴趣的更高档的概念。现在你能够在这儿找到这个帖子的代码。